Предположим, у меня есть ряд операторов, которые я хочу выполнить в фиксированном порядке. Я хочу использовать g ++ с уровнем оптимизации 2, чтобы некоторые операторы можно было переупорядочить. Какие инструменты нужны, чтобы обеспечить определенный порядок высказываний?
Рассмотрим следующий пример.
using Clock = std::chrono::high_resolution_clock;
auto t1 = Clock::now(); // Statement 1
foo(); // Statement 2
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
В этом примере важно, чтобы операторы 1-3 выполнялись в заданном порядке. Однако не может ли компилятор подумать, что оператор 2 независим от 1 и 3, и выполнить код следующим образом?
using Clock=std::chrono::high_resolution_clock;
foo(); // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
c++
c++11
operator-precedence
S2108887
источник
источник
__sync_synchronize()
помочь?foo
необходимого для выполнения, которое компилятор может игнорировать при переупорядочении, так же, как ему разрешено игнорировать наблюдение из другого потока.Ответы:
Я хотел бы попытаться дать более исчерпывающий ответ после обсуждения этого вопроса с комитетом по стандартам C ++. Помимо того, что я являюсь членом комитета C ++, я также занимаюсь разработкой компиляторов LLVM и Clang.
По сути, нет возможности использовать барьер или какую-либо операцию в последовательности для достижения этих преобразований. Основная проблема заключается в том, что операционная семантика чего-то вроде сложения целых чисел полностью известна реализации. Он может имитировать их, знает, что они не могут быть замечены правильными программами, и всегда может перемещать их.
Мы могли бы попытаться предотвратить это, но это привело бы к крайне негативным результатам и в конечном итоге потерпело бы неудачу.
Во-первых, единственный способ предотвратить это в компиляторе - сказать ему, что все эти базовые операции наблюдаемы. Проблема в том, что это помешает подавляющему большинству оптимизаций компилятора. Внутри компилятора, мы по существу не хорошие механизмы для модели , что время наблюдается , но ничего. У нас даже нет хорошей модели того, какие операции требуют времени . Например, требуется ли время для преобразования 32-разрядного целого числа без знака в 64-разрядное целое число? На x86-64 это занимает нулевое время, но на других архитектурах это ненулевое время. Здесь нет общего правильного ответа.
Но даже если нам удастся героически помешать компилятору переупорядочить эти операции, нет никакой гарантии, что этого будет достаточно. Рассмотрим допустимый и соответствующий способ выполнения вашей программы C ++ на машине x86: DynamoRIO. Это система, которая динамически оценивает машинный код программы. Одна вещь, которую он может сделать, - это онлайн-оптимизация, и он даже способен спекулятивно выполнять весь диапазон основных арифметических инструкций вне времени. И это поведение не является уникальным для динамических оценщиков, фактический процессор x86 также будет спекулировать (гораздо меньшее количество) инструкций и динамически их переупорядочивать.
Существенное осознание состоит в том, что тот факт, что арифметика не наблюдаема (даже на уровне синхронизации), пронизывает все уровни компьютера. Это верно для компилятора, среды выполнения и часто даже для оборудования. Принуждение к тому, чтобы он был наблюдаемым, резко ограничил бы компилятор, но также резко ограничил бы аппаратное обеспечение.
Но все это не должно лишать вас надежды. Если вы хотите рассчитать время выполнения основных математических операций, мы хорошо изучили методы, которые работают надежно. Обычно они используются при микротестировании . Я говорил об этом на CppCon2015: https://youtu.be/nXaxk27zwlk
Показанные там методы также предоставляются различными библиотеками микротестов, такими как Google: https://github.com/google/benchmark#preventing-optimization
Ключ к этим методам - сосредоточиться на данных. Вы делаете входные данные для вычислений непрозрачными для оптимизатора, а результат вычислений - для оптимизатора. Как только вы это сделаете, вы сможете точно рассчитать время. Давайте посмотрим на реалистичный вариант примера в исходном вопросе, но с определением,
foo
полностью видимым для реализации. Я также извлек (непереносимую) версию изDoNotOptimize
библиотеки Google Benchmark, которую вы можете найти здесь: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208Здесь мы гарантируем, что входные и выходные данные помечены как неоптимизируемые во время вычислений
foo
, и только вокруг этих маркеров вычисляются тайминги. Поскольку вы используете данные для фиксации вычислений, гарантируется, что они останутся между двумя временными интервалами, но при этом само вычисление может быть оптимизировано. Результирующая сборка x86-64, созданная недавней сборкой Clang / LLVM, выглядит так:Здесь вы можете увидеть, как компилятор оптимизирует вызов
foo(input)
до одной инструкции,addl %eax, %eax
но не перемещает его за пределы времени и не устраняет его полностью, несмотря на постоянный ввод.Надеюсь, это поможет, и комитет по стандартам C ++ рассматривает возможность стандартизации API, аналогичных приведенным
DoNotOptimize
здесь.источник
Clock::now()
переупорядочение вызовов относительно foo ()? Имеет ли optimzer должны предположить , чтоDoNotOptimize
иClock::now()
иметь доступ и может изменить некоторые общие глобальные состояния , которое в свою очередь, связать их на входы и выход? Или вы полагаетесь на какие-то текущие ограничения реализации оптимизатора?DoNotOptimize
в этом примере - синтетически «наблюдаемое» событие. Это как если бы он условно распечатал видимый вывод на некоторый терминал с представлением ввода. Поскольку считывание часов также можно наблюдать (вы наблюдаете за течением времени), их нельзя переупорядочить без изменения наблюдаемого поведения программы.foo
функция выполняет некоторые операции, такие как чтение из сокета, которое может быть заблокировано на время, считается ли это наблюдаемой операцией? И так какread
это не «полностью известная» операция (верно?), Будет ли код в порядке?Резюме:
Кажется, нет гарантированного способа предотвратить переупорядочение, но пока не включена оптимизация времени компоновки / полной программы, размещение вызываемой функции в отдельном модуле компиляции кажется довольно хорошей ставкой . (По крайней мере, с GCC, хотя логика подсказывает, что это вероятно и с другими компиляторами.) Это происходит за счет вызова функции - встроенный код по определению находится в той же единице компиляции и открыт для переупорядочения.
Оригинальный ответ:
GCC переупорядочивает вызовы при оптимизации -O2:
GCC 5.3.0:
g++ -S --std=c++11 -O0 fred.cpp
:Но:
g++ -S --std=c++11 -O2 fred.cpp
:Теперь с foo () в качестве внешней функции:
g++ -S --std=c++11 -O2 fred.cpp
:НО, если это связано с -flto (оптимизация времени компоновки):
источник
Переупорядочивание может выполняться компилятором или процессором.
Большинство компиляторов предлагают специфичный для платформы метод предотвращения переупорядочения инструкций чтения-записи. В gcc это
( Подробнее здесь )
Обратите внимание, что это только косвенно предотвращает операции переупорядочения, если они зависят от операций чтения / записи.
На практике я еще не встречал системы, в которой системный вызов
Clock::now()
бы имел такой же эффект, как и такой барьер. Чтобы убедиться, вы можете осмотреть получившуюся сборку.Однако нередко тестируемая функция оценивается во время компиляции. Чтобы обеспечить «реалистичное» выполнение, вам может потребоваться получить входные данные для
foo()
ввода-вывода илиvolatile
чтения.Другой вариант - отключить встраивание для
foo()
- опять же, это зависит от компилятора и обычно не переносится, но будет иметь тот же эффект.В gcc это будет
__attribute__ ((noinline))
@Ruslan поднимает фундаментальный вопрос: насколько реалистично это измерение?
На время выполнения влияет множество факторов: один - это фактическое оборудование, на котором мы работаем, другой - одновременный доступ к общим ресурсам, таким как кеш, память, диск и ядра процессора.
Итак, что мы обычно делаем для получения сопоставимых таймингов: убедитесь, что они воспроизводимы с малой погрешностью. Это делает их несколько искусственными.
Производительность выполнения «горячего кеша» и «холодного кеша» может легко отличаться на порядок - но на самом деле это будет что-то среднее («теплый»?)
источник
asm
влияет на время выполнения операторов между вызовами таймера: код после затирания памяти должен перезагружать все переменные из памяти.Язык C ++ определяет то, что можно наблюдать, несколькими способами.
Если
foo()
ничего не наблюдается, то это можно полностью устранить. Еслиfoo()
выполняется только вычисление, сохраняющее значения в «локальном» состоянии (будь то в стеке или где-то в объекте), и компилятор может доказать, что ни один безопасно полученный указатель не может попасть вClock::now()
код, тогда нет никаких наблюдаемых последствий для перемещениеClock::now()
звонков.Если при
foo()
взаимодействии с файлом или дисплеем компилятор не может доказать, чтоClock::now()
он не взаимодействует с файлом или дисплеем, то переупорядочение не может быть выполнено, потому что взаимодействие с файлом или дисплеем является наблюдаемым поведением.Хотя вы можете использовать специальные хаки для компилятора, чтобы заставить код не перемещаться (например, встроенная сборка), другой подход - попытаться перехитрить ваш компилятор.
Создайте динамически загружаемую библиотеку. Загрузите его до рассматриваемого кода.
Эта библиотека раскрывает одно:
и оборачивает его так:
который упаковывает нулевую лямбду и использует динамическую библиотеку для ее запуска в контексте, который компилятор не может понять.
Внутри динамической библиотеки мы делаем:
что довольно просто.
Теперь, чтобы изменить порядок вызовов
execute
, он должен понимать динамическую библиотеку, чего он не может во время компиляции вашего тестового кода.Он все еще может устранить
foo()
s с нулевыми побочными эффектами, но что-то вы выигрываете, некоторые теряете.источник
volatile
доступа или вызова внешнего кода.Нет, не может. Согласно стандарту C ++ [intro.execution]:
Полное выражение - это, по сути, оператор, заканчивающийся точкой с запятой. Как видите, в приведенном выше правиле указано, что операторы должны выполняться по порядку. Именно внутри операторов компилятору предоставляется больше свободы действий (т.е. при некоторых обстоятельствах ему разрешается оценивать выражения, составляющие оператор, в порядке, отличном от слева направо или в каком-либо другом порядке).
Обратите внимание, что условия применения правила «как если бы» здесь не выполняются. Неразумно думать, что какой-либо компилятор сможет доказать, что изменение порядка вызовов для получения системного времени не повлияет на наблюдаемое поведение программы. Если бы возникла ситуация, при которой два вызова для получения времени можно было бы переупорядочить без изменения наблюдаемого поведения, было бы крайне неэффективно создавать компилятор, который анализирует программу с достаточным пониманием, чтобы иметь возможность сделать это с уверенностью.
источник
Нет.
Иногда по правилу «как если бы» можно изменить порядок операторов. Это происходит не потому, что они логически независимы друг от друга, а потому, что эта независимость позволяет такому переупорядочиванию происходить без изменения семантики программы.
Очевидно, что перемещение системного вызова, который получает текущее время, не удовлетворяет этому условию. Компилятор, который сознательно или неосознанно делает это, несовместим и действительно глуп.
В общем, я бы не ожидал, что какое-либо выражение, приводящее к системному вызову, будет "второстепенным" даже агрессивно оптимизирующим компилятором. Ему просто недостаточно информации о том, что делает этот системный вызов.
источник
int x = 0; clock(); x = y*2; clock();
не существует определенных способовclock()
взаимодействия кода с состояниемx
. В соответствии со стандартом C ++ ему не нужно знать, чтоclock()
делает - он может исследовать стек (и замечать, когда происходит вычисление), но это не проблема C ++ .t2
а второй -t1
был бы несоответствующим и глупым, если бы эти значения использовались, в этом ответе не хватает того, что соответствующий компилятор может иногда переупорядочивать другой код в системном вызове. В этом случае, если он знает, чтоfoo()
делает (например, потому что он это встроил) и, следовательно, (грубо говоря) это чистая функция, тогда он может перемещать ее.y*y
перед системным вызовом, просто для удовольствия. Также нет гарантии, что фактическая реализация не будет использовать результат этого умозрительного вычисления позже в какой бы точке ниx
использовалась, поэтому ничего не будет делать между вызовамиclock()
. То же самое касается того, что делает встроенная функцияfoo
, при условии, что она не имеет побочных эффектов и не может зависеть от состояния, которое может быть измененоclock()
.noinline
функция + встроенный черный ящик сборки + полные зависимости данныхЭто основано на https://stackoverflow.com/a/38025837/895245, но поскольку я не видел четкого обоснования того, почему
::now()
нельзя переупорядочить там, я бы предпочел быть параноиком и поместить его в функцию noinline вместе с как м.Таким образом, я уверен, что переупорядочение не может произойти, поскольку
noinline
«связывает»::now
зависимость между данными и данными.main.cpp
GitHub вверх по течению .
Скомпилируйте и запустите:
Единственным незначительным недостатком этого метода является то, что мы добавляем одну дополнительную
callq
инструкцию кinline
методу.objdump -CD
показывает, чтоmain
содержит:Итак, мы видим, что это
foo
было встроено, ноget_clock
не было, и окружаем его.get_clock
сам по себе, однако, чрезвычайно эффективен, состоящий из одной оптимизированной инструкции по вызову листа, которая даже не касается стека:Поскольку точность часов сама по себе ограничена, я думаю, что маловероятно, что вы сможете заметить временные эффекты одного дополнительного
jmpq
. Обратите внимание, что онcall
требуется независимо от того, что он::now()
находится в общей библиотеке.Вызов
::now()
из встроенной сборки с зависимостью данныхЭто было бы наиболее эффективное решение, преодолевающее даже лишнее,
jmpq
упомянутое выше.К сожалению, это чрезвычайно сложно сделать правильно, как показано на: Вызов printf в расширенном встроенном ASM
Однако, если ваше измерение времени может быть выполнено непосредственно во встроенной сборке без вызова, тогда можно использовать этот метод. Это относится, например, к инструкциям gem5 magic Instrumentation , x86 RDTSC (не уверен, что это более репрезентативно) и, возможно, другим счетчикам производительности.
Связанные темы:
Протестировано с GCC 8.3.0, Ubuntu 19.04.
источник
"+m"
помощью, использование"+r"
- гораздо более эффективный способ заставить компилятор материализовать значение, а затем предположить, что переменная изменилась.