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

#include <stdio.h>
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 Compiler Explorer)

Ответ 1

Прежде всего, проблема не в 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 на этом этапе компиляции).

Ответ 2

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

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


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

Ответ 3

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

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

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

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

  • Иногда разработчик делил строку формата среди вызовов 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);
    

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

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