Обеспечение порядка операторов в C++
Предположим, у меня есть несколько операторов, которые я хочу выполнить в установленный порядок. Я хочу использовать 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;
6 ответов
Я хотел бы попытаться дать несколько более полный ответ после того, как это было обсуждено с Комитетом по стандартам C++. Помимо того, что я член комитета C++, я также разработчик компиляторов LLVM и Clang.
в принципе, нет способа использовать барьер или какую-либо операцию в последовательности для достижения этих преобразований. Основная проблема заключается в том, что операционная семантика чего-то вроде целочисленного сложения полностью известно для реализации. Он может имитировать их, он знает, что они не могут наблюдаться правильными программами, и всегда свободен перемещать их.
мы могли бы попытаться предотвратить это, но это будет иметь крайне негативные результаты и в конечном счете терпят неудачу.
во-первых, единственный способ предотвратить это в компиляторе-сказать ему, что все эти основные операции наблюдаемы. Проблема в том, что это тогда исключило бы подавляющее большинство компилятора процессы оптимизации. Внутри компилятора у нас по существу нет хороших механизмов для моделирования того, что времени наблюдается, но ничего больше. У нас даже нет хорошей модели какие операции требуют времени. Например, требуется ли время для преобразования 32-разрядного целого числа без знака в 64-разрядное целое число без знака? Это занимает нулевое время на x86-64, но на других архитектурах это занимает ненулевое время. Здесь нет общего правильного ответа.
но даже если нам удастся благодаря некоторому героизму в предотвращении переупорядочения компилятором этих операций нет никакой гарантии, что этого будет достаточно. Рассмотрим допустимый и соответствующий способ выполнения программы C++ на компьютере x86: DynamoRIO. Это система, которая динамически анализирует машинный код программы. Одна вещь, которую он может сделать, - это онлайн-оптимизация,и он даже способен спекулятивно выполнять весь диапазон основных арифметических инструкций за пределами времени. И такое поведение не является уникальным для динамические оценщики, фактический процессор x86 также будет спекулировать (гораздо меньшее количество) инструкций и динамически переупорядочивать их.
существенная реализация заключается в том, что тот факт, что арифметика не наблюдаема (даже на уровне синхронизации), является чем-то, что пронизывает слои компьютера. Это верно для компилятора, среды выполнения, и часто даже оборудование. Принуждение его к наблюдаемости резко ограничило бы компилятор, но оно также резко ограничило бы аппаратура.
но все это не должно заставить вас потерять надежду. Если вы хотите, чтобы время выполнения основных математических операций, мы хорошо изучили методы, которые надежно работают. Обычно они используются при выполнении микро-бенчмаркинг. Я говорил об этом на CppCon2015:https://youtu.be/nXaxk27zwlk
методы, показанные там, также предоставляются различными библиотеками micro-benchmark, такими как Google: https://github.com/google/benchmark#preventing-optimisation
ключ к этим методам-сосредоточиться на данных. Вы делаете вход в вычисление непрозрачным для оптимизатора, а результат вычисления непрозрачным для оптимизатора. Как только вы это сделаете, вы сможете точно рассчитать время. Давайте рассмотрим реалистичный вариант примера в исходном вопросе, но с определением foo
полностью виден для реализации. Я также извлек (non-portable) версия 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 , %rsp
.Ltmp1:
.cfi_def_cfa_offset 32
.Ltmp2:
.cfi_offset %rbx, -16
movl , 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 , %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
здесь.
резюме:
кажется, нет гарантированного способа предотвратить переупорядочивание, но пока оптимизация времени связи/полной программы не включена,размещение вызываемой функции в отдельном блоке компиляции кажется довольно хорошей ставкой. (По крайней мере, с GCC, хотя логика предполагает, что это вероятно и с другими компиляторами.) Это происходит за счет функции call-inlined код по определению находится в том же блоке компиляции и открыт для переупорядочивание.
оригинальный ответ:
GCC переупорядочивает вызовы под оптимизацией-O2:
#include <chrono>
static int foo(int x) // 'static' or not here doesn't affect ordering.
{
return x*2;
}
int fred(int x)
{
auto t1 = std::chrono::high_resolution_clock::now();
int y = foo(x);
auto t2 = std::chrono::high_resolution_clock::now();
return y;
}
GCC 5.3.0:
g++ -S --std=c++11 -O0 fred.cpp
:
_ZL3fooi:
pushq %rbp
movq %rsp, %rbp
movl %ecx, 16(%rbp)
movl 16(%rbp), %eax
addl %eax, %eax
popq %rbp
ret
_Z4fredi:
pushq %rbp
movq %rsp, %rbp
subq , %rsp
movl %ecx, 16(%rbp)
call _ZNSt6chrono3_V212system_clock3nowEv
movq %rax, -16(%rbp)
movl 16(%rbp), %ecx
call _ZL3fooi
movl %eax, -4(%rbp)
call _ZNSt6chrono3_V212system_clock3nowEv
movq %rax, -32(%rbp)
movl -4(%rbp), %eax
addq , %rsp
popq %rbp
ret
но:
g++ -S --std=c++11 -O2 fred.cpp
:
_Z4fredi:
pushq %rbx
subq , %rsp
movl %ecx, %ebx
call _ZNSt6chrono3_V212system_clock3nowEv
call _ZNSt6chrono3_V212system_clock3nowEv
leal (%rbx,%rbx), %eax
addq , %rsp
popq %rbx
ret
теперь, с foo () в качестве функции extern:
#include <chrono>
int foo(int x);
int fred(int x)
{
auto t1 = std::chrono::high_resolution_clock::now();
int y = foo(x);
auto t2 = std::chrono::high_resolution_clock::now();
return y;
}
g++ -S --std=c++11 -O2 fred.cpp
:
_Z4fredi:
pushq %rbx
subq , %rsp
movl %ecx, %ebx
call _ZNSt6chrono3_V212system_clock3nowEv
movl %ebx, %ecx
call _Z3fooi
movl %eax, %ebx
call _ZNSt6chrono3_V212system_clock3nowEv
movl %ebx, %eax
addq , %rsp
popq %rbx
ret
но, если это связано с-flto (оптимизация времени связи):
0000000100401710 <main>:
100401710: 53 push %rbx
100401711: 48 83 ec 20 sub x20,%rsp
100401715: 89 cb mov %ecx,%ebx
100401717: e8 e4 ff ff ff callq 100401700 <__main>
10040171c: e8 bf f9 ff ff callq 1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
100401721: e8 ba f9 ff ff callq 1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
100401726: 8d 04 1b lea (%rbx,%rbx,1),%eax
100401729: 48 83 c4 20 add x20,%rsp
10040172d: 5b pop %rbx
10040172e: c3 retq
переупорядочивание может выполняться компилятором или процессором.
большинство компиляторов предлагают метод, специфичный для платформы, чтобы предотвратить переупорядочение инструкций чтения и записи. На gcc это
asm volatile("" ::: "memory");
обратите внимание, что это только косвенно предотвращает операции переупорядочивания, если они зависят от чтения / записи.
на практике я еще не видел систему, где системный вызов в Clock::now()
имеет тот же эффект, что такой барьер. Вы можете проверить полученную сборку, чтобы быть уверенным.
однако не редкость, что тестируемая функция оценивается во время компиляции. Чтобы обеспечить "реалистичное" выполнение, вам может потребоваться получить входные данные для foo()
от ввода / вывода или volatile
читать.
другой вариант-отключить встроенное для foo()
- опять же, это специфичный компилятор и обычно не переносимый, но будет иметь то же самое эффект.
на gcc это будет __attribute__ ((noinline))
@Ruslan поднимает фундаментальный вопрос: насколько реалистично это измерение?
время выполнения зависит от многих факторов: один-это фактическое оборудование, на котором мы работаем, другой-одновременный доступ к общим ресурсам, таким как кэш, память, диск и ядра процессора.
Итак, что мы обычно делаем, чтобы получить сравнима тайминги: убедитесь, что они являются воспроизводимость С a низкая погрешность. Это делает их несколько искусственными.
производительность выполнения"горячего кэша" и "холодного кэша" может легко отличаться на порядок, но на самом деле это будет что - то среднее ("теплое"?)
язык C++ определяет, что можно наблюдать несколькими способами.
если foo()
не делает ничего наблюдаемого, тогда его можно полностью устранить. Если foo()
только вычисление, которое хранит значения в "локальном" состоянии (будь то в стеке или в объекте где-то),и компилятор может доказать, что никакой безопасно-производный указатель не может попасть в Clock::now()
код, тогда нет никаких наблюдаемых последствий для перемещения Clock::now()
звонки.
если foo()
взаимодействовал с файлом или дисплей, и компилятор не может доказать, что Clock::now()
тут не взаимодействовать с файлом или дисплеем, то переупорядочивание не может быть сделано, потому что взаимодействие с файлом или дисплеем является наблюдаемым поведением.
хотя вы можете использовать специфичные для компилятора хаки, чтобы заставить код не перемещаться (например, встроенная сборка), другой подход-попытаться перехитрить ваш компилятор.
создайте динамически загружаемую библиотеку. Загрузить его до код в вопрос.
эта библиотека раскрывает одну вещь:
namespace details {
void execute( void(*)(void*), void *);
}
и обертывает его так:
template<class F>
void execute( F f ) {
struct bundle_t {
F f;
} bundle = {std::forward<F>(f)};
auto tmp_f = [](void* ptr)->void {
auto* pb = static_cast<bundle_t*>(ptr);
(pb->f)();
};
details::execute( tmp_f, &bundle );
}
который упаковывает нулевую лямбду и использует динамическую библиотеку для ее запуска в контексте, который компилятор не может понять.
внутри динамической библиотеки, мы делаем:
void details::execute( void(*f)(void*), void *p) {
f(p);
}
что довольно просто.
теперь, чтобы изменить порядок вызовов execute
, он должен понимать динамическую библиотеку, которую он невозможно при компиляции тестового кода.
он все еще может ликвидировать foo()
s с нулевыми побочными эффектами, но вы выигрываете некоторые, вы теряете некоторые.
нет, не может. Согласно стандарту C++ [вступление.исполнение]:
14 каждое вычисление значения и побочный эффект, связанный с полное выражение применяется перед каждым вычислением стоимости и побочных эффект, связанный со следующим полным выражением для оценки.
полное выражение-это в основном оператор, заканчивающийся точкой с запятой. Как вы можете видеть выше правило оговаривает заявления должны быть выполнены в порядке. Это внутри утверждения о том, что компилятору разрешено больше свободы действий (т. е. при некоторых обстоятельствах разрешено оценивать выражения, которые составляют оператор в порядках, отличных от слева направо или чего-либо еще конкретного).
обратите внимание, что здесь не выполняются условия для применения правила as-if. Неразумно думать, что любой компилятор сможет доказать что переупорядочивание вызовов для получения системного времени не повлияет на наблюдаемое поведение программы. Если было обстоятельство, в котором два вызова, чтобы получить время, могут быть переупорядочены без изменения наблюдаемого поведения, было бы крайне неэффективно фактически создать компилятор, который анализирует программу с достаточным пониманием, чтобы иметь возможность сделать это с уверенностью.
нет.
иногда, по правилу "как будто", операторы могут быть переупорядочены. Это не потому, что они логически независимы друг от друга, а потому, что эта независимость позволяет такие реорганизации происходят без изменения семантики программы.
перемещение системного вызова, который получает текущее время, очевидно, не удовлетворяет этому условию. Компилятор, который сознательно или неосознанно делает это, несовместим и действительно глупый.
В общем, я бы не ожидал, что любое выражение, которое приводит к системному вызову, будет" угадано " даже агрессивно оптимизирующим компилятором. Он просто не знает достаточно о том, что делает этот системный вызов.