web-dev-qa-db-ja.com

連続したrdtscを使用した負のクロックサイクル測定?

セマフォを取得するために必要なクロックサイクル数を測定するためのCコードを書いています。私はrdtscを使用しており、セマフォで測定を行う前に、オーバーヘッドを測定するためにrdtscを2回連続して呼び出します。これをforループで何度も繰り返し、平均値をrdtscオーバーヘッドとして使用します。

まず、平均値を使用するのは正しいですか?

それにもかかわらず、ここでの大きな問題は、オーバーヘッドに対して負の値を取得することがあることです(必ずしも平均値ではありませんが、少なくともforループ内の部分的な値)。

これは、sem_wait()操作に必要なCPUサイクル数の連続計算にも影響し、負になることもあります。私が書いたことが明確でない場合は、ここに私が取り組んでいるコードの一部があります。

なぜ私はそのような負の値を取得しているのですか?


(編集者注:完全な64ビットタイムスタンプを取得する正確でポータブルな方法については、 CPUサイクルカウントを取得しますか? を参照してください。"=A"asm制約は、32ビットの下位または上位のみを取得します。レジスタ割り当てがuint64_t出力に対してRAXまたはRDXのどちらを選択するかに応じて、x86-64用にコンパイルされます。edx:eaxは選択されません。)

(編集者の2番目のメモ:おっと、それが否定的な結果が得られる理由の答えです。このrdtsc実装をコピーしないように警告するために、ここにメモを残す価値があります。)


#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

static inline uint64_t get_cycles()
{
  uint64_t t;
           // editor's note: "=A" is unsafe for this in x86-64
  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

int num_measures = 10;

int main ()
{
   int i, value, res1, res2;
   uint64_t c1, c2;
   int tsccost, tot, a;

   tot=0;    

   for(i=0; i<num_measures; i++)
   {    
      c1 = get_cycles();
      c2 = get_cycles();

      tsccost=(int)(c2-c1);


      if(tsccost<0)
      {
         printf("####  ERROR!!!   ");
         printf("rdtsc took %d clock cycles\n", tsccost);
         return 1;
      }   
      tot = tot+tsccost;
   }

   tsccost=tot/num_measures;
   printf("rdtsc takes on average: %d clock cycles\n", tsccost);      

   return EXIT_SUCCESS;
}
17
Discipulus

Intelが最初にTSCを発明したとき、CPUサイクルを測定しました。さまざまな電源管理機能により、「1秒あたりのサイクル数」は一定ではありません。したがって、TSCは元々、コードのパフォーマンスの測定には適していました(そして、経過時間の測定には適していませんでした)。

良くも悪くも;当時、CPUの電源管理はそれほど多くなく、CPUはとにかく固定された「1秒あたりのサイクル数」で実行されることがよくありました。一部のプログラマーは間違った考えを思いつき、サイクルではなく時間の測定にTSCを誤用しました。その後(電源管理機能の使用がより一般的になったとき)、これらの人々はTSCを誤用して時間を測定し、誤用によって引き起こされたすべての問題について泣き言を言いました。 CPUメーカー(AMD以降)はTSCを変更して、サイクルではなく時間を測定するようにしました(コードのパフォーマンスを測定するために壊れましたが、経過した時間を測定するために修正しました)。これにより混乱が生じたため(ソフトウェアがTSCが実際に測定したものを判別するのが困難でした)、少し後にAMDがCPUIDに「TSCInvariant」フラグを追加しました。このフラグが設定されている場合、プログラマーはTSCが壊れていることを認識します(測定用)サイクル)または固定(時間を測定するため)。

IntelはAMDに従い、TSCの動作を変更して時間も測定し、AMDの「TSC不変」フラグも採用しました。

これにより、4つの異なるケースが発生します。

  • TSCは時間とパフォーマンスの両方を測定します(1秒あたりのサイクル数は一定です)

  • TSCは時間ではなくパフォーマンスを測定します

  • TSCはパフォーマンスではなく時間を測定しますが、「TSC不変」フラグを使用してそのように言うことはありません。

  • TSCはパフォーマンスではなく時間を測定し、「TSC不変」フラグを使用してそのように言います(最新のCPUのほとんど)

TSCが時間を測定する場合、パフォーマンス/サイクルを適切に測定するには、パフォーマンス監視カウンターを使用する必要があります。残念ながら、パフォーマンス監視カウンターはCPU(モデル固有)ごとに異なり、MSR(特権コード)へのアクセスが必要です。これにより、アプリケーションが「サイクル」を測定することはかなり非現実的になります。

また、TSCが時間を測定する場合、他の時間ソースを使用してスケーリング係数を決定しないと、TSCが返すタイムスケール(「ふりサイクル」でのナノ秒数)を知ることができないことにも注意してください。

