C ++でのステートメント順序の強制


111

決まった順序で実行したいステートメントがいくつかあるとします。最適化レベル2でg ++を使用したいので、一部のステートメントを並べ替えることができます。ステートメントの特定の順序を強制するためにどのツールが必要ですか?

次の例を考えてみましょう。

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
コンパイラが独立していないと判断した場合、コンパイラは壊れているため、より優れたコンパイラを使用する必要があります。
David Schwartz 2016年


1
可能性が__sync_synchronize()任意の助けになりますか?
vsz 2016年

3
@HowardHinnant:そのようなディレクティブが定義されていて、その前に書き込まれたデータのバリアの後に実行された読み取りを除外するようにエイリアスルールが調整されている場合、標準Cのセマンティックパワーは大幅に向上します。
スーパーキャット2016年

4
@DavidSchwartzこの場合fooは、実行にかかる時間を測定することです。これは、別のスレッドからの観測を無視できるのと同じように、並べ替え時にコンパイラが無視できるようにすることです。
CodesInChaos 2016年

回答:


100

これがC ++標準委員会で議論された後、もう少し包括的な答えを提供したいと思います。C ++委員会のメンバーであることに加えて、私はLLVMおよびClangコンパイラーの開発者でもあります。

基本的に、これらの変換を実現するために、シーケンスでバリアまたは何らかの操作を使用する方法はありません。基本的な問題は、整数の加算などの動作セマンティクスが実装に完全に認識さていることです。それはそれらをシミュレートでき、正しいプログラムでは観察できないことを認識しており、常に自由に移動できます。

これを防ぐことはできますが、結果は非常に悪い結果となり、最終的には失敗します。

まず、コンパイラでこれを防ぐ唯一の方法は、これらの基本的な操作がすべて監視可能であることを伝えることです。問題は、これにより、圧倒的多数のコンパイラ最適化が妨げられることです。コンパイラーの内部には、タイミングが観察可能であることをモデル化する優れたメカニズムは基本的にありませんが、それ以外には何もありません。どの操作に時間がかかるかを示す適切なモデルさえありません。例として、32ビットの符号なし整数を64ビットの符号なし整数に変換するには時間がかかりますか?x86-64では時間がかかりませんが、他のアーキテクチャでは時間がかかりません。ここには一般的に正しい答えはありません。

しかし、これらの操作をコンパイラが再順序付けできないようにすることでいくつかの英雄的成功を収めたとしても、これで十分である保証はありません。x86マシンでDynamoRIOを実行するための有効で準拠した方法を検討してください。プログラムのマシンコードを動的に評価するシステムです。それができることの1つは、オンライン最適化であり、タイミングの外にある基本的な算術命令の範囲全体を投機的に実行することさえできます。また、この動作は動的エバリュエーターに固有のものではなく、実際のx86 CPUは(はるかに少ない数の)命令を推測し、それらを動的に並べ替えます。

本質的な認識は、演算が(タイミングレベルでも)観測できないという事実は、コンピューターのレイヤーに浸透するものであるということです。これは、コンパイラー、ランタイム、さらにはハードウェアにも当てはまります。強制的に監視可能にすると、コンパイラが劇的に抑制されますが、ハードウェアも劇的に抑制されます。

しかし、これらすべてがあなたの希望を失う原因にはなりません。基本的な数学演算の実行時間を計測したい場合は、確実に機能する手法を十分に検討しました。通常、これらはマイクロベンチマークを行うときに使用されます。CppCon2015でこれについて講演しました:https ://youtu.be/nXaxk27zwlk

ここに示されている手法は、Googleのようなさまざまなマイクロベンチマークライブラリによっても提供されています。https//github.com/google/benchmark#preventing-optimization

これらの手法の鍵は、データに焦点を当てることです。計算への入力をオプティマイザに対して不透明にし、計算の結果をオプティマイザに対して不透明にします。これを実行したら、確実にタイミングを合わせることができます。元の質問の例の現実的なバージョンを見てみましょう。ただしfoo、実装から完全に見えるように定義されています。私はDoNotOptimizeここで見つけることができるGoogleベンチマークライブラリから(非ポータブル)バージョンも抽出しました: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、それらのマーカーの周りのみがタイミングが計算されるようにします。データを使用して計算を正確に把握しているため、2つのタイミングの間に留まることが保証されていますが、計算自体を最適化することができます。Clang / LLVMの最近のビルドによって生成された結果のx86-64アセンブリは次のとおりです。

% ./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)1つの命令に最適化していることを確認できますがaddl %eax, %eax、それをタイミングの外に移動したり、定数入力にもかかわらずそれを完全に削除したりすることはありません。

