Обеспечение порядка операторов в C ++

111

Предположим, у меня есть ряд операторов, которые я хочу выполнить в фиксированном порядке. Я хочу использовать 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;
S2108887
источник
34
Если компилятор считает, что они независимы, а это не так, компилятор неисправен, и вам следует использовать лучший компилятор.
Дэвид Шварц
1
может __sync_synchronize()помочь?
vsz
3
@HowardHinnant: семантическая мощь стандарта C значительно улучшилась бы, если бы такая директива была определена, и если бы правила псевдонима были скорректированы так, чтобы исключить чтение, выполняемое после барьера данных, который был записан до него.
supercat
4
@DavidSchwartz В этом случае речь идет об измерении времени, fooнеобходимого для выполнения, которое компилятор может игнорировать при переупорядочении, так же, как ему разрешено игнорировать наблюдение из другого потока.
CodesInChaos

Ответы:

100

Я хотел бы попытаться дать более исчерпывающий ответ после обсуждения этого вопроса с комитетом по стандартам 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здесь.

Чендлер Каррут
источник
1
Спасибо за ваш ответ. Я отметил его как новый лучший ответ. Я мог бы сделать это раньше, но я не читал эту страницу stackoverflow уже много месяцев. Мне очень интересно использовать компилятор Clang для создания программ на C ++. Кроме всего прочего, мне нравится, что в Clang можно использовать символы Unicode в именах переменных. Думаю, я задам больше вопросов о Clang в Stackoverflow.
S2108887
5
Хотя я понимаю, как это предотвращает полную оптимизацию foo, не могли бы вы пояснить, почему это предотвращает Clock::now()переупорядочение вызовов относительно foo ()? Имеет ли optimzer должны предположить , что DoNotOptimizeи Clock::now()иметь доступ и может изменить некоторые общие глобальные состояния , которое в свою очередь, связать их на входы и выход? Или вы полагаетесь на какие-то текущие ограничения реализации оптимизатора?
MikeMB
2
DoNotOptimizeв этом примере - синтетически «наблюдаемое» событие. Это как если бы он условно распечатал видимый вывод на некоторый терминал с представлением ввода. Поскольку считывание часов также можно наблюдать (вы наблюдаете за течением времени), их нельзя переупорядочить без изменения наблюдаемого поведения программы.
Chandler Carruth
1
Я все еще не совсем понимаю понятие «наблюдаемый», если fooфункция выполняет некоторые операции, такие как чтение из сокета, которое может быть заблокировано на время, считается ли это наблюдаемой операцией? И так как readэто не «полностью известная» операция (верно?), Будет ли код в порядке?
ravenisadesk
«Основная проблема в том, что операционная семантика чего-то вроде сложения целых чисел полностью известна реализации». Но мне кажется, что проблема не в семантике сложения целых чисел, а в семантике вызова функции foo (). Если foo () не находится в одном модуле компиляции, откуда ему знать, что foo () и clock () не взаимодействуют?
Дэйв
59

Резюме:

Кажется, нет гарантированного способа предотвратить переупорядочение, но пока не включена оптимизация времени компоновки / полной программы, размещение вызываемой функции в отдельном модуле компиляции кажется довольно хорошей ставкой . (По крайней мере, с GCC, хотя логика подсказывает, что это вероятно и с другими компиляторами.) Это происходит за счет вызова функции - встроенный код по определению находится в той же единице компиляции и открыт для переупорядочения.

Оригинальный ответ:

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    $64, %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    $64, %rsp
        popq    %rbp
        ret

Но:

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

Теперь с foo () в качестве внешней функции:

#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    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

