GCC/Clang не оптимизирует статическую глобальную переменную

Похоже, что 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 не оптимизирует глобальную переменную?, но в приведенном там ответе упоминаются внешние функции и потоки, ни один из которых здесь не применим.

Между constant и variable есть разница. test здесь не константа. Отметьте if (100 > 100) один раз.

SparKot 04.04.2022 21:12

Кроме того, я считать, что с extern int test; другая единица компиляции все еще может изменять переменную.

Mooing Duck 04.04.2022 21:13

@SparKot Я знаю, что это не константа, но return 123 все еще недоступен, что, я думаю, GCC сможет узнать, учитывая, что начальное значение test и все места, куда его можно записать, известны. Он работает с локальными переменными.

Heath Mitchell 04.04.2022 21:14

@MooingDuck У меня нет extern int test, разве что это как-то сработает из другого файла? В любом случае у меня есть -fwhole-program, поэтому он должен предположить, что этого не происходит, не так ли?

Heath Mitchell 04.04.2022 21:16

«Я ожидаю, что современные компиляторы смогут проследить ход программы и убрать проверку». ну тогда ваши ожидания неверны

Support Ukraine 04.04.2022 21:18

@SupportUkraine Так это потенциальная оптимизация, которую компиляторы просто не могут сделать в данный момент?

Heath Mitchell 04.04.2022 21:19

Вы даже можете создать меньшие примеры этого. Даже если единственным оператором в main является изменение переменной test, она не оптимизируется: godbolt.org/z/ano76fe4T

Jakob Stark 04.04.2022 21:26

@HeathMitchell Я этого не говорил ... Я просто сказал, что компиляторы, которые вы пробовали, этого не сделали. И они не должны.

Support Ukraine 04.04.2022 21:28

@SupportUkraine Я пытаюсь спросить вот что: могу ли я установить какой-то атрибут, который позволит компилятору оптимизировать это, или это нужно будет сделать вручную? Это уменьшенная версия реального примера с использованием инструмента wasm2c, где он увеличивает счетчик перед функцией, проверяет, превышает ли он значение, а затем снова уменьшает его. Вы можете увидеть это здесь: github.com/WebAssembly/wabt/blob/main/wasm2c/examples/fac/… Это портит некоторые оптимизации, поэтому я хотел бы автоматически удалить это.

Heath Mitchell 04.04.2022 21:31

Если сделать функцию static int abc(void), что-нибудь изменится?

Jonathan Leffler 04.04.2022 21:40

@JonathanLeffler Нет, это ничего не меняет :(

Heath Mitchell 04.04.2022 21:42

ХОРОШО. Очевидно, создание функции static сообщает компилятору, что единственный код, использующий abc(), находится в этом файле, что открывает дверь для встроенной оптимизации. Вы тоже пробовали inline (когда функция тоже static)? Вероятно, это ничего не изменит. Пробовали ли вы альтернативные варианты оптимизации (-O3 и т. д.)? Вы уверены, что не беспокоитесь о незначительной оптимизации — в каком более широком контексте это будет иметь измеримое значение?

Jonathan Leffler 04.04.2022 21:45

@MooingDuck Надеюсь, что нет.

Paul Sanders 04.04.2022 23:50

Можно представить себе хак, в котором у вас есть явно неизменная переменная static, которая управляет некоторым поведением программы, но затем вы исправляете исполняемый файл после компиляции, чтобы инициализировать его каким-то другим значением. С этим возникают всевозможные проблемы, но может случиться так, что такой код существует, и gcc хочет его не сломать. Просто дикая догадка.

Nate Eldredge 05.04.2022 08:47

@NateEldredge Теперь это программирование правильный :)

Paul Sanders 05.04.2022 09:41

@MooingDuck: Нет, к static int foo; нельзя получить доступ из другой единицы компиляции. extern int foo; - это то, как другим CU необходимо получить доступ к int foo; глобальному в C (все они не могут просто объявить int foo;, за исключением gcc -fcommon старого значения по умолчанию). Другой CU может вызвать abc() (например, из другого потока, запущенного в функции инициализации, которая выполнялась до main). Но я думаю, что любой аргумент в пользу этого наталкивается на UB, так что это просто непонятная пропущенная оптимизация. Большинство функций, которые изменяют переменную, действительно возвращают нет ее исходное значение каждый раз, так что это не то, что стоит искать.

Peter Cordes 05.04.2022 09:56

«Вы уверены, что не беспокоитесь о мизерных оптимизациях?» — наверное, да. а мне просто интересно почему так происходит

Heath Mitchell 05.04.2022 10:32

@NateEldredge: GCC оптимизирует статические переменные, которые на самом деле доступны только для чтения; это модификация test здесь мешает GCC заметить. (Потому что он не пытается доказать, что модификации никогда не изменяют значение; по-видимому, это редко встречается в реальных функциях и/или дорого искать.) Интересно, добавление if (test == 0) return 1; раннего выхода к начальному значению (или даже на пару выше) позволяет clang оптимизировать его: godbolt.org/z/vnx6z7G53

Peter Cordes 05.04.2022 11:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
18
122
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я думаю, что вы требуете слишком многого от большинства компиляторов. Хотя компилятору, вероятно, разрешено оптимизировать статическую переменную в соответствии с правило «как если бы» в стандарте, это, по-видимому, не реализовано во многих компиляторах, как вы указали для 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

(См. здесь для демонстрации)
Однако использование такой локальной переменной лишило бы смысла иметь глобальную переменную. Если вы никогда не пишете в него, вы можете также определить константу.

Оптимизация, о которой вы просите, происходит на этапе связывания. Вы уверены в этом?
Paul Sanders 04.04.2022 23:52

Существуют и другие оптимизации, связанные с static, которые выполняются во время компиляции, а не во время компоновки. Самый простой пример: никогда не используемая функция или переменная static будет удалена при компиляции и никогда не попадет в компоновщик. Так что, если бы эту оптимизацию собирались сделать, я бы ожидал, что это будет сделано компилятором, а не компоновщиком.

Nate Eldredge 05.04.2022 08:42

@PaulSanders нет, теперь я не уверен, что ты упомянул об этом. я отредактирую ответ

Jakob Stark 05.04.2022 09:34

Забавный факт: добавление if (test == 0) return 1; раннего выхода в abc() с начальным значением (или даже test == 1 или 2 при инициализации нулями) позволяет clang оптимизировать его: godbolt.org/z/vnx6z7G53 . Так что это просто пропущенная GCC-оптимизация (которую смысла особо не искать без раннего выхода). Если они не хотят поддерживать библиотечную функцию .init, запускающую несколько потоков и использующую PTRACE_SINGLESTEP на каждом из них для запуска приращения, но не уменьшения. Sinec, это все еще гонка данных UB, я не думаю, что GCC намеренно поддерживает это.

Peter Cordes 05.04.2022 11:25

Другие вопросы по теме