retrage.github.io

OSはどうやってP-coreとE-coreを使い分けているのか

Alder Lake以降のIntel CPUでは、P-coreとE-coreの2種類のコアが搭載されている。 P-coreは性能重視、E-coreは省電力重視という位置づけで、OSがうまくこれらのコアを使い分けることで、消費電力と性能の両立が図られている。 ここまでの話は広く知られているが、実際にどのようにしてOSに対してコアの使い分けをさせているのかの実装レベルでの解説は (少なくとも日本語では) ほぼ存在しないようなので調べてみた。

OSから見たP-coreとE-core

OSの役割の一つとしてプロセススケジューリングがあり、どのプロセスをいつどれぐらいの期間どのCPUコアで実行するかを決める。OSができるだけ効率よくプロセスをスケジューリングするためには、CPUコアの性能や消費電力の違いを考慮したスケジューリングが必要になる。そこで、Intel CPUではOSに対して次の2つの情報をプロセススケジューリングのヒントとして提供している。

  • あるCPUコアがどんな性能と電力効率なのか: Intel Hardware Feedback Interface (HFI)
  • あるCPUコアで実行されている処理がどのような性質なのか: Intel Thread Director (ITD)

Intel Hardware Feedback Interface (HFI)

Intel Hardware Feedback Interface (HFI) は、CPUコアの性能と電力効率を表したテーブルである。DRAM上に確保されたHFIの領域にテーブルを作成し、CPUは自動的にそのテーブルを更新する。OSはHFIのテーブルを読み取り、プロセススケジューリングのヒントとして利用することが期待される。

HFIを読む方法

HFIを読むには、最初にCPUID 0x06:0x00を読む。ここにはHFIがサポートされているか、HFI class情報などのHFIのテーブルをパースするために必要な情報が含まれている。 次にMSR 0x17D1: IA32_HW_FEEDBACK_CONFIG を読み、HFIが有効になっているかを調べる。 HFIが有効になっていれば、MSR 0x17D0: IA32_HW_FEEDBACK_PTRにHFIテーブル先頭の物理アドレスが格納されているので、そのアドレスからHFIテーブルを読み取る。

HFIテーブルの構造

HFIテーブルは以下のような構造をしている。最初にヘッダがあり、その後にCPUコアごとのエントリが続く。エントリの数はCPUのコア数と同じである。

#[repr(C, packed)]
pub struct HfiTable<const NUM_CPUS: usize> {
    pub header: HfiHeader,
    pub entries: [HfiEntry; NUM_CPUS],
}

ヘッダには、HFIテーブルの更新時刻など、OSがHFIテーブルを読むべきか判断するための情報が含まれている。各エントリは、そのCPUコアの性能と電力効率を表している。エントリの構造は以下のようになっている。

#[repr(C, packed)]
pub struct HfiEntry {
    perf_cap: u8,
    ee_cap: u8,
    _reserved: [u8; 6],
}

perf_capとee_capがそれぞれそのコアの性能と電力効率を0から255で表している。OSはこれらの値を見て、性能が必要な場合はperf_capの値が大きいコアを、電力効率が必要な場合はee_capの値が大きいコアを選ぶことで、P-coreとE-coreを使い分けることができる。

Intel Thread Director (ITD)

Intel Thread Director (ITD) では、あるコアで実行されている処理の分類の情報を提供する。この情報をOSのプロセススケジューリングのヒントとして利用することが期待される。ITDでは以下の4つのクラスに処理が分類される。

  • Class 0: Non-vectorizable integer or floating-point code.
  • Class 1: Integer or floating-point vectorized code, excluding Intel Deep Learning Boost (Intel DL Boost) code.
  • Class 2: Intel DL Boost code.
  • Class 3: Pause (spin-wait) dominated code.

現時点では上の4つだけだが、Intelのドキュメントでは一般化された形で描かれており、今後サポートされるクラスが増える可能性がある。

分類の方法の詳細は明記されいないが、後述するように、CPUコアは実行した命令の履歴を内部に持っており、その履歴から分類を行っていると考えられる。

ITDを読む方法

CPUID 0x06:0x00を読むと、ITDがサポートされているか、サポートされているITD classの数がわかる。MSR 0x17D4: IA32_HW_FEEDBACK_THREAD_CONFIGにはITDが有効かどうかを示すフラグがあり、有効な場合には、MSR 0x17D2: IA32_THREAD_FEEDBACK_CHARを読むことで、そのコアで実行されている処理のクラスがわかる。

hreset: ITD classをリセットする命令

ITD classは実行した命令の履歴をもとに決められる。このため、あるCPUコアでプロセスの切り替えが起きるなど、実行される命令の性質が切り替わった場合に、十分に命令の履歴が残るまでITD classが正しく設定されるまでに遅延が生じる。この遅延により、OSが効率よくプロセスをスケジューリングできない可能性がある。そこで、OSから明示的にITD classをリセットする、hreset命令が定義されている。

