パフォーマンスモニタリングにtscを使用しようとしているので、命令の並べ替えを防ぎたいとします。
これらは私たちのオプションです:
1:rdtscp
はシリアル化呼び出しです。 rdtscpの呼び出しの前後の並べ替えを防ぎます。
__asm__ __volatile__("rdtscp; " // serializing read of tsc
"shl $32,%%rdx; " // shift higher 32 bits stored in rdx up
"or %%rdx,%%rax" // and or onto rax
: "=a"(tsc) // output to tsc variable
:
: "%rcx", "%rdx"); // rcx and rdx are clobbered
ただし、rdtscp
は新しいCPUでのみ使用可能です。そのため、この場合はrdtsc
を使用する必要があります。ただし、rdtsc
はシリアル化されないため、単独で使用してもCPUが並べ替えを妨げることはありません。
したがって、次の2つのオプションのいずれかを使用して、並べ替えを防ぐことができます。
2:これはcpuid
を呼び出し、次にrdtsc
を呼び出します。 cpuid
はシリアル化呼び出しです。
volatile int dont_remove __attribute__((unused)); // volatile to stop optimizing
unsigned tmp;
__cpuid(0, tmp, tmp, tmp, tmp); // cpuid is a serialising call
dont_remove = tmp; // prevent optimizing out cpuid
__asm__ __volatile__("rdtsc; " // read of tsc
"shl $32,%%rdx; " // shift higher 32 bits stored in rdx up
"or %%rdx,%%rax" // and or onto rax
: "=a"(tsc) // output to tsc
:
: "%rcx", "%rdx"); // rcx and rdx are clobbered
:これはrdtsc
をclobberリストにmemory
で呼び出し、並べ替えを防ぎます
__asm__ __volatile__("rdtsc; " // read of tsc
"shl $32,%%rdx; " // shift higher 32 bits stored in rdx up
"or %%rdx,%%rax" // and or onto rax
: "=a"(tsc) // output to tsc
:
: "%rcx", "%rdx", "memory"); // rcx and rdx are clobbered
// memory to prevent reordering
3番目のオプションに対する私の理解は次のとおりです。
__volatile__
を呼び出すと、オプティマイザーはasmを削除したり、asmの結果を必要とする(または入力を変更する)命令間でそれを移動したりできなくなります。ただし、関連のない操作に関しては、依然として移動する可能性があります。したがって、__volatile__
では十分ではありません。
: "memory")
というコンパイラメモリが破壊されていることを伝えます。 "memory"
clobberは、GCCがasm全体でメモリの内容が同じままであるという仮定を立てることができないことを意味します。したがって、GCCはその周りで並べ替えを行いません。
だから私の質問は:
__volatile__
および"memory"
の理解は正しいですか?"memory"
の使用は、別のシリアル化命令を使用するよりもはるかに簡単に見えます。なぜ誰かが2番目のオプションよりも3番目のオプションを使用するのでしょうか?コメントで述べたように、コンパイラバリアとプロセッサバリアには違いがあります。 asmステートメントのvolatile
およびmemory
はコンパイラーバリアとして機能しますが、プロセッサーは命令を自由に並べ替えることができます。
プロセッサバリアは、明示的に指定する必要がある特別な命令です。 rdtscp, cpuid
、メモリフェンス命令(mfence, lfence,
...)など.
余談ですが、cpuid
が一般的になる前に、rdtsc
をバリアとして使用する一方で、パフォーマンスの観点からも非常に悪い場合があります。仮想マシンプラットフォームは、複数のマシンに共通のCPU機能のセットを課すためにcpuid
命令をトラップしてエミュレートすることが多いためですクラスタ内(ライブマイグレーションが機能することを確認するため)。したがって、メモリフェンス命令の1つを使用することをお勧めします。
Linuxカーネルは、AMDプラットフォームではmfence;rdtsc
、Intelではlfence;rdtsc
を使用します。これらを区別する必要がない場合は、mfence;rdtsc
は両方で機能しますが、mfence
はlfence
よりも強力なバリアなので、少し遅くなります。
次のように使用できます。
asm volatile (
"CPUID\n\t"/*serialize*/
"RDTSC\n\t"/*read the clock*/
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r"
(cycles_low):: "%rax", "%rbx", "%rcx", "%rdx");
/*
Call the function to benchmark
*/
asm volatile (
"RDTSCP\n\t"/*read the clock*/
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
"CPUID\n\t": "=r" (cycles_high1), "=r"
(cycles_low1):: "%rax", "%rbx", "%rcx", "%rdx");
上記のコードでは、最初のCPUID呼び出しがバリアを実装して、RDTSC命令の上下にある命令の順不同の実行を回避しています。この方法では、リアルタイムレジスタの読み取りの間にCPUID命令を呼び出すことを避けます。
次に、最初のRDTSCがタイムスタンプレジスタを読み取り、値がメモリに保存されます。次に、測定するコードが実行されます。 RDTSCP命令は、タイムスタンプレジスタを2回目に読み取り、測定したいすべてのコードの実行が完了することを保証します。その後の2つの「mov」命令は、edxおよびeaxレジスタ値をメモリに保存します。最後に、CPUID呼び出しにより、バリアが再度実装されることが保証されるため、CPUID自体の前にその後に来る命令が実行されることは不可能です。