Как функция GoogleDoNotOptimize() обеспечивает упорядочение операторов
Я пытаюсь понять, как должен работать именноGoogle .
Для полноты, вот его определение (для данных clang и неконстантных данных):
template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) {
asm volatile("" : "+r,m"(value) : : "memory");
}
Насколько я понимаю, мы можем использовать это в таком коде:
start_time = time();
bench_output = run_bench(bench_inputs);
result = time() - start_time;
Чтобы тест оставался в критическом разделе:
start_time = time();
DoNotOptimize(bench_inputs);
bench_output = run_bench(bench_inputs);
DoNotOptimise(bench_output);
result = time() - start_time;
В частности, я не понимаю, почему это гарантирует (не так ли?), Что
run_bench()
это не перемещается выше.
(Кто-то спросил именно об этом в этом комментарии , но я не понимаю ответа).
Как я понял, выше
DoNotOptimze()
делает несколько вещей:
- Он заставляет стек, так как он передается по ссылке C ++. У вас не может быть указателя на регистр, поэтому он должен быть в памяти.
- Поскольку сейчас находится в стеке, последующее затирание памяти (как это сделано в ограничениях asm) заставит компилятор предположить, что
value
одновременно читается и записывается при вызовеDoNotOptimize(value)
. - (мне не ясно, если
+r,m
ограничение актуально. Насколько мне известно, это говорит о том, что сам указатель может храниться в регистре или в памяти, но само значение указателя может быть прочитано и / или записано.)
И здесь у меня все становится нечетким.
Если также выделен стек, засорение памяти будет означать, что компилятор должен предположить, что потенциально читает. Следовательно, порядок утверждений может быть только следующим:
start_time = time(); // on the stack
DoNotOptimize(bench_inputs); // reads start_time, writes bench_inputs
bench_output = run_bench(bench_inputs)
Но если он хранится не в памяти, а вместо этого в регистре, то затирание памяти не будет затираться.
start_time
, Правильно? В этом случае желаемый порядок
start_time = time()
а также
DoNotOptimize(bench_inputs)
теряется, и компилятор может делать:
DoNotOptimize(bench_inputs); // only writes bench_inputs
bench_output = run_bench(bench_inputs)
start_time = time(); // in a register
Очевидно, я что-то неправильно понял. Кто-нибудь может помочь объяснить? Спасибо :)
Мне интересно, связано ли это с тем, что оптимизация переупорядочения происходит до выделения регистров, и, следовательно, предполагается, что в это время все будет выделено стеком. Но если бы это было так, то
DoNotOptimize()
будет лишним, так как ClobberMemory()
будет достаточно.
1 ответ
Резюме: заказывается по с помощью клобберов, как если бы это был другой вызов непрозрачной функции, которая могла бы изменить любое глобальное состояние.
заказывается по вычисление вывода из ввода посредством зависимости данных вычисления от ввода, а вывода от вычисления, как объяснил Чендлер Каррут в связанных с вами вопросах и ответах . Clobber не имеет значения для этой части.
clobber похож на вызов функции, не являющейся встроенной
Заявление содержит треп. Что касается оптимизатора, это эквивалентно вызову непрозрачной функции: предполагается, что он читает и записывает каждый глобально доступный объект 1. (Даже те, о которых этот модуль компиляции может не знать.)
Поскольку сам по себе не имеет встроенного определения ни в одном заголовке, он не может изменить порядок во время компиляции по той же причине, по которой компилятор не может переупорядочить вызовы в
(Гипотетика, которая могла бы быть встроенной и содержала только оператор, должна была бы использоваться, чтобы убедиться, что повторяющиеся вызовы не просто использовали вывод первого.
Сноска 1: Глобально достижимый = любой объект, на который может указывать любая гипотетическая глобальная переменная. т.е. что угодно, кроме локальных переменных в этой функции или памяти, только что выделенной с помощью
Как работает инструкция
Я думаю, вы серьезно не понимаете, как работает asm. сообщает компилятору материализовать значение в регистре (или в памяти, если он хочет), а затем использовать значение в конце (пустого) шаблона asm в качестве нового значения этого объекта C++.
Таким образом, это заставляет компилятор фактически материализовать (производить) значение где-то, что означает, что оно должно быть вычислено. И это означает, что он должен забыть то, что он ранее знал о значении (например, что это была постоянная времени компиляции 5, или неотрицательная, или что-то еще), потому что
Смысл
И на выходе, чтобы убедиться, что окончательный результат действительно материализован в регистре (или в памяти), вместо того, чтобы оптимизировать все вычисления, ведущие к неиспользованному результату. (Это то место, где
Таким образом, вычисление, которое вы хотите протестировать, должно происходить между двумя
Компилятор должен предположить, что оператор asm изменяет значение, например
Заблуждения о ссылочном аргументе и выборе
"m"
Я лишь частично рассказал о деталях вашей попытки рассуждать о
Функция C++, содержащая оператор, может быть встроена в строку, позволяя выполнять оптимизацию функции arg по ссылке. (Это даже объявлено
Конечный результат такой, как если бы оператор asm использовался непосредственно для переменной C++, переданной в DoNotOptimize. например
asm volatile("" : "+r,m"(foo) :: "memory")
Компилятор всегда может выбрать регистр, если захочет, например, выбрав загрузку значения переменной в регистр перед
Например, мы видим, что GCC делает это. (Думаю, я мог бы использовать
void foo(int *p)
{
asm volatile("nop # operand picked %0" : "+r,m" (p[4]) );
}
# GCC 11.2 -O2
foo(int*):
movl 16(%rdi), %eax
nop # operand picked %eax
movl %eax, 16(%rdi)
ret
vs. clang, если оставить значение в памяти, поэтому каждая инструкция в шаблоне asm будет обращаться к памяти, а не к регистру. (Если были инструкции).
# clang 12.0.1 -O2 -fPIE
foo(int*): # @foo(int*)
nop # operand picked 16(%rdi)
retq
Интересный факт: