Похоже, что GCC не может отслеживать и оптимизировать программы, которые читают/записывают глобальные переменные в C/C++, даже если они являются static
, что должно позволить ему гарантировать, что другие единицы компиляции не изменят эту переменную.
При компиляции кода
static int test = 0;
int abc() {
test++;
if (test > 100) \
return 123;
--test;
return 1;
}
int main() {
return abc();
}
с флагами -Os
(для создания более короткой и читаемой сборки) и -fwhole-program
или -flto
с использованием GCC версии 11.2, я ожидаю, что это будет оптимизировано для return 1
или следующей сборки:
main:
mov eax, 1
ret
На самом деле это то, что получается, если test
является локальной переменной. Однако вместо этого производится следующее:
main:
mov eax, DWORD PTR test[rip]
mov r8d, 1
inc eax
cmp eax, 100
jle .L1
mov DWORD PTR test[rip], eax
mov r8d, 123
.L1:
mov eax, r8d
ret
Пример: https://godbolt.org/z/xzrPjanjd
Это происходит и с GGC, и с Clang, и с любым другим компилятором, который я пробовал. Я бы ожидал, что современные компиляторы смогут проследить ход программы и убрать проверку. Есть ли что-то, что я не рассматриваю, что может позволить чему-то внешнему по отношению к программе влиять на переменную, или это просто еще не реализовано ни в одном компиляторе?
Связано: Почему gcc не оптимизирует глобальную переменную?, но в приведенном там ответе упоминаются внешние функции и потоки, ни один из которых здесь не применим.
Кроме того, я считать, что с extern int test;
другая единица компиляции все еще может изменять переменную.
@SparKot Я знаю, что это не константа, но return 123
все еще недоступен, что, я думаю, GCC сможет узнать, учитывая, что начальное значение test
и все места, куда его можно записать, известны. Он работает с локальными переменными.
@MooingDuck У меня нет extern int test
, разве что это как-то сработает из другого файла? В любом случае у меня есть -fwhole-program
, поэтому он должен предположить, что этого не происходит, не так ли?
«Я ожидаю, что современные компиляторы смогут проследить ход программы и убрать проверку». ну тогда ваши ожидания неверны
@SupportUkraine Так это потенциальная оптимизация, которую компиляторы просто не могут сделать в данный момент?
Вы даже можете создать меньшие примеры этого. Даже если единственным оператором в main
является изменение переменной test
, она не оптимизируется: godbolt.org/z/ano76fe4T
@HeathMitchell Я этого не говорил ... Я просто сказал, что компиляторы, которые вы пробовали, этого не сделали. И они не должны.
@SupportUkraine Я пытаюсь спросить вот что: могу ли я установить какой-то атрибут, который позволит компилятору оптимизировать это, или это нужно будет сделать вручную? Это уменьшенная версия реального примера с использованием инструмента wasm2c
, где он увеличивает счетчик перед функцией, проверяет, превышает ли он значение, а затем снова уменьшает его. Вы можете увидеть это здесь: github.com/WebAssembly/wabt/blob/main/wasm2c/examples/fac/… Это портит некоторые оптимизации, поэтому я хотел бы автоматически удалить это.
Если сделать функцию static int abc(void)
, что-нибудь изменится?
@JonathanLeffler Нет, это ничего не меняет :(
ХОРОШО. Очевидно, создание функции static
сообщает компилятору, что единственный код, использующий abc()
, находится в этом файле, что открывает дверь для встроенной оптимизации. Вы тоже пробовали inline
(когда функция тоже static
)? Вероятно, это ничего не изменит. Пробовали ли вы альтернативные варианты оптимизации (-O3
и т. д.)? Вы уверены, что не беспокоитесь о незначительной оптимизации — в каком более широком контексте это будет иметь измеримое значение?
@MooingDuck Надеюсь, что нет.
Можно представить себе хак, в котором у вас есть явно неизменная переменная static
, которая управляет некоторым поведением программы, но затем вы исправляете исполняемый файл после компиляции, чтобы инициализировать его каким-то другим значением. С этим возникают всевозможные проблемы, но может случиться так, что такой код существует, и gcc хочет его не сломать. Просто дикая догадка.
@NateEldredge Теперь это программирование правильный :)
@MooingDuck: Нет, к static int foo;
нельзя получить доступ из другой единицы компиляции. extern int foo;
- это то, как другим CU необходимо получить доступ к int foo;
глобальному в C (все они не могут просто объявить int foo;
, за исключением gcc -fcommon
старого значения по умолчанию). Другой CU может вызвать abc()
(например, из другого потока, запущенного в функции инициализации, которая выполнялась до main
). Но я думаю, что любой аргумент в пользу этого наталкивается на UB, так что это просто непонятная пропущенная оптимизация. Большинство функций, которые изменяют переменную, действительно возвращают нет ее исходное значение каждый раз, так что это не то, что стоит искать.
«Вы уверены, что не беспокоитесь о мизерных оптимизациях?» — наверное, да. а мне просто интересно почему так происходит
@NateEldredge: GCC оптимизирует статические переменные, которые на самом деле доступны только для чтения; это модификация test
здесь мешает GCC заметить. (Потому что он не пытается доказать, что модификации никогда не изменяют значение; по-видимому, это редко встречается в реальных функциях и/или дорого искать.) Интересно, добавление if (test == 0) return 1;
раннего выхода к начальному значению (или даже на пару выше) позволяет clang оптимизировать его: godbolt.org/z/vnx6z7G53
Я думаю, что вы требуете слишком многого от большинства компиляторов. Хотя компилятору, вероятно, разрешено оптимизировать статическую переменную в соответствии с правило «как если бы» в стандарте, это, по-видимому, не реализовано во многих компиляторах, как вы указали для GCC и Clang.
Две причины, о которых я мог подумать, это:
В вашем примере, очевидно, оптимизация времени ссылки решила встроить функцию abc
, но не оптимизировала переменную test
. Для этого потребуется анализ семантики чтения/записи переменной test
. Это очень сложно сделать в общем виде. Это может быть возможно в простом случае, который вы предоставили, но что-то более сложное было бы очень сложно.
Вариант использования таких оптимизаций встречается редко. Глобальные переменные чаще всего используются для представления некоторого общего глобального состояния. Мне нет смысла оптимизировать это. Усилия по реализации такой функции в компиляторе/компоновщике были бы большими по сравнению с выгодой для большинства программ.
Добавление
По-видимому, GCC оптимизирует переменную, если вы получаете к ней доступ только для чтения. Если вы скомпилируете следующее:
static int test = 0;
int abc() {
int test_ = test;
test_++;
if (test_ > 100) \
return 123;
--test_;
return 1;
}
int main() {
return abc();
}
Когда вы читаете переменную один раз в локальную переменную и никогда не записываете в нее, она оптимизируется до:
main:
mov eax, 1
ret
(См. здесь для демонстрации)
Однако использование такой локальной переменной лишило бы смысла иметь глобальную переменную. Если вы никогда не пишете в него, вы можете также определить константу.
Существуют и другие оптимизации, связанные с static
, которые выполняются во время компиляции, а не во время компоновки. Самый простой пример: никогда не используемая функция или переменная static
будет удалена при компиляции и никогда не попадет в компоновщик. Так что, если бы эту оптимизацию собирались сделать, я бы ожидал, что это будет сделано компилятором, а не компоновщиком.
@PaulSanders нет, теперь я не уверен, что ты упомянул об этом. я отредактирую ответ
Забавный факт: добавление if (test == 0) return 1;
раннего выхода в abc()
с начальным значением (или даже test == 1
или 2 при инициализации нулями) позволяет clang оптимизировать его: godbolt.org/z/vnx6z7G53 . Так что это просто пропущенная GCC-оптимизация (которую смысла особо не искать без раннего выхода). Если они не хотят поддерживать библиотечную функцию .init
, запускающую несколько потоков и использующую PTRACE_SINGLESTEP
на каждом из них для запуска приращения, но не уменьшения. Sinec, это все еще гонка данных UB, я не думаю, что GCC намеренно поддерживает это.
Между
constant
иvariable
есть разница.test
здесь не константа. Отметьтеif (100 > 100)
один раз.