Забезпечення порядку замовлення заяв у 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;

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 ++. Крім усього іншого, мені подобається, що можна використовувати символи Unicode у змінних імен у Clang. Думаю, я запитаю більше запитань про Clang на Stackoverflow.
S2108887

5
Хоча я розумію, як це заважає повністю оптимізувати foo, чи можете ви трохи розробити, чому це не дозволяє Clock::now()передзвонити виклики відносно foo ()? Чи повинен оптимізатор припускати це DoNotOptimizeта Clock::now()мати доступ до та може змінювати якесь загальне глобальне стан, що, в свою чергу, прив'язує їх до входу та виходу? Або ви покладаєтесь на деякі поточні обмеження впровадження оптимізатора?
MikeMB

2
DoNotOptimizeу цьому прикладі - синтетично "спостережувана" подія. Це як би умовно надруковано видимий вихід на якийсь термінал із поданням входу. Оскільки читання годинників також спостерігається (ви спостерігаєте за тим, як проходить час), їх неможливо переупорядкувати, не змінивши поведінку програми, що спостерігається.
Чендлер Каррут

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. Кланг - єдиний, який, здається, зберігає початкову послідовність.
Коді Грей

3
ви ніде не використовуєте t1 і t2, тому може здатися, що результат можна відкинути і змінити код
phuclv

3
@Niall - я не можу запропонувати нічого конкретнішого, але я думаю, що мій коментар натякає на основну причину: компілятор знає, що foo () не може впливати зараз (), а також навпаки, і так само переупорядковує. Різні експерименти із залученням зовнішніх функцій сфери та даних, схоже, підтверджують це. Це включає наявність статичного foo () залежно від змінної файлової області N - якщо N оголошено статичною, відбувається переупорядкування, тоді як якщо він оголошений нестатичним (тобто він видимий для інших підрозділів компіляції, і, отже, потенційно може бути предметом побічних ефектів зовнішніх функцій, таких як тепер ()) переупорядкування не відбувається.
Джеремі

3
@ Lưu Vĩnh Phúc: За винятком того, що самі дзвінки не проходять. Ще раз, я підозрюю , що це відбувається тому , що компілятор не знає , що може бути їх побічні ефекти - але це дійсно знають , що ці побічні ефекти не можуть впливати на поведінку Foo ().
Джеремі

3
І остаточне зауваження: вказівка ​​-flto (оптимізація часу зв’язку) викликає переупорядкування навіть у випадках, які не є упорядкованими.
Джеремі

20

Переупорядкування може здійснюватися компілятором або процесором.

Більшість компіляторів пропонують певний платформенний метод для запобігання переупорядкуванню інструкцій читання-запису. Що стосується gcc, це так

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

( Більше інформації тут )

Зауважте, що це лише опосередковано запобігає переупорядкуванню операцій, якщо вони залежать від читання / запису.

На практиці я ще не бачив системи, де системний виклик Clock::now()має такий же ефект, як і такий бар'єр. Ви можете перевірити отриману збірку, щоб бути впевненим.

Однак нечасто, що тестова функція оцінюється під час компіляції. Щоб застосувати "реалістичне" виконання, можливо, вам знадобиться отримати вхід для вводу- foo()виводу або volatileчитання.


Іншим варіантом може бути відключення вбудовування для foo()- знову це специфічний для компілятора і, як правило, не портативний, але матиме такий же ефект.

Що стосується gcc, це було б __attribute__ ((noinline))


@ Руслан висуває фундаментальне питання: наскільки реалістичним є це вимірювання?

На час виконання впливає багато факторів: один - це власне обладнання, на якому ми працюємо, інший - це паралельний доступ до спільних ресурсів, таких як кеш, пам'ять, диск та ядра CPU.

Отже, що ми зазвичай робимо для отримання порівнянних термінів: переконайтеся, що вони відтворюються з низьким рівнем помилок. Це робить їх дещо штучними.

Виконання "гарячого кеша" та "холодного кеша" може легко відрізнятися на порядок - але насправді це буде щось середнє ("тепле"?)


