Почему 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)

3 ответов


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

теперь gcc имеет некоторую логику для обработки особых случаев printf (в частности, это оптимизация printf("something\n") и даже printf("%s\n", "something") to 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 global на этом этапе компиляции).


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

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


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


не уверен, что это убедительный ответ, но я ожидал, что компиляторы не оптимизируют printf("%d\n", 10) дело 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, есть проблема, что десятичная точка ".- зависит от локали. Следовательно, компилятор не может развернуть его до string constant для вас. Хотя мы только обсуждаем %d я упоминаю об этом здесь для полноты.