TurboBoostの周波数監視ツールを作成しました
新しいマシン(Sandy Bridge)を買ったのでTurboBoostを試してみました。
Win環境だとIntelが作成&公開しているインテル® ターボ・ブースト・テクノロジー・モニターというツールがあります。Linux用だと、turbostatというツールがあります。私はLinuxユーザですのでturbostatを使用します。
まず、OSの設定でTurboBoostを有効にしなければいけません。こちらの情報で/sys/devices/system/cpu/cpuN/cpufreq/scaling_governorが"ondemand"にしないといけないといことが分かっています。ただ、私の環境ではすでに有効になっていました。
こちらからturbostat.cとMakefileをダウンロードしてきます。
コンパイルしてそのまま実行すると「modprobe msrをしなさい」と言われます。なので実行する前にカーネルモジュールをロードしておきます。
#modprobe msr
実行します(root権限がいるのでrootで実行します)
#./turbostat
左が平常時、右がカーネルビルドを行っている時です。
違いますね。周波数。なんか変態的です。
ふと、思いました。
「sched_setaffinity(2)で実行するCPUを限定したらそのCPUだけクロック数が跳ね上がるのだろうか?」
実験しましょう。ビジーループをするスレッドをCPU#0だけで実行する様にしてみます。
ビジーループのプログラムを実行した状態
全体的に上がっています。不思議です。これはプログラムがおかしいのでは?と思いまして、ソースを読んでみました。そうするとturbostatは以下の方法で周波数を調べていることが分かりました。
- MSR_TSC, MSR_APERF, MSR_MPERFというモデル固有レジスタを用いてデータを取得していること
- 5秒周期で上記3レジスタへRDMSR命令を通じてデータを取得し、前回取得時との差分を用いて周波数を計算していること
- 計算式はFcurrent = MSR_TSCsub / MSR_APERFsub / MSR_MPERFsub [Hz] であること
IntelのTurbo Boostのホワイトペーパー*1にはcurrentの周波数を取得する"アルゴリズム"が載っています。(私の拙い英語力をもってして)列挙すると以下のようになります。
- PLATFORM_INFO MSRに対してRDMSRを発行してベースクロック比を取得する。これとバスクロック数の乗算でベースクロック数が計算できる
- IA32_PERF_GLOBAL_CTRLとIA32_FIXED_CTR_CTLをOSが認識したCPU数分設定する
- 1秒のタイマをOSのAPIを使って設定する。
- 5~10の工程をアプリケーションが終了するまで実行する
- 1秒のタイマが終了するまで待つ
- IA32_FIXED_CTR1とIA32_FIXED_CTR2に対してRDMSRを発行するしてデータを取得する。これをOSが認識したCPU数分設定する
- 6で取得したIA32_FIXED_CTR1とIA32_FIXED_CTR2の前回との差分(IA32_FIXED_CTR1sub, IA32_FIXED_CTR2sub)を計算する
- CPUの実際の周波数を計算する。計算式はFcurrent = ベースクロック数 * (IA32_FIXED_CTR1sub / IA32_FIXED_CTR2sub)である
- GUIアプリの周波数表示をリフレッシュする
- IA32_FIXED_CTR1とIA32_FIXED_CTR2の値をメモリに保存しておく
ということで、turbstatでの周波数計算アルゴリズムはIntelのホワイトペーパーのアルゴリズム*2に従っていないことになります。
#ちなみにturbostatを書いたのはIntelの方です。ですが、きっとホワイトペーパーのアルゴリズムに従っていないのはそれなりの理由があるのです。そしてそれは全うな理由なのです。
それは置いておいて、私としてはIntelが技術資料で公開しているアルゴリズムで実装されたツールで周波数を調べたいと思ったので、実装しました。turbofreqというプログラムです。
例によって、すべて掲載すると非常に縦長のエントリになるので、メインのアルゴリズムの部分だけ載せておきます。
for(i = 0; i < nr_cpus; i++){ if(i == 0 && f){ fprintf(f, "%d,", cnt); } pmc1 = get_msr(i, IA32_FIXED_CTR1); pmc2 = get_msr(i, IA32_FIXED_CTR2); sub_pmc1 = pmc1 - pmc1_last[i]; sub_pmc2 = pmc2 - pmc2_last[i]; current = base_op_ratio * bus_clock * ((double)sub_pmc1 / (double)sub_pmc2); /* 出力先がファイルかコンソールか */ if(f == NULL){ printf("CPU#%d freq = %1.2f GHz\n", i, current); } else{ fprintf(f, "%1.2f,", current); } if(i == nr_cpus - 1 && f){ fprintf(f, "\n"); } /* 次回のために保存 */ pmc1_last[i] = pmc1; pmc2_last[i] = pmc2; }
ソースはgithubにおいてあります。周波数を変化させるためには、CPUを限定して負荷をかける必要がります。そこで今回はビジーループを用います。
コマンドライン引数でCPU番号と、生成するスレッド数を受け取って、ビジーループを実行するスレッドを生成し、SIGTERMで終了するプログラムを作成しました。busyloopというプログラムです。これもgithubに置いておきます。
そして、さらにturbostatと比較をしたかったので、turbostatのアルゴリズムも実装してturbofreqに組み込みました。Intelのアルゴリズムで周波数を監視するか、turbostatのアルゴリズムで周波数を監視するか実行時に選択できます。
turbofreqのturbostatアルゴリズムのメイン部分です。
for(i = 0; i < nr_cpus; i++){ if(i == 0 && f){ fprintf(f, "%d,", cnt); } pmc1 = get_msr(i, MSR_APERF); pmc2 = get_msr(i, MSR_MPERF); pmc3 = get_msr(i, MSR_TSC); sub_pmc1 = pmc1 - pmc1_last[i]; sub_pmc2 = pmc2 - pmc2_last[i]; sub_pmc3 = pmc3 - pmc3_last[i]; current = (double)sub_pmc3 / TSC_ORDER * (double)sub_pmc1 / (double)sub_pmc2; /* 出力先がファイルかコンソールか */ if(f == NULL){ printf("CPU#%d freq = %1.2f GHz\n", i, current); } else{ fprintf(f, "%1.2f,", current); } if(i == nr_cpus - 1 && f){ fprintf(f, "\n"); } /* 次回のために保存 */ pmc1_last[i] = pmc1; pmc2_last[i] = pmc2; pmc3_last[i] = pmc3; }
これで条件が揃ったので、実験ができます。
実験はアーキテクチャ2種類に対して負荷のかけかた3種類の合計6種類やってみます。6種類の実験に対し、turbostatとIntelの技術文書とで2種類のアルゴリズムの比較をしたいと思いますので、合計12個のグラフを描いてみます。
実験環境は以下の通りです。
実験ハードウェア
Model | Architecture | Num cpu | Base_clock_ratio | Bus_clock | Max frequency |
Core i7 870 2.93GHz | Nehalem | 4C/8T | 22 | 133.33MHz | 3.6GHz |
Core i7 2600 3.4GHz | SandyBridge | 4C/8T | 34 | 100MHz | 3.8GHz |
※但し、TurboBoostでは物理コアごとに周波数の制御が行われるのでBIOSでHTTは無効にすることとします。
負荷のかけ方
- 1つのCPUにだけbusyloopさせる
- 10秒ずつbusyloopするCPUを変えていく
- 10秒ごとにbusyloopするCPUを増やしていく
結果
左側がturbostatのアルゴリズム、右側がIntelの技術資料に掲載されているアルゴリズムでそれぞれ周波数を監視したものです。
また、1秒毎に周波数を計算し、45秒間周波数を監視することとします。負荷がかかったことが確認できるように、負荷をかけない間隔として最初の5秒をとりました。なのでビジーループが始まるのは実験開始5秒後からです。
まずはCore i7 870の実験結果です。このCPUはNehalemです。
「1つのCPUだけbusyloopさせる」場合。
「10秒ずつbusyloopするCPUを変えていく」場合。
「10秒ごとにbusyloopするCPUを増やしていく」場合。
次はCore i7 2600の実験結果です。アーキテクチャはSandyBridgeで、TurboBoost2.0が搭載されています。
「1つのCPUだけbusyloopさせる」場合。
「10秒ずつbusyloopするCPUを変えていく」場合。
「10秒ごとにbusyloopするCPUを増やしていく」場合。
考察
まずは負荷のかけ方のアプローチから。1つのCPUにだけ負荷をかける実験では負荷をかけたCPUの周波数が高い周波数で動作しています。順番に負荷をかけるCPUを変える実験では変えた通りに順番に周波数が変化しています。さらに順番にビジーループさせるCPUを増やしていくと徐々に周波数が落ちていって、ある一定の周波数に収束します。予想通りのグラフです。
次にアーキテクチャ毎のアプローチです。SandyBridgeではTurboBoost2.0という従来のTurboBoostを改良した技術が実装されています。CPUパッケージの熱容量を考慮して、できるだけ高い周波数な状態を維持するようにできています*3。なので、Nehalemのグラフでは負荷をかけるCPUと負荷をかけないCPUで周波数の差が顕著に出ていますが、SandyBridgeでは負荷がかかっているCPUとそうでないCPUであまり差がでていません。
最後にアルゴリズム別のアプローチです。Intelの技術資料のアルゴリズムの優位性を示したかったのですが、正直、グラフの形では大きな差が現れませんでした。ただ、負荷がかかっているCPU以外のCPUではグラフの形が違っています。なので、例えば「この瞬間はこんなカーネルスレッドが走っていた」とかもっと裾野を広げて解析すると違いを示せるかもしれません。