Я хотів би спробувати дати дещо більш вичерпну відповідь після того, як це було обговорено з комітетом зі стандартів 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
цього.