これは想像だが、マルチテナントのVMを提供している環境で、あるVMから別のVMに切り替わった場合などに、ITD classがリセットされなかった場合に、前のVMのITD classが別のVMから観測できてしまう可能性が考えられる。実用的ではないが、間接的に他のVMの情報が漏れる可能性がある。また、もしCPUコアの命令の履歴がサイドチャネル的に観測できた場合、CPUの脆弱性につながるかもしれない。

intel-hfi: HFIとITDを読むためのツール

HFIとITDを読むためのツールとして、intel-hfiを作成した。このツールでは、HFIのテーブルを読んだり、ITD classを読んだりすることができる。Linuxで動かすことを前提としており、msrcpuidのカーネルモジュールをロードする必要がある。

ビルドとカーネルモジュールのロード

ビルド方法は以下の通り。

git clone https://github.com/retrage/intel-hfi.git
cd intel-hfi
cargo build --release

事前にmsrcpuidのカーネルモジュールをロードする:

sudo modprobe msr
sudo modprobe cpuid

HFIテーブルを表示する

CPU #0のHFIテーブルを表示する:

sudo target/release/intel-hfi hfi

出力例:

CPU: 0
  CoreType: Core
HFI Table:
  Address: 0x10aa70000
  Size: 0x1000
  Timestamp: 881758343
  Performance Capability:
    Updated: true
    Idle Requested: false
  Energy Efficiency Capability:
    Updated: true
    Idle Requested: false
  CPU 0:
    Performance Capability: 70
    Energy Efficiency Capability: 92

この例では、CPU #0のperf_capが70、ee_capが92であることがわかる。

ちなみに、動いていないCPUコアはすべてが0である。

  CPU 12:
    Performance Capability: 0
    Energy Efficiency Capability: 0

OSの気持ちになってHFIテーブルを解釈してみる

上の例では、ただ値を読んだだけなので、実際にOSがHFIテーブルを解釈する場合を考えてみる。例えば、あなたがOSだったとして、以下のようなHFIテーブルがあったとする。 このとき、OSである、あなたはどのCPUコアを選ぶべきだろうか。

  CPU 0:                                                                        
    Performance Capability: 70                                                  
    Energy Efficiency Capability: 92                                            
# snip
  CPU 4:
    Performance Capability: 74
    Energy Efficiency Capability: 92
# snip
  CPU 8:
    Performance Capability: 43
    Energy Efficiency Capability: 100

上の例では、perf_capは CPU#4 > CPU#0 > CPU#8 となっており、ee_capは CPU#8 > CPU#0 >= CPU#4 となっている。これらの値を見ると、CPU#4が最も性能が高いが、消費電力が高いことがわかる。一方、CPU#8は性能は低いが、消費電力が低いことがわかる。この場合、性能を優先したい場合はCPU#4を、電力効率を優先したい場合はCPU#8を選ぶのが効率的である。

なお、HFIテーブルでは、P-coreとE-coreを区別せずに、同じテーブルで表現している。このため、OSはP-coreとE-coreを区別することなく、HFIテーブルの情報でプロセススケジューリングを効率化できる。

ITD classを表示する

ITDを有効にする

ITDはLinux 6.3.0ではデフォルトで無効になっているので、有効にする必要がある。今回は、ITDを有効にするカーネルモジュールを作成して、それをロードする。

cd itd-kmod
make
sudo insmod itd.ko

ITD classを表示する

ちゃんとITD classが設定されているか確認するために、特定のCPUコアに対して、特定の処理を実行させて、ITD classが変化するか確認する。今回は、CPU #0でclass 3の処理を実行させる。

cargo build --release --example class3
taskset 0x1 ./target/release/examples/class3

この状態で、別のターミナルでITD classを表示する:

sudo target/release/intel-hfi itd

出力例:

CPU: 0
  CoreType: Core
HFI Table:
  Address: 0x10aa70000
  Size: 0x1000
ITD Table (CPU 0):
  ITD enabled: true
  HRESET enabled: false
  ITD class ID: 3

この例では、確かにCPU #0でclass 3と分類されていることがわかる。

Ehhanced Hardware Feedback Interface (EHFI)

ITDとともに導入された機能として、Enhanced Hardware Feedback Interface (EHFI) がある。これは、HFIを一般化した形で拡張したものである。具体的には、各コアのITD classごとにHFIのエントリ追加して表すようにしたものである。これまでのHFIは、ITD class 0のみサポートされているEHFIとみなすことができる。

EHFIについても所持しているRaptor LakeのCPUで確認してみたが、EHFIのテーブルとして解釈しても、ITD class 0以外はすべて0となっており、EHFIのテーブルとしては使われていないようだった。

各OSでのHFIとITDの利用状況

こちらの記事によれば、2022年8月末時点ではWindowsではHFIとITDどちらもサポートされているとのことである。Linuxでは、HFIへの対応はすでにtorvalds/linuxに入っているものの、ITDへの対応はパッチが送られているものの、まだupstreamには入っていないようである。 LinuxのITD対応はIntelのRicardo Neriさんらによって進められているらしく、書いている最中にLPC 2022での発表資料を見つけた。詳細なLinuxでの実装はこちらが参考になるはずである。

また、自作OSを開発している方でP-coreとE-coreを立ち上げる方法について解説している。

参考文献