2番目の問題は、マルチCPUシステムの場合、ほとんどのオペレーティングシステムが問題を抱えていることです。 OSがTSCを処理する正しい方法は、アプリケーションがTSCを直接使用しないようにすることです(CR4でTSDフラグを設定することにより、RDTSC命令によって例外が発生します)。これにより、さまざまなセキュリティの脆弱性(サイドチャネルのタイミング)が防止されます。また、OSがTSCをエミュレートし、正しい結果を返すようにします。たとえば、アプリケーションがRDTSC命令を使用して例外を発生させた場合、OSの例外ハンドラーは正しい「グローバルタイムスタンプ」を見つけて返すことができます。

もちろん、異なるCPUには独自のTSCがあります。これは、アプリケーションがTSCを直接使用する場合、異なるCPUで異なる値を取得することを意味します。 OSが問題を修正できないことを回避するために(必要に応じてRDTSCをエミュレートすることにより)。 AMDは、TSCと「プロセッサID」を返すRDTSCP命令を追加しました(IntelもRDTSCP命令を採用することになりました)。壊れたOSで実行されているアプリケーションは、「プロセッサID」を使用して、前回とは異なるCPUで実行されていることを検出できます。このようにして(RDTSCP命令を使用して)、「elapsed = TSC--previous_TSC」が有効な結果をもたらすタイミングを知ることができます。しかしながら;この命令によって返される「プロセッサID」はMSRの値にすぎず、OSは各CPUのこの値を別の値に設定する必要があります。そうしないと、RDTSCPは「プロセッサID」がゼロであると表示します。すべてのCPU。

基本的に; CPUがRDTSCP命令をサポートしている場合、およびOSが「プロセッサID」を正しく設定している場合(MSRを使用)。次に、RDTSCP命令は、アプリケーションが「経過時間」の結果が悪いことを知るのに役立ちます(ただし、悪い結果を修正または回避する方法はありません)。

そう;長い話を短くするために、正確なパフォーマンス測定が必要な場合は、ほとんどが失敗します。現実的に期待できる最善の方法は、正確な時間測定です。ただし、一部の場合に限ります(たとえば、単一CPUマシンで実行している場合、または特定のCPUに「固定」されている場合、または無効な値を検出して破棄する限り、適切にセットアップされたOSでRDTSCPを使用する場合)。 。

もちろん、それでもIRQのようなもののために危険な測定値を取得します。このために;コードをループ内で何度も実行し、他の結果よりも高すぎる結果を破棄することをお勧めします。

最後に、本当に適切に実行したい場合は、測定のオーバーヘッドを測定する必要があります。これを行うには、何もしないのにかかる時間を測定します(危険な測定値を破棄しながら、RDTSC/RDTSCP命令のみ)。次に、「何かを測定する」結果から測定のオーバーヘッドを差し引きます。これにより、「何か」が実際にかかる時間をより正確に見積もることができます。