НО, если это связано с -flto (оптимизация времени компоновки):

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%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    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq
Джереми
источник
3
То же самое с MSVC и ICC. Clang - единственный, который, кажется, сохраняет исходную последовательность.
Коди Грей
3
вы нигде не используете t1 и t2, поэтому он может подумать, что результат можно отбросить, и изменить порядок кода
phuclv
3
@Niall - я не могу предложить ничего более конкретного, но я думаю, что мой комментарий ссылается на основную причину: компилятор знает, что foo () не может повлиять на now (), и наоборот, и то же самое касается переупорядочения. Кажется, это подтверждают различные эксперименты с функциями и данными extern scope. Это включает в себя статическую foo (), зависящую от переменной N в области файла - если N объявлено как статическое, происходит переупорядочение, тогда как если он объявлен нестатическим (т.е. он виден другим единицам компиляции и, следовательно, потенциально подвержен побочным эффектам extern функции, такие как now ()), не переупорядочиваются.
Джереми
3
@ Lưu Vĩnh Phúc: За исключением того, что сами звонки не отменяются. Еще раз, я подозреваю , что это происходит потому , что компилятор не знает , что может быть их побочные эффекты - но это действительно знают , что эти побочные эффекты не могут влиять на поведение Foo ().
Джереми
3
И последнее замечание: указание -flto (оптимизация времени компоновки) вызывает переупорядочение даже в случаях, не переупорядоченных иначе.
Джереми
20

Переупорядочивание может выполняться компилятором или процессором.

Большинство компиляторов предлагают специфичный для платформы метод предотвращения переупорядочения инструкций чтения-записи. В gcc это

asm volatile("" ::: "memory");

( Подробнее здесь )

Обратите внимание, что это только косвенно предотвращает операции переупорядочения, если они зависят от операций чтения / записи.

На практике я еще не встречал системы, в которой системный вызов Clock::now()бы имел такой же эффект, как и такой барьер. Чтобы убедиться, вы можете осмотреть получившуюся сборку.

Однако нередко тестируемая функция оценивается во время компиляции. Чтобы обеспечить «реалистичное» выполнение, вам может потребоваться получить входные данные для foo()ввода-вывода или volatileчтения.


Другой вариант - отключить встраивание для foo()- опять же, это зависит от компилятора и обычно не переносится, но будет иметь тот же эффект.

В gcc это будет __attribute__ ((noinline))


@Ruslan поднимает фундаментальный вопрос: насколько реалистично это измерение?

На время выполнения влияет множество факторов: один - это фактическое оборудование, на котором мы работаем, другой - одновременный доступ к общим ресурсам, таким как кеш, память, диск и ядра процессора.

Итак, что мы обычно делаем для получения сопоставимых таймингов: убедитесь, что они воспроизводимы с малой погрешностью. Это делает их несколько искусственными.

Производительность выполнения «горячего кеша» и «холодного кеша» может легко отличаться на порядок - но на самом деле это будет что-то среднее («теплый»?)

peterchen
источник
2
Ваш взлом asmвлияет на время выполнения операторов между вызовами таймера: код после затирания памяти должен перезагружать все переменные из памяти.
Руслан
@Ruslan: Их хак, а не мой. Есть разные уровни очистки, и выполнение чего-то подобного неизбежно для воспроизводимых результатов.
peterchen
2
Обратите внимание, что взлом с помощью asm помогает только в качестве барьера для операций, которые касаются памяти, а OP заинтересован в большем. Подробнее см. Мой ответ.
Chandler Carruth
11

Язык 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 с нулевыми побочными эффектами, но что-то вы выигрываете, некоторые теряете.

Якк - Адам Неврамонт
источник
19
«Другой подход - попытаться перехитрить ваш компилятор». Если эта фраза не является признаком того, что я провалился в кроличью нору, я не знаю, что это такое. :-)
Коди Грей
1
Я думаю, было бы полезно отметить, что время, необходимое для выполнения блока кода, не считается «наблюдаемым» поведением, которое компиляторы обязаны поддерживать . Если бы время выполнения блока кода было «наблюдаемым», то никакие формы оптимизации производительности были бы недопустимыми. Хотя для C и C ++ было бы полезно определить «причинно-следственный барьер», который потребовал бы, чтобы компилятор задерживал выполнение любого кода после барьера до тех пор, пока все побочные эффекты, возникшие до барьера, не были обработаны сгенерированным кодом [код, который хочет убедиться, что данные полностью ...
supercat
1
... распространяемые через аппаратные кэши, для этого потребуется использовать аппаратно-зависимые средства, но аппаратно-зависимые средства ожидания завершения всех опубликованных записей будут бесполезны без директивы барьера, гарантирующей, что все ожидающие записи, отслеживаемые компилятором должны быть отправлены на оборудование до того, как оборудование будет запрошено, чтобы гарантировать, что все отправленные записи завершены.] Я не знаю никакого способа сделать это на любом языке без использования фиктивного volatileдоступа или вызова внешнего кода.
supercat
4

