x86-64ではインラインasmが壊れています。 "=A"
64ビットモードでは、コンパイラはEDX:EAXではなくRAXまたはRDXのいずれかを選択できます。詳細については、このQ&Aを参照してください
このためにインラインasmは必要ありません。メリットはありません。コンパイラにはとの組み込みがrdtsc
ありrdtscp
、(少なくとも最近では)__rdtsc
適切なヘッダーを含めると、すべてが組み込み関数を定義します。ただし、他のほとんどすべての場合(https://gcc.gnu.org/wiki/DontUseInlineAsm )とは異なり、@ Mysticialのような適切で安全な実装を使用している限り、 asmに重大な欠点はありません。
(asmの小さな利点の1つは、確かに2 ^ 32カウント未満になる小さな間隔の時間を計りたい場合、結果の上位半分を無視できることです。コンパイラーは、組み込みで最適化を行うことができuint32_t time_low = __rdtsc()
ますが、練習することで、シフト/ ORを実行する指示を無駄にすることがあります。)
残念ながら、MSVCは、SIMD以外の組み込み関数に使用するヘッダーについて他のすべての人と意見が一致していません。
Intelの本質的なガイドによると_rdtsc
(アンダースコアが1つあります)はにあります<immintrin.h>
が、gccとclangでは機能しません。それらはでSIMD組み込み関数を定義するだけなので、 (MSVC)と(最近のICCを含む他のすべて)<immintrin.h>
に固執しています。MSVCおよびIntelのドキュメントと互換性があるため、gccおよびclangは、関数の1アンダースコアバージョンと2アンダースコアバージョンの両方を定義します。<intrin.h>
<x86intrin.h>
おもしろい事実:二重アンダースコアバージョンは符号なし64ビット整数を返しますが、Intel_rdtsc()
は(符号付き)を返すものとして文書化し__int64
ます。
// valid C99 and C++
#include <stdint.h> // <cstdint> is preferred in C++, but stdint.h works.
#ifdef _MSC_VER
# include <intrin.h>
#else
# include <x86intrin.h>
#endif
// optional wrapper if you don't want to just use __rdtsc() everywhere
inline
uint64_t readTSC() {
// _mm_lfence(); // optionally wait for earlier insns to retire before reading the clock
uint64_t tsc = __rdtsc();
// _mm_lfence(); // optionally block later instructions until rdtsc retires
return tsc;
}
// requires a Nehalem or newer CPU. Not Core2 or earlier. IDK when AMD added it.
inline
uint64_t readTSCp() {
unsigned dummy;
return __rdtscp(&dummy); // waits for earlier insns to retire, but allows later to start
}
32ビットまたは64ビットの場合、4つの主要なコンパイラ(gcc / clang / ICC / MSVC)すべてでコンパイルします。いくつかのテスト呼び出し元を含む、Godboltコンパイラエクスプローラーの結果を 参照してください。
これらの組み込み関数は、gcc4.5(2010年から)およびclang3.5(2014年から)で新しく追加されました。Godboltのgcc4.4とclang3.4はこれをコンパイルしませんが、gcc4.5.3(2011年4月)はコンパイルします。古いコードでインラインasmが表示される場合がありますが、これを。に置き換えることができます__rdtsc()
。10年以上前のコンパイラは通常、gcc6、gcc7、またはgcc8よりも遅いコードを作成し、有用性の低いエラーメッセージを表示します。
MSVCはx86-64のインラインasmをサポートしていなかったため、MSVC組み込み関数は(私が思うに)はるかに長く存在していました。ICC13には__rdtsc
がimmintrin.h
ありますが、まったくありませんx86intrin.h
。最近のICCにはx86intrin.h
、少なくともGodboltがLinux用にインストールする方法があります。
long long
特にそれらを減算してfloatに変換する場合は、 それらを符号付きとして定義することをお勧めします。-> float / doubleは、AVX512のないx86int64_t
よりも効率的です。uint64_t
また、TSCが完全に同期されていない場合、CPUの移行が原因で小さなマイナスの結果が生じる可能性があります。これは、巨大な符号なしの数値よりもおそらく理にかなっています。
ところで、clangには__builtin_readcyclecounter()
あらゆるアーキテクチャで動作するポータブルもあります。(サイクルカウンターのないアーキテクチャでは常にゼロを返します。)clang/LLVM言語拡張ドキュメントを参照してください。
アウトオブオーダー実行をブロックすることにより、 (または)を使用して再現性を改善し、時間間隔内にある/ない命令を正確に制御する方法lfence
cpuid
rdtsc
の詳細については、clflushの@HadiBraisの回答を参照して、 C関数とそれがもたらす違いの例についてのコメント。
LFENCEはAMDプロセッサでシリアル化されていますか?も参照してください。(TL:DRはい、Spectre軽減が有効になっています。そうでない場合、カーネルは関連するMSRを未設定のままにするためcpuid
、シリアル化に使用する必要があります。)これは常にIntelで部分シリアル化として定義されています。
インテル®IA-32およびIA-64命令セット・アーキテクチャーでコード実行時間をベンチマークする方法、2010年のインテルのホワイトペーパー。
rdtsc
CPUコアクロックサイクルではなく、参照サイクルをカウントします
ターボ/省電力に関係なく固定周波数でカウントされるため、uops-per-clock分析が必要な場合は、パフォーマンスカウンターを使用してください。 rdtsc
は実時間と正確に相関しています(システムクロックの調整はカウントされないため、の完璧な時間ソースですsteady_clock
)。
TSC周波数は、常にCPUの定格周波数、つまりアドバタイズされたステッカー周波数と同じでした。一部のCPUでは、単に近いだけです。たとえば、i7-6700HQ 2.6 GHzSkylakeでは2592MHz、4000MHzi7-6700kでは4008MHzです。i5-1035 Ice Lakeなどのさらに新しいCPUでは、TSC = 1.5 GHz、ベース= 1.1 GHzであるため、ターボを無効にしても、これらのCPUのTSC=コアサイクルではほぼ機能しません。
マイクロベンチマークに使用する場合は、最初にウォームアップ期間を含めて、タイミングを開始する前にCPUがすでに最大クロック速度になっていることを確認してください。(オプションでターボを無効にし、マイクロベンチマーク中のCPU周波数シフトを回避するために最大クロック速度を優先するようにOSに指示します)。
マイクロベンチマークは難しい:パフォーマンス評価の慣用的な方法を参照してください。他の落とし穴のために。
TSCの代わりに、ハードウェアパフォーマンスカウンターにアクセスできるライブラリを使用できます。複雑ですがオーバーヘッドの少ない方法は、perfカウンターをプログラムrdmsr
してユーザースペースで使用することです。または、時間指定領域が十分に長いためにをアタッチできる場合は、プログラムの一部にperfstatperf stat -p PID
などのトリックを含めることもできます。
ただし、メモリバウンドなどの場合に、さまざまな負荷によってSkylakeがどのようにクロックダウンするかを確認したい場合を除いて、通常はマイクロベンチマーク用にCPUクロックを固定したままにしておきます。(メモリ帯域幅/遅延は、コアとは異なるクロックを使用してほとんど固定されていることに注意してください。アイドルクロック速度では、L2またはL3キャッシュミスにかかるコアクロックサイクルははるかに少なくなります。)
チューニングの目的でRDTSCを使用してマイクロベンチマークを行う場合、最善の策は、ティックを使用し、ナノ秒に変換しようとしてもスキップすることです。std::chrono
それ以外の場合は、または のような高解像度のライブラリ時間関数を使用しclock_gettime
ます。タイムスタンプ関数の説明/比較については、gettimeofdayと同等の高速化を参照してくださいrdtsc
。または、タイマー割り込みまたはスレッドが更新するのに十分な精度要件が低い場合は、メモリから共有タイムスタンプを読み取って完全に回避してください。
結晶周波数と乗数の検出については、rdtscを使用したシステム時間の計算も参照してください。
特にマルチコア-マルチプロセッサ環境でのCPUTSCフェッチ操作では、Nehalem以降では、パッケージ内のすべてのコアに対してTSCが同期され、ロックされています(不変=定数およびノンストップTSC機能とともに)。マルチソケット同期に関するいくつかの良い情報については、@amdnの回答を参照してください。
(そして、その機能を備えている限り、最新のマルチソケットシステムでも通常は信頼できるようです。リンクされた質問に関する@amdnの回答、および以下の詳細を参照してください。)
TSCに関連するCPUID機能
Linux/proc/cpuinfo
がCPU機能に使用する名前、および同じ機能の他のエイリアスを使用します。
tsc
-TSCが存在し、rdtsc
サポートされています。x86-64のベースライン。
rdtscp
-rdtscp
サポートされています。
tsc_deadline_timer
CPUID.01H:ECX.TSC_Deadline[bit 24] = 1
-ローカルAPICは、TSCが入力した値に達したときに割り込みを発生させるようにプログラムできますIA32_TSC_DEADLINE
。「ティックレス」カーネルを有効にし、次に起こるはずのことまでスリープしていると思います。
constant_tsc
:一定のTSC機能のサポートは、CPUファミリとモデル番号を確認することによって決定されます。TSCは、コアクロック速度の変化に関係なく、一定の周波数でティックします。これがないと、RDTSCはコアクロックサイクルをカウントします。
nonstop_tsc
:この機能は、Intel SDMマニュアルでは不変TSCと呼ばれ、が付いたプロセッサでサポートされていますCPUID.80000007H:EDX[8]
。TSCは、深い睡眠のC状態でもカチカチ音をたて続けます。すべてのx86プロセッサで、はをnonstop_tsc
意味しますconstant_tsc
が、constant_tsc
必ずしもを意味するわけではありませんnonstop_tsc
。個別のCPUID機能ビットはありません。IntelとAMDでは、同じ不変のTSCCPUIDビットが両方constant_tsc
とnonstop_tsc
機能を意味します。Linuxのx86/kernel / cpu / intel.c検出コードを参照してくださいamd.c
。これは、類似したものです。
Saltwell / Silvermont / Airmontに基づくプロセッサの一部(すべてではありません)は、ACPI S3フルシステムスリープでTSCを刻み続けます:nonstop_tsc_s3
。これは常時接続TSCと呼ばれます。(エアモントをベースにしたものはリリースされていないようですが。)
一定および不変のTSCの詳細については、次を参照してください。一定の非不変のtscは、CPU状態全体で周波数を変更できますか?。
tsc_adjust
: MSRが利用可能であり、OSCPUID.(EAX=07H, ECX=0H):EBX.TSC_ADJUST (bit 1)
は、TSCを読み取るときにまたは読み取るIA32_TSC_ADJUST
ときにTSCに追加されるオフセットを設定できます。これにより、論理コア間でTSCを非同期化することなく、一部またはすべてのコアでTSCを効果的に変更できます。(これは、ソフトウェアが各コアでTSCを新しい絶対値に設定した場合に発生します。関連するWRMSR命令をすべてのコアで同じサイクルで実行することは非常に困難です。)rdtsc
rdtscp
constant_tsc
またnonstop_tsc
、TSCclock_gettime
をユーザースペースなどのタイムソースとして使用できるようにします。(しかし、LinuxのようなOSはRDTSCのみを使用して、NTPで維持される遅いクロックのティック間を補間し、タイマー割り込みのスケール/オフセット係数を更新します。constant_tscおよびnonstop_tscを使用するCPUで、なぜ私の時間がドリフトするのですか?)ディープスリープ状態または周波数スケーリングをサポートしていない場合でも、タイムソースとしてのTSCは引き続き使用可能です。
Linuxソースコードのコメントは、 constant_tsc
/ nonstop_tsc
features(Intel上)が「コアとソケット間でも信頼できる(ただし、キャビネット間では信頼できない-その場合は明示的にオフにする)」ことを意味することも示しています。
「ソケット間」の部分は正確ではありません。一般に、不変のTSCは、TSCが同じソケット内のコア間で同期されることを保証するだけです。Intelフォーラムのスレッドで、Martin Dixon(Intel)は、 TSCの不変性はソケット間の同期を意味しないと指摘しています。そのためには、プラットフォームベンダーがRESETをすべてのソケットに同期的に配布する必要があります。上記のLinuxカーネルのコメントを考えると、 プラットフォームベンダーは実際にそうしているようです。特にマルチコア-マルチプロセッサ環境でのCPUTSCフェッチ操作に関する回答は、単一のマザーボード上のすべてのソケットが同期して開始する必要があることにも同意しています。
マルチソケット共有メモリシステムでは、すべてのコアのTSCが同期されているかどうかを直接確認する方法はありません。Linuxカーネルは、デフォルトで起動時と実行時のチェックを実行して、TSCをクロックソースとして使用できることを確認します。これらのチェックには、TSCが同期されているかどうかの判別が含まれます。コマンドの出力はdmesg | grep 'clocksource'
、カーネルがクロックソースとしてTSCを使用しているかどうかを示します。これは、チェックに合格した場合にのみ発生します。しかし、それでも、これはTSCがシステムのすべてのソケット間で同期されていることの決定的な証拠にはなりません。カーネルパラメータtsc=reliable
を使用して、チェックを行わずにTSCをクロックソースとして盲目的に使用できることをカーネルに通知できます。
クロスソケットTSCが同期していない場合があります:(1)CPUのホットプラグ、(2)拡張ノードコントローラーによって接続された異なるボードにソケットが分散している場合、(3)ウェイクアップ後にTSCが再同期されない場合一部のプロセッサでTSCの電源がオフになっているC状態からアップし、(4)異なるソケットには異なるCPUモデルがインストールされています。
TSC_ADJUSTオフセットを使用する代わりにTSCを直接変更するOSまたはハイパーバイザーは、それらの同期を解除できるため、ユーザースペースでは、CPUの移行によって別のクロックを読み取ることができないと想定するのは必ずしも安全ではありません。(これが、追加の出力としてコアIDを生成する理由rdtscp
です。これにより、開始/終了時刻が異なるクロックから来る場合を検出できます。これは、不変のTSC機能の前に導入された可能性があります。または、すべての可能性を考慮したかっただけかもしれません。 )。
直接使用している場合はrdtsc
、プログラムまたはスレッドをコアに固定することをお勧めします(taskset -c 0 ./myprogram
Linuxなど)。TSCに必要かどうかにかかわらず、CPUの移行は通常、多くのキャッシュミスを引き起こし、とにかくテストを台無しにし、余分な時間を要します。(ただし、割り込みも発生します)。
asmは組み込みを使用することでどれほど効率的ですか?
@MysticialのGNUCインラインasmから得られるものとほぼ同じか、RAXの上位ビットがゼロになっていることがわかっているのでそれ以上です。インラインasmを維持したい主な理由は、無愛想な古いコンパイラとの互換性のためです。
readTSC
関数自体の非インラインバージョンは、次のようにx86-64用のMSVCでコンパイルされます。
unsigned __int64 readTSC(void) PROC ; readTSC
rdtsc
shl rdx, 32 ; 00000020H
or rax, rdx
ret 0
; return in RAX
で64ビット整数を返す32ビット呼び出し規約の場合は、 /edx:eax
だけです。それは重要ではありません、あなたは常にこれをインラインにしたいです。rdtsc
ret
それを2回使用し、間隔を時間に差し引くテスト呼び出し元では、次のようになります。
uint64_t time_something() {
uint64_t start = readTSC();
// even when empty, back-to-back __rdtsc() don't optimize away
return readTSC() - start;
}
4つのコンパイラはすべて、非常によく似たコードを作成します。これはGCCの32ビット出力です。
# gcc8.2 -O3 -m32
time_something():
push ebx # save a call-preserved reg: 32-bit only has 3 scratch regs
rdtsc
mov ecx, eax
mov ebx, edx # start in ebx:ecx
# timed region (empty)
rdtsc
sub eax, ecx
sbb edx, ebx # edx:eax -= ebx:ecx
pop ebx
ret # return value in edx:eax
これは、MSVCのx86-64出力です(名前のデマングルが適用されています)。gcc / clang/ICCはすべて同じコードを出力します。
# MSVC 19 2017 -Ox
unsigned __int64 time_something(void) PROC ; time_something
rdtsc
shl rdx, 32 ; high <<= 32
or rax, rdx
mov rcx, rax ; missed optimization: lea rcx, [rdx+rax]
; rcx = start
;; timed region (empty)
rdtsc
shl rdx, 32
or rax, rdx ; rax = end
sub rax, rcx ; end -= start
ret 0
unsigned __int64 time_something(void) ENDP ; time_something
4つのコンパイラはすべて、下半分と上半分を別のレジスタに結合する代わりにor
+を使用します。彼らが最適化に失敗するのは、一種の缶詰のシーケンスだと思います。mov
lea
しかし、インラインasmでシフト/リーを自分で書くことは、ほとんど良いことではありません。32ビットの結果しか保持しないような短い間隔でタイミングをとっている場合は、EDXの結果の上位32ビットを無視する機会をコンパイラーから奪うことになります。または、コンパイラが開始時刻をメモリに格納することを決定した場合、shift/または/movの代わりに2つの32ビットストアを使用することができます。タイミングの一部として1つの余分なuopが気になる場合は、マイクロベンチマーク全体を純粋なasmで作成することをお勧めします。
ただし、@ Mysticialのコードの修正バージョンを使用すると、両方の長所を活用できる可能性があります。
// More efficient than __rdtsc() in some case, but maybe worse in others
uint64_t rdtsc(){
// long and uintptr_t are 32-bit on the x32 ABI (32-bit pointers in 64-bit mode), so #ifdef would be better if we care about this trick there.
unsigned long lo,hi; // let the compiler know that zero-extension to 64 bits isn't required
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) + lo;
// + allows LEA or ADD instead of OR
}
Godboltでは、これによりgcc / clang / ICCよりも優れたasmが得られる場合がありますが__rdtsc()
、コンパイラーをだましてloとhiを別々に保存するように追加のレジスターを使用させ、clangをに最適化できる場合もあります((end_hi-start_hi)<<32) + (end_lo-start_lo)
。うまくいけば、実際のレジスターのプレッシャーがあれば、コンパイラーはもっと早く結合するでしょう。(gccとICCは引き続きlo / hiを別々に保存しますが、最適化もしません。)
しかし、32ビットのgcc8はそれを台無しにし、clangのようにedx:eaxで結果を返すのではなく、rdtsc()
関数自体をゼロの実数でコンパイルします。add/adc
(gcc6以前は|
の代わりに問題ありませんが、gccからの32ビットコード生成を気にする場合+
は間違いなく組み込みを優先します)。__rdtsc()