これがお役に立てば幸いです。C++標準委員会は、DoNotOptimizeここと同様にAPIを標準化する可能性を検討しています。


1
お返事ありがとうございます。新しいベストアンサーとしてマークしました。私はこれを以前に行うこともできましたが、このスタックオーバーフローページを何ヶ月も読んでいません。Clangコンパイラを使用してC ++プログラムを作成することに非常に興味があります。特に、Clangの変数名にUnicode文字を使用できるのが気に入っています。StackoverflowでClangについてさらに質問したいと思います。
S2108887 16

5
これによってfooが完全に最適化されないようにする方法を理解していますが、これがClock::now()foo()に関連して呼び出しの並べ替えを妨げる理由を少し詳しく説明できますか?オプティマイザはそれを前提DoNotOptimizeとするClock::now()必要があり、いくつかの一般的なグローバル状態にアクセスし、それらを変更する可能性があるので、次にそれらを入出力に結び付けますか?または、オプティマイザの実装の現在の制限に依存していますか?
MikeMB 2017

2
DoNotOptimizeこの例では、総合的に「観測可能な」イベントです。それはあたかもそれが入力の表現であるターミナルに目に見える出力を概念的に印刷したかのようです。時計の読み取りも観察できるため(時間の経過を観察しているため)、プログラムの観察可能な動作を変更しない限り、時計を並べ替えることはできません。
Chandler Carruth 2017年

1
「監視可能」の概念はまだはっきりしていません。foo関数がソケットからの読み取りなどの操作をしばらくブロックしている場合、これは監視可能な操作をカウントしますか?そして、これreadは「完全に既知の」操作ではないので(正しいですか?)、コードは正しい順序で保持されますか?
ravenisadesk 2018

「根本的な問題は、整数加算のようなものの操作上のセマンティクスが実装に完全に知られていることです。」しかし、問題は整数加算のセマンティクスではなく、関数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スコープ関数とデータを含むさまざまな実験がこれを確認しているようです。これには、静的なfoo()がファイルスコープ変数Nに依存することが含まれます-Nが静的として宣言されている場合、並べ替えが発生しますが、静的でないと宣言されている場合(つまり、他のコンパイルユニットから見えるため、潜在的に副作用があります) now()などの外部関数の並べ替えは行われません。
ジェレミー

3
@LĩuVĩnhPhúc:呼び出し自体が省略されないことを除いて。しかし、それは-もう一度、私は、コンパイラはその副作用が何であるかわからないので、これは疑いない)、それらの副作用がfoo(の動作に影響を与えることができないことを知っています。
ジェレミー

3
そして最後の注記:-flto(リンク時最適化)を指定すると、それ以外の場合は並べ替えられない場合でも並べ替えが発生します。
ジェレミー

20

並べ替えは、コンパイラまたはプロセッサによって行われます。

ほとんどのコンパイラは、読み取り/書き込み命令の並べ替えを防ぐためのプラットフォーム固有の方法を提供します。gccでは、これは

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

詳細はこちら

これは、読み取り/書き込みに依存している限り、間接的にのみ並べ替え操作を防止することに注意してください。

実際には、システムコールインClock::now()がこのようなバリアと同じ効果を持つシステムはまだ見ていません。結果として得られるアセンブリを検査して確認できます。

ただし、テスト中の関数がコンパイル時に評価されることは珍しくありません。「現実的な」実行を強制するにはfoo()、I / Oまたはvolatile読み取りからの入力を導出する必要がある場合があります。


別のオプションはfoo()、インライン化を無効にすることです。これもコンパイラ固有であり、通常は移植できませんが、同じ効果があります。

gccでは、これは __attribute__ ((noinline))


@Ruslanは根本的な問題を提起します:この測定はどの程度現実的ですか?

実行時間は多くの要因の影響を受けます。1つは実行中の実際のハードウェア、もう1つはキャッシュ、メモリ、ディスク、CPUコアなどの共有リソースへの同時アクセスです。

したがって、同等のタイミングを取得するために通常行うことは、エラーマージンが低くても再現可能であることを確認することです。これはそれらを幾分人工的にします。

「ホットキャッシュ」と「コールドキャッシュ」の実行パフォーマンスは、桁違いに簡単に異なる可能性がありますが、実際には、中間的なものになります(「ぬるい」ですか?)


2
ハックasmは、タイマー呼び出し間のステートメントの実行時間に影響します。メモリクローバー後のコードは、メモリからすべての変数を再ロードする必要があります。
ルスラン

@Ruslan:私のものではなく、彼らのハック。パージにはさまざまなレベルがあり、再現性のある結果を得るためには、そのようなことを行うことは避けられません。
peterchen 2016年

2
「asm」を使用したハックは、メモリを操作する操作の障壁としてのみ役立ち、OPはそれ以上に関心があることに注意してください。詳細については、私の回答を参照してください。
Chandler Carruth 2016年

11

C ++言語は、いくつかの方法で何が観察可能かを定義します。

foo()何も観察できない場合は、完全に排除できます。場合はfoo()、「ローカル」状態に格納値が(スタック上またはオブジェクトのどこかでそれをすること)という計算を行い、のみコンパイラはそのノー安全由来のポインタが入ることができますことを証明することができClock::now()、コード、そして観察可能な結果にはありませんClock::now()通話を移動します。

場合はfoo()、ファイルやディスプレイ、およびコンパイラと相互作用することが証明できないClock::now()ではない、ファイルやディスプレイとの相互作用が観察できる動作であるため、実行することはできません並べ替え、ファイルやディスプレイとの対話。

コンパイラー固有のハックを使用して、コードが動き回らないようにすることができますが(インラインアセンブリーのように)、別のアプローチは、コンパイラーの裏をかこうとすることです。

動的に読み込まれるライブラリを作成します。問題のコードの前にロードします。

そのライブラリは1つのことを公開します。

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 );
}