2
Ваш хакер asmвпливає на час виконання висловлювань між викликами таймера: код після clobber пам'яті повинен перезавантажити всі змінні з пам'яті.
Руслан

@Ruslan: Їх хак, а не мій. Існують різні рівні очищення, і робити щось подібне неминуче для відтворюваних результатів.
peterchen

2
Зауважте, що злом "asm" допомагає лише як бар'єр для операцій, які торкаються пам'яті, і ОП цікавить більше. Дивіться мою відповідь для отримання більш детальної інформації.
Чендлер Каррут

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
Але все ще існує правило, ніби
ММ

18
За міру якщо правило компілятор може зробити що - небудь код, поки він не змінює спостерігається поведінка. Час виконання не спостерігається. Тож він може переупорядкувати арбутарні рядки коду до тих пір, поки результат буде однаковим (більшість компіляторів роблять розумні речі і не впорядковують тимчасові дзвінки, але це не потрібно)
Revolver_Ocelot

6
Час виконання не спостерігається. Це досить дивно. З практичної, нетехнічної точки зору, час виконання (він же "виконання") дуже добре спостерігається.
Фредерік Хаміді

3
Залежить від того, як ви вимірюєте час. Неможливо виміряти кількість тактових циклів, виконаних для виконання деякого коду коду в стандартному C ++.
Пітер

3
@dba Ви змішуєте кілька речей разом. Лінкер вже не може генерувати програми Win16, це досить правда, але це тому, що вони видалили підтримку для генерування цього типу бінарних файлів. Програми WIn16 не використовують формат PE. Це не означає, що або компілятор, або лінкер мають спеціальні знання про функції API. Інше питання пов'язане з бібліотекою часу виконання. Немає жодної проблеми з отриманням останньої версії MSVC для створення бінарного файлу, який працює на NT 4. Я це зробив. Проблема виникає, як тільки ви намагаєтесь зв’язатись у CRT, який викликає функції, недоступні.
Коді Грей

2

Немає.

Іноді за правилом "як-ніби" заяви можна переупорядкувати. Це не тому, що вони логічно незалежні один від одного, а тому, що така незалежність дозволяє таке переупорядкування відбуватися без зміни семантики програми.

Переміщення системного дзвінка, який отримує поточний час, очевидно, не відповідає цій умові. Компілятор, який це свідомо чи несвідомо робить, є невідповідним і справді нерозумним.

Взагалі, я б не очікував, що жоден вираз, який призводить до того, що системний виклик буде "другим відгадуванням", навіть агресивно оптимізуючим компілятором. Він просто недостатньо знає, що робить цей системний дзвінок.


5
Я погоджуюся, що це було б нерозумно, але я б не назвав це невідповідним . Компілятор може мати знання про те, що саме вимагає система конкретної системи та чи має побічні ефекти. Я би очікував, що компілятори не змінять порядок такого виклику, щоб просто охопити загальний випадок використання, що дозволяє покращити роботу користувачів, не тому, що стандарт забороняє це.
Revolver_Ocelot

4
@Revolver_Ocelot: Оптимізації, які змінюють семантику програми (добре, збережіть для копіювання elision), не відповідають стандарту, чи ви згодні чи ні.
Гонки легкості по орбіті

6
У тривіальному випадку int x = 0; clock(); x = y*2; clock();не існує визначених способів clock()взаємодії коду зі станом x. Відповідно до стандарту C ++, він не повинен знати, що clock()робить - він може вивчити стек (і помітити, коли відбувається обчислення), але це не проблема C ++ .
Якк - Адам Невраумон

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 , x86 RDTSC (не впевнений, чи це вже є репрезентативно) та, можливо, інших лічильників продуктивності.

Пов'язані теми:

Тестовано на GCC 8.3.0, Ubuntu 19.04.


1
Зазвичай вам не потрібно змушувати проливати / перезавантажувати "+m", використовуючи "+r"набагато ефективніший спосіб зробити компілятор матеріалізувати значення, а потім припустити, що змінна змінилася.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.