Нет, не может. Согласно стандарту C ++ [intro.execution]:

14 Каждое вычисление значения и побочный эффект, связанный с полным выражением, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным со следующим оцениваемым полным выражением.

Полное выражение - это, по сути, оператор, заканчивающийся точкой с запятой. Как видите, в приведенном выше правиле указано, что операторы должны выполняться по порядку. Именно внутри операторов компилятору предоставляется больше свободы действий (т.е. при некоторых обстоятельствах ему разрешается оценивать выражения, составляющие оператор, в порядке, отличном от слева направо или в каком-либо другом порядке).

Обратите внимание, что условия применения правила «как если бы» здесь не выполняются. Неразумно думать, что какой-либо компилятор сможет доказать, что изменение порядка вызовов для получения системного времени не повлияет на наблюдаемое поведение программы. Если бы возникла ситуация, при которой два вызова для получения времени можно было бы переупорядочить без изменения наблюдаемого поведения, было бы крайне неэффективно создавать компилятор, который анализирует программу с достаточным пониманием, чтобы иметь возможность сделать это с уверенностью.

Смихи
источник
12
Тем не менее, все еще действует правило «как будто»
MM
18
По правилу «как если бы» компилятор может делать что угодно с кодом, пока он не изменяет наблюдаемое поведение. Время исполнения не наблюдается. Таким образом, он может переупорядочивать произвольные строки кода до тех пор, пока результат будет таким же (большинство компиляторов делают разумные вещи и не меняют порядок вызовов времени, но это не требуется)
Revolver_Ocelot
6
Время исполнения не наблюдается. Это довольно странно. С практической, нетехнической точки зрения, время выполнения (также известное как «производительность») очень заметно.
Frédéric Hamidi
3
Зависит от того, как вы измеряете время. Невозможно измерить количество тактов, затрачиваемых на выполнение некоторой части кода в стандартном C ++.
Питер
3
@dba Вы смешиваете несколько вещей вместе. Компоновщик больше не может создавать приложения Win16, это правда, но это потому, что они удалили поддержку для создания этого типа двоичного файла. Приложения WIn16 не используют формат PE. Это не означает, что у компилятора или компоновщика есть специальные знания о функциях API. Другая проблема связана с библиотекой времени выполнения. Нет абсолютно никаких проблем с получением последней версии MSVC для создания двоичного файла, работающего на NT 4. Я сделал это. Проблема возникает, как только вы пытаетесь установить ссылку в CRT, которая вызывает недоступные функции.
Коди Грей
2

Нет.

Иногда по правилу «как если бы» можно изменить порядок операторов. Это происходит не потому, что они логически независимы друг от друга, а потому, что эта независимость позволяет такому переупорядочиванию происходить без изменения семантики программы.

Очевидно, что перемещение системного вызова, который получает текущее время, не удовлетворяет этому условию. Компилятор, который сознательно или неосознанно делает это, несовместим и действительно глуп.

В общем, я бы не ожидал, что какое-либо выражение, приводящее к системному вызову, будет "второстепенным" даже агрессивно оптимизирующим компилятором. Ему просто недостаточно информации о том, что делает этот системный вызов.