これは、nullary lambdaをまとめ、動的ライブラリを使用して、コンパイラが理解できないコンテキストで実行します。

ダイナミックライブラリ内では、次のことを行います。

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

とても簡単です。

ここで、への呼び出しを並べ替えるにはexecute、動的ライブラリを理解する必要があります。これは、テストコードのコンパイル中には理解できません。

それでもfoo()副作用はゼロでsを排除できますが、一部は勝ち、一部は失われます。


19
「別のアプローチは、コンパイラーを裏切ることを試みることです」そのフレーズがうさぎの穴を掘り下げた兆候ではない場合、私は何であるかわかりません。:-)
コーディグレイ

1
コードのブロックの実行に必要な時間は、コンパイラーが維持する必要のある「観察可能な」動作とは見なされないことに注意すると役立つと思います。コードのブロックを実行する時間が「観察可能」である場合、パフォーマンスの最適化の形式は許可されません。CとC ++が「因果関係バリア」を定義すると便利ですが、バリアが生成されたコードによって処理される前に、バリアの前からのすべての副作用が発生するまで、コンパイラがバリアの後にコードの実行を保留する必要があります[コードデータが完全に含まれるようにしたい...
スーパーキャット2013年

1
...ハードウェアキャッシュを介して伝播するには、ハードウェア固有の方法を使用する必要がありますが、投稿されたすべての書き込みが完了するまで待機するハードウェア固有の方法は、コンパイラによって追跡されるすべての保留中の書き込みを確実にするバリアディレクティブなしでは役に立たないでしょう。投稿されたすべての書き込みが完了したことをハードウェアに要求する前に、ハードウェアに投稿する必要がありvolatileます。
スーパーキャット2016年

4

いいえ、できません。C ++標準[intro.execution]によると:

14全式に関連付けられているすべての値の計算と副作用は、評価される次の全式に関連付けられているすべての値の計算と副作用の前にシーケンスされます。

全式は、基本的にセミコロンで終了するステートメントです。上記のルールからわかるように、ステートメントは順番に実行する必要があります。これは、ある範囲内コンパイラはより多くの行動の自由を許可されていることを文(すなわち、それ以外の注文で文を作る式を評価することができ、いくつかの状況下にある左から右または何か他の特定)。

as-ifルールを適用するための条件がここでは満たされていないことに注意してください。システム時刻を取得するために呼び出しを並べ替えても、監視可能なプログラムの動作に影響がないことをコンパイラが証明できると考えるのは不合理です。観測された動作を変更せずに時間を取得するための2つの呼び出しを並べ替えることができる状況があった場合、これを確実に推測できるように十分に理解してプログラムを分析するコンパイラを実際に作成することは非常に非効率的です。


12
ただし、as-ifルールはまだあります
MM

18
as-ifルールによりコンパイラは、観察可能な動作を変更しない限り、コードに対して何でも実行できます。実行時間は監視できません。そのため、結果が同じである限り、任意のコード行を並べ替えることができます(ほとんどのコンパイラは賢明な処理を行い、時間呼び出しを並べ替えませんが、必須ではありません)
Revolver_Ocelot

6
実行時間は監視できません。これはかなり奇妙です。実用的で非技術的な観点から、実行時間(別名「パフォーマンス」)は非常に観察可能です。
フレデリックハミディ2016年

3
時間の測定方法によって異なります。標準C ++でコードの本体を実行するために必要なクロックサイクル数を測定することはできません。
Peter

3
@dbaいくつかを混ぜ合わせています。リンカはWin16アプリケーションを生成できなくなりましたが、それは十分に真実ですが、それはそれらがそのタイプのバイナリを生成するためのサポートを削除したためです。WIn16アプリはPE形式を使用しません。これは、コンパイラーまたはリンカーのいずれかがAPI関数について特別な知識を持っていることを意味するものではありません。もう1つの問題は、ランタイムライブラリに関連しています。MSVCの最新バージョンを取得して、NT 4で実行されるバイナリを生成しても問題はありません。私はそれを実行しました。問題は、使用できない関数を呼び出すCRTにリンクしようとするとすぐに発生します。
コーディグレイ

2

番号。

場合によっては、「as-if」ルールによって、ステートメントが並べ替えられることがあります。これは、それらが互いに論理的に独立しているからではなく、その独立性によって、プログラムのセマンティクスを変更することなく、そのような再配列を実行できるためです。

現在の時刻を取得するシステムコールを移動しても、明らかにその条件は満たされません。故意または無意識にそうするコンパイラは、非準拠であり、本当にばかげています。

一般的に、積極的に最適化するコンパイラでさえ、システムコールが「2番目に推測される」結果となる式は期待していません。それはそのシステムコールが何をするかについて十分に知らないだけです。


5
私はそれが愚かになることに同意しますが、私はそれを呼び出すことはありませウォルド非準拠。コンパイラーは、具体的なシステムのシステムコールが正確に何を行うか、および副作用があるかどうかを知ることができます。コンパイラーが一般的なユースケースをカバーするためだけにそのような呼び出しを並べ替えないことを期待します、標準がそれを禁止するのではなく、より良いユーザー体験を可能にします。
Revolver_Ocelot 2016年

4
@Revolver_Ocelot:プログラムのセマンティクスを変更する最適化(大丈夫、コピー省略のために保存)は、同意するかどうかに関係なく、標準に準拠していません。
Orbitのライトネスレース2016年

6
自明なケースでは、コードがの状態と対話するため定義された方法int x = 0; clock(); x = y*2; clock();はありません。C ++標準では、何を行うかを知る必要はありません。スタックを調べて(計算がいつ発生するかを知ることができます)が、C ++の問題ではありませんclock()xclock()
Yakk-Adam Nevraumont 2016年

5
Yakkのポイントをさらに取り上げると、システムコールの順序を変更して、最初の結果がに割り当てられt2、2番目がに割り当てられるようにすることは、t1これらの値が使用された場合に不適合であり、ばかげていることになります。適合コンパイラは、システムコール全体で他のコードを並べ替えることがあります。この場合、それが何をするかfoo()(たとえば、それをインライン化したため)を知っていれば、(大まかに言えば)純粋な関数であり、移動することができます。
Steve Jessop 2016年

1
..大まかに言えば、これは、実際の実装(抽象マシンではない)がy*y、楽しみのためにシステムコールの前に投機的に計算しないという保証がないためです。また、実際の実装では、この投機的計算の結果xが使用される時点で使用されないという保証もないため、への呼び出しの間に何も実行されませんclock()foo副作用がなく、によって変更される可能性のある状態に依存できない場合、インライン関数が何をする場合でも同じことが言えますclock()
Steve Jessop 2016年

0

noinline 関数+インラインアセンブリブラックボックス+完全なデータ依存関係

これはhttps://stackoverflow.com/a/38025837/895245に基づいていますが、なぜ::now()がそこで並べ替えられないのかについて明確な正当化が見られなかったので、私はむしろ偏執的であり、それをnoinline関数の中に入れますasm。

この方法ではnoinline、the ::nowとdataの依存関係が「結び付いている」ため、並べ替えが発生しないと確信しています。

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

このメソッドの唯一のマイナーな欠点は、メソッドに1つの追加の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>

クロックの精度自体が制限されているため、1つの追加のタイミング効果に気付く可能性は低いと思いますjmpq。は共有ライブラリにあるcallため、いずれかが必要であることに注意してください::now()

::now()データに依存するインラインアセンブリから呼び出す

これは可能な限り最も効率的なソリューションであり、jmpq上記の余分なものも克服します。

残念ながら、これを正しく実行することは非常に困難です。拡張インラインASMでのprintfの呼び出し

ただし、呼び出しなしでインラインアセンブリで直接時間測定を実行できる場合は、この手法を使用できます。これは、たとえば、gem5マジックインストルメンテーション命令、x86 RDTSC(これが代表的なものかどうか不明)、および場合によっては他のパフォーマンスカウンターの場合です。

関連スレッド:

GCC 8.3.0、Ubuntu 19.04でテスト済み。


1
通常"+m"、を使用してスピル/リロードを強制する必要はありません。これを使用"+r"すると、コンパイラーに値を具体化させ、変数が変更されたと見なすことができます。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.