注:Pentiumが最初にリリースされたときからIntelのシステムプログラミングガイドのコピーを掘り下げることができれば(1990年代半ば-オンラインで利用できるかどうかはわかりません-1980年代からコピーをアーカイブしています)、Intelが見つかりますタイムスタンプカウンターを「プロセッサイベントの相対的な発生時間を監視および特定するために使用できる」ものとして文書化し、(64ビットのラップアラウンドを除く)単調に増加することを保証しました(ただし、マニュアルの最新版では、タイムスタンプカウンターの詳細が記載されており、古いCPU(P6、Pentium M、古いPentium 4)の場合と記載されています。タイムスタンプカウンターは「内部プロセッサークロックサイクルごとに増加」し、「Intel(r)SpeedStep(r)テクノロジーの移行はプロセッサークロックに影響を与える可能性があります」、および新しいCPU(新しいPentium 4、Core Solo、Core Duo、Core 2 、Atom)TSCは一定の速度(およびtこれが「前進する建築行動」です。基本的に、最初からタイムスタンプに使用されるのは(可変の)「内部サイクルカウンター」であり(「実時間」時間を追跡するために使用されるタイムカウンターではありません)、この動作は直後に変更されました。 2000年(Pentium 4のリリース日に基づく

54
Brendan
  1. 平均値を使用しないでください

    大きい値はOSマルチタスクによって中断されているため、代わりに小さい値または小さい値の平均を使用してください(CACHEのために平均を取得するため)。

    また、すべての値を覚えてから、OSプロセスの粒度の境界を見つけ、この境界の後のすべての値を除外することもできます(通常は> 1ms簡単に検出できます)

    enter image description here

  2. RDTSCのオーバーヘッドを測定する必要はありません

    ある時間だけオフセットを測定すると、同じオフセットが両方の時間に存在し、減算すると消えます。

  3. RDTSの可変クロックソースの場合(ラップトップの場合など)

    [〜#〜] cpu [〜#〜]の速度を、安定した集中的な計算ループによって最大に変更する必要があります。通常は数秒で十分です。 [〜#〜] cpu [〜#〜]周波数を継続的に測定し、十分に安定している場合にのみ測定を開始する必要があります。

7
Spektre

あるプロセッサでコードを開始してから別のプロセッサにスワップすると、プロセッサがスリープ状態になるなどの理由でタイムスタンプの差が負になる場合があります。

測定を開始する前に、プロセッサ親和性を設定してみてください。

質問からWindowsとLinuxのどちらで実行しているかわからないので、両方に答えます。

ウィンドウズ:

DWORD affinityMask = 0x00000001L;
SetProcessAffinityMask(GetCurrentProcessId(), affinityMask);

Linux:

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity (getpid(), sizeof(cpuset), &cpuset)
3
Neil

他の答えは素晴らしいです(それらを読んでください)が、rdtscが正しく読まれていると仮定します。この答えは、ネガティブを含む完全に偽の結果につながるインラインasmバグに対処しています。

もう1つの可能性は、これを32ビットコードとしてコンパイルしていましたが、繰り返しが多く、不変TSC(すべてのコア間で同期されたTSC)を持たないシステムでCPU移行時に負の間隔が発生することがありました。マルチソケットシステム、または古いマルチコアのいずれか。 特にマルチコア-マルチプロセッサ環境でのCPUTSCフェッチ操作


x86-64用にコンパイルした場合、否定的な結果は、asm.の誤った_"=A"_出力制約によって完全に説明されます。 を参照してください。 CPUサイクルカウントを取得しますか? すべてのコンパイラに移植可能なrdtscを使用する正しい方法、および32ビットモードと64ビットモード。または、_"=a"_および_"=d"_出力を使用し、32ビットをオーバーフローしない短い間隔で、上位半分の出力を単に無視します。)

(私はあなたがそれらが巨大であり、大きく変化していること、そして個人がいない場合でも負の平均を与えるためにtotが溢れていることについて言及しなかったことに驚いています測定値は負でした。_-63421899_、_69374170_、または_115365476_のような平均が表示されています。)

_gcc -O3 -m32_でコンパイルすると、期待どおりに動作し、平均24〜26を出力します(ループで実行してCPUが最高速度を維持する場合、それ以外の場合は、24コアクロックサイクルの125リファレンスサイクルのようになります。 Skylakeでrdtscに戻る)。 https://agner.org/optimize/ 命令テーブル用。


_"=A"_制約の問題の詳細

rdtsc(insn ref手動入力)alwaysは、64ビット結果の2つの32ビット_hi:lo_半分を_edx:eax_、64ビットモードでも、実際には単一の64ビットレジスタにあります。

_"=A"_出力制約が_edx:eax_に対して_uint64_t t_を選択することを期待していました。しかし、それは起こりません。oneレジスタに収まる変数の場合、コンパイラはRAXまたはRDX_"=r"_制約が1つのレジスタを選択し、残りが変更されていないと仮定するのと同じように、もう1つは変更されていないと仮定します。または、_"=Q"_制約は、a、b、c、またはdのいずれかを選択します。 ( x86制約 を参照)。

X86-64では、通常、複数の結果やdiv入力のように、_"=A"_オペランドには_unsigned __int128_のみが必要です。 asmテンプレートで_%0_を使用すると低レジスタにのみ展開され、_"=A"_が両方を使用しない場合でも警告が表示されないため、これは一種のハックですaおよびdレジスタ。

これがどのように問題を引き起こすかを正確に確認するために、asmテンプレート内にコメントを追加しました。
__asm__ volatile ("rdtsc # compiler picked %0" : "=A"(t));。したがって、オペランドで指示した内容に基づいて、コンパイラーが何を期待しているかを確認できます。

結果のループ(Intel構文)は、クリーンアップされたバージョンのコードをコンパイルした場合 Godboltコンパイラエクスプローラーで 64ビットgccおよび32ビットclangの場合、次のようになります。

_# the main loop from gcc -O3  targeting x86-64, my comments added
.L6:
    rdtsc  # compiler picked rax     # c1 = rax
    rdtsc  # compiler picked rdx     # c2 = rdx, not realizing that rdtsc clobbers rax(c1)

      # compiler thinks   RAX=c1,               RDX=c2
      # actual situation: RAX=low half of c2,   RDX=high half of c2

    sub     edx, eax                 # tsccost = edx-eax
    js      .L3                      # jump if the sign-bit is set in tsccost
   ... rest of loop back to .L6
_

コンパイラが_c2-c1_を計算しているとき、それは実際に2番目のrdtscから_hi-lo_を計算しています。asmステートメントの機能についてコンパイラーに嘘をついたため。 2番目のrdtsc clobbered _c1_

出力を取得するレジスタを選択できることを伝えたので、最初に1つのレジスタを選択し、2回目に別のレジスタを選択したので、mov命令は必要ありません。

TSCは、最後の再起動以降の参照サイクルをカウントします。ただし、コードは_hi<lo_に依存せず、_hi-lo_の符号に依存するだけです。 loは1〜2秒ごとにラップアラウンドするため(2 ^ 32 Hzは4.3GHzに近い)、いつでもプログラムを実行すると、約50%の確率で否定的な結果が表示されます。

hiの現在の値には依存しません。 hiがラップアラウンドすると、loが1つずつ変化するため、_2^32_のバイアスにはおそらく1つの部分があります。

_hi-lo_はほぼ均一に分散された32ビット整数であるため、平均のオーバーフローは非常に一般的です。平均が通常小さい場合、コードは問題ありません。 (ただし、平均値が必要ない理由については、他の回答を参照してください。中央値または外れ値を除外するものが必要です。)

2
Peter Cordes

私の質問の主なポイントは、結果の正確さではなく、時々負の値を取得しているという事実でした(rdstcへの最初の呼び出しは2番目の呼び出しよりも大きな値を与えます)。さらに調査を行って(そしてこのWebサイトで他の質問を読んで)、rdtscを使用するときに物事を機能させる方法は、その直前にcpuidコマンドを配置することであることがわかりました。このコマンドはコードをシリアル化します。これが私が今している方法です:

static inline uint64_t get_cycles()
{
  uint64_t t;          

   volatile int dont_remove __attribute__((unused));
   unsigned tmp;
     __asm volatile ("cpuid" : "=a"(tmp), "=b"(tmp), "=c"(tmp), "=d"(tmp)
       : "a" (0));

   dont_remove = tmp; 




  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

Get_cycles関数の2番目の呼び出しと最初の呼び出しの間にまだ負の違いがあります。どうして? cpuidアセンブリインラインコードの構文について100%確信が持てません。これは、インターネットで見つけたものです。

1
Discipulus

サーマルスロットリングとアイドルスロットリング、マウスモーションとネットワークトラフィックの割り込み、GPUで何をしているのか、そして現代のマルチコアシステムが誰も気にせずに吸収できる他のすべてのオーバーヘッドに直面して、これに対するあなたの唯一の合理的なコースは数千の個別のサンプルを蓄積し、中央値または平均をとる前に外れ値を投げるだけです(統計学者ではありませんが、ここではあまり違いはありません)。

実行中のシステムのノイズを排除するためにあなたがすることは、それがどれくらいの時間がかかるかを確実に予測することができる方法がないことを単に受け入れるよりもはるかに悪い結果を歪めると思います何でも =これらの日を完了する。

0
jthill

rdtscを使用すると、信頼性が高く非常に正確な経過時間を取得できます。 Linuxを使用している場合は、/ proc/cpuinfoを調べて、constant_tscが定義されているかどうかを確認することで、プロセッサが一定レートのtscをサポートしているかどうかを確認できます。

同じコアにとどまるようにしてください。すべてのコアには、独自の値を持つ独自のtscがあります。 rdtscを使用するには、 taskset 、または SetThreadAffinityMask (windows)または pthread_setaffinity_np のいずれかを実行して、プロセスが同じコア上にあることを確認してください。

次に、これをLinuxでは/ proc/cpuinfoにあるメインクロックレートで除算するか、実行時に次のように実行できます。

rdtsc
clock_gettime
1秒間スリープ
clock_gettime
rdtsc

次に、1秒あたりのティック数を確認します。次に、ティックの差を分割して、経過した時間を調べることができます。

0
Michael

コードを実行しているスレッドがコア間を移動している場合、返されるrdtsc値が別のコアで読み取られた値よりも小さい可能性があります。パッケージの電源投入時に、コアのすべてがカウンターを0に設定するわけではありません。したがって、テストを実行するときは、スレッドアフィニティを特定のコアに設定してください。

0
BitTwiddler

私は自分のマシンであなたのコードをテストし、RDTSC機能の間はuint32_tだけが妥当であると考えました。

私はそれを修正するために私のコードで次のことをします:

if(before_t<after_t){ diff_t=before_t + 4294967296 -after_t;}
0
Zhu Guoliang