Гонки легкости на орбите
источник
5
Я согласен, что это было бы глупо, но я бы не назвал это несоответствием . Компилятор может знать, что именно выполняет системный вызов в конкретной системе и имеет ли он побочные эффекты. Я бы ожидал, что компиляторы не будут переупорядочивать такие вызовы только для того, чтобы охватить общий случай использования, позволяя улучшить взаимодействие с пользователем, а не потому, что стандарт запрещает это.
Revolver_Ocelot
4
@Revolver_Ocelot: Оптимизации, которые изменяют семантику программы (хорошо, за исключением копирования), не соответствуют стандарту, согласны вы или нет.
Гонки легкости на орбите
6
В тривиальном случае int x = 0; clock(); x = y*2; clock();не существует определенных способов clock()взаимодействия кода с состоянием x. В соответствии со стандартом C ++ ему не нужно знать, что clock()делает - он может исследовать стек (и замечать, когда происходит вычисление), но это не проблема C ++ .
Yakk - Adam Nevraumont
5
Чтобы продолжить точку зрения Якка: это правда, что переупорядочивание системных вызовов так, чтобы результат первого был назначен, t2а второй - t1был бы несоответствующим и глупым, если бы эти значения использовались, в этом ответе не хватает того, что соответствующий компилятор может иногда переупорядочивать другой код в системном вызове. В этом случае, если он знает, что foo()делает (например, потому что он это встроил) и, следовательно, (грубо говоря) это чистая функция, тогда он может перемещать ее.
Стив Джессоп,
1
... опять же, грубо говоря, это потому, что нет гарантии, что фактическая реализация (хотя и не абстрактная машина) не будет спекулятивно вычислять y*yперед системным вызовом, просто для удовольствия. Также нет гарантии, что фактическая реализация не будет использовать результат этого умозрительного вычисления позже в какой бы точке ни xиспользовалась, поэтому ничего не будет делать между вызовами clock(). То же самое касается того, что делает встроенная функция foo, при условии, что она не имеет побочных эффектов и не может зависеть от состояния, которое может быть изменено clock().
Стив Джессоп,
0

noinline функция + встроенный черный ящик сборки + полные зависимости данных

Это основано на https://stackoverflow.com/a/38025837/895245, но поскольку я не видел четкого обоснования того, почему ::now()нельзя переупорядочить там, я бы предпочел быть параноиком и поместить его в функцию noinline вместе с как м.

Таким образом, я уверен, что переупорядочение не может произойти, поскольку noinline«связывает» ::nowзависимость между данными и данными.

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub вверх по течению .

Скомпилируйте и запустите:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

Единственным незначительным недостатком этого метода является то, что мы добавляем одну дополнительную callqинструкцию к inlineметоду. objdump -CDпоказывает, что mainсодержит:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

Итак, мы видим, что это fooбыло встроено, но get_clockне было, и окружаем его.

get_clock сам по себе, однако, чрезвычайно эффективен, состоящий из одной оптимизированной инструкции по вызову листа, которая даже не касается стека:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

Поскольку точность часов сама по себе ограничена, я думаю, что маловероятно, что вы сможете заметить временные эффекты одного дополнительного jmpq. Обратите внимание, что он callтребуется независимо от того, что он ::now()находится в общей библиотеке.

Вызов ::now()из встроенной сборки с зависимостью данных

Это было бы наиболее эффективное решение, преодолевающее даже лишнее, jmpqупомянутое выше.

К сожалению, это чрезвычайно сложно сделать правильно, как показано на: Вызов printf в расширенном встроенном ASM

Однако, если ваше измерение времени может быть выполнено непосредственно во встроенной сборке без вызова, тогда можно использовать этот метод. Это относится, например, к инструкциям gem5 magic Instrumentation , x86 RDTSC (не уверен, что это более репрезентативно) и, возможно, другим счетчикам производительности.

Связанные темы:

Протестировано с GCC 8.3.0, Ubuntu 19.04.

Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
1
Обычно вам не нужно форсировать сброс / перезагрузку с "+m"помощью, использование "+r"- гораздо более эффективный способ заставить компилятор материализовать значение, а затем предположить, что переменная изменилась.
Питер Кордес,