Почему GCC не оптимизирует этот вызов для printf?

#include  int main(void) { int i; scanf("%d", &i); if(i != 30) { return(0); } printf("i is equal to %d\n", i); } 

Похоже, что результирующая строка всегда будет «i равна 30», поэтому почему GCC не оптимизирует этот вызов printf с вызовом puts() или write() , например?

(Просто проверил сгенерированную сборку с gcc -O3 (версия 5.3.1) или в проводнике компилятора Godbolt )

Прежде всего, проблема не в том, if ; как вы видели, gcc видит через if и умеет передавать 30 прямо на printf .

Теперь у gcc есть некоторая логика для обработки особых случаев printf (в частности, она оптимизирует printf("something\n") и даже printf("%s\n", "something") чтобы puts("something") ), но он чрезвычайно специфичен и не идет намного дальше; printf("Hello %s\n", "world") , например, остается как-есть. Хуже того, любой из вышеперечисленных вариантов без задней новой строки остается нетронутым, даже если они могут быть преобразованы в fputs("something", stdout) .

Я предполагаю, что это сводится к двум основным проблемам:

  • два случая, описанные выше, являются чрезвычайно легкими образцами для реализации и происходят довольно часто, но для остальных, вероятно, это редко стоит усилий; если строка является постоянной и производительность важна, программист может позаботиться об этом легко – фактически, если производительность printf имеет решающее значение, он не должен полагаться на такую ​​оптимизацию, которая может нарушиться при малейшем изменении формата строка.

    Если вы спросите меня, даже только вышеперечисленные оптимизации puts уже «идут на точки стиля», вы на самом деле не получите серьезную производительность ни в чем, кроме искусственных тестов.

  • Когда вы начинаете выходить за пределы области %s\n , printf – это минное поле, потому что оно сильно зависит от среды выполнения; в частности, многие printf (к сожалению) затронуты языковой версией, плюс есть талисвины специфических для реализации особенностей и спецификаторов (и gcc может работать с printf от glibc, musl, mingw / msvcrt, … – и при компиляции время, когда вы не можете вызвать целевую среду выполнения C – подумайте, когда вы выполняете кросс-компиляцию).

    Я согласен с тем, что этот простой случай %d вероятно, безопасен, но я понимаю, почему они, вероятно, решили избежать чрезмерной умности и выполнять самые тупые и безопасные оптимизации здесь.


Для любопытного читателя, вот где эта оптимизация фактически реализована; как вы можете видеть, функция соответствует ограниченному числу очень простых случаев (и GIMPLE в сторону, не сильно изменилась, поскольку эта хорошая статья, излагающая их, была написана). Кстати, источник фактически объясняет, почему они не могут реализовать вариант fputs для случая, отличного от новой строки (нет простого способа ссылаться на глобальный stdout на этом этапе компиляции).

Современные компиляторы довольно умны, но недостаточно умны, чтобы предвидеть выход с использованием логики. В этом случае для программистов-программистов довольно просто оптимизировать этот код, но эта задача слишком сложна для машин. Фактически, предсказание вывода программы без запуска невозможно для программ (например, gcc). Для доказательства см. Проблему остановки .

В любом случае, вы не ожидаете, что все программы без входов будут оптимизированы для нескольких операторов puts() , поэтому GCC не будет оптимизировать этот код, содержащий один оператор scanf() .


Однако это не означает, что компиляторы не могут или не должны оптимизироваться для создания более оптимизированных исполнительных файлов. Хотя невозможно предсказать результат всех программ, вполне возможно и надеяться улучшить многие из них.

Не уверен, что это убедительный ответ, но я бы ожидал, что компиляторы не должны оптимизировать printf("%d\n", 10) case to puts("10") .

Зачем? Потому что этот случай может быть более сложным, чем вы думаете. Вот некоторые из проблем, о которых я могу думать в данный момент:

  1. Преобразование двоичных чисел в ASCII увеличивает размер строкового литерала и, следовательно, общий размер кода. Хотя это не имеет значения для небольших чисел, но если это printf("some number: %d", 10000) —- 5 цифр или более (при условии, что int 32-бит), размер строки увеличится, будет бить размер, сохраненный для целое число, и некоторые люди могут считать это недостатком. Да, с преобразованием я сохранил инструкцию «push to stack», но сколько байтов является инструкцией и сколько будет сохранено, зависит от архитектуры. Для компилятора нетривиально сказать, стоит ли это.

  2. Заполнение , если оно используется в форматах, также может увеличить размер расширенного строкового литерала. Пример: printf("some number: %10d", 100)

  3. Иногда разработчик делил строку формата среди вызовов printf по причинам размера кода:

     printf("%-8s: %4d\n", "foo", 100); printf("%-8s: %4d\n", "bar", 500); printf("%-8s: %4d\n", "baz", 1000); printf("%-8s: %4d\n", "something", 10000); 

    Преобразование их в разные строковые литералы может потерять преимущество в размере.

  4. Для %f , %e и %g существует проблема с десятичной точкой “.” зависит от локали. Следовательно, компилятор не может расширять его до строковой константы для вас. Хотя мы только обсуждаем о %d я упоминаю это здесь для полноты.