Я хотел бы попытаться дать более исчерпывающий ответ после обсуждения этого вопроса с комитетом по стандартам 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
#include <chrono>
template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
asm volatile("" : "+m"(const_cast<T &>(value)));
}
// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }
auto time_foo() {
using Clock = std::chrono::high_resolution_clock;
auto input = 42;
auto t1 = Clock::now(); // Statement 1
DoNotOptimize(input);
auto output = foo(input); // Statement 2
DoNotOptimize(output);
auto t2 = Clock::now(); // Statement 3
return t2 - t1;
}
Здесь мы гарантируем, что входные и выходные данные помечены как неоптимизируемые во время вычислений foo
, и только вокруг этих маркеров вычисляются тайминги. Поскольку вы используете данные для фиксации вычислений, гарантируется, что они останутся между двумя временными интервалами, но при этом само вычисление может быть оптимизировано. Результирующая сборка x86-64, созданная недавней сборкой Clang / LLVM, выглядит так:
% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
.text
.file "so.cpp"
.globl _Z8time_foov
.p2align 4, 0x90
.type _Z8time_foov,@function
_Z8time_foov: # @_Z8time_foov
.cfi_startproc
# BB#0: # %entry
pushq %rbx
.Ltmp0:
.cfi_def_cfa_offset 16
subq $16, %rsp
.Ltmp1:
.cfi_def_cfa_offset 32
.Ltmp2:
.cfi_offset %rbx, -16
movl $42, 8(%rsp)
callq _ZNSt6chrono3_V212system_clock3nowEv
movq %rax, %rbx
#APP
#NO_APP
movl 8(%rsp), %eax
addl %eax, %eax # This is "foo"!
movl %eax, 12(%rsp)
#APP
#NO_APP
callq _ZNSt6chrono3_V212system_clock3nowEv
subq %rbx, %rax
addq $16, %rsp
popq %rbx
retq
.Lfunc_end0:
.size _Z8time_foov, .Lfunc_end0-_Z8time_foov
.cfi_endproc
.ident "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
.section ".note.GNU-stack","",@progbits
Здесь вы можете увидеть, как компилятор оптимизирует вызов foo(input)
до одной инструкции, addl %eax, %eax
но не перемещает его за пределы времени и не устраняет его полностью, несмотря на постоянный ввод.
Надеюсь, это поможет, и комитет по стандартам C ++ рассматривает возможность стандартизации API, аналогичных приведенным DoNotOptimize
здесь.