0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

P-core / E-core ってなんだ?〜Apple Siliconの「使い分け」戦略〜

0
Posted at

はじめに:なぜMacBookのバッテリーは持つのか

MacBook Airを使っていて、不思議に思ったことはないか。

「重い処理をしてもファンが回らない(そもそもファンがない)のに、軽い作業だとバッテリーが20時間持つ」

この矛盾、どう説明する?

答えは「異種混合コアアーキテクチャ」にある。

Apple Siliconには、2種類のCPUコアが搭載されている。P-core(Performance Core)とE-core(Efficiency Core)。高性能コアと高効率コア。

IntelのAlderLake以降や、ARMのbig.LITTLE設計と似たコンセプトだが、Appleの実装は一味違う。

今回は、この2種類のコアの使い分けと、開発者として意識すべきQoS(Quality of Service)について解説する。


P-coreとE-core:何が違うのか

P-core(Performance Core)- 高性能コア

別名「Firestorm」(M1/M2世代)、「Avalanche」(M3)、「Sawtooth」(M4)。

特徴を挙げよう。

高クロック動作
M4では最大4.5GHz。フルスピードで回ると、そのへんのデスクトップCPUを凌ぐシングルスレッド性能を発揮する。

広い実行パイプライン
分岐予測、命令デコード、実行ユニットすべてが大規模。複雑な処理を高速に捌ける。

大きなキャッシュ
L1/L2キャッシュが大きく、データアクセスが高速。

消費電力も大きい
フル稼働時は数ワット〜10ワット以上を消費。発熱も大きい。

E-core(Efficiency Core)- 高効率コア

別名「Icestorm」(M1/M2)、「Blizzard」(M3)、「Gale」(M4)。

低クロック動作
P-coreの半分〜3分の2程度のクロック。でもそれで十分な処理もある。

コンパクトな設計
実行パイプラインがシンプル。トランジスタ数が少ない。

省電力
フル稼働でも数百ミリワット程度。P-coreの10分の1以下。

それでも十分速い
Intel Core i7(第7世代くらい)と同等の性能がある。日常タスクには余裕で対応。

チップごとのコア構成

M4:4 P-cores + 6 E-cores = 10コア
M4 Pro:10 P-cores + 4 E-cores = 14コア(または8+4=12)
M4 Max:12 P-cores + 4 E-cores = 16コア(または10+4=14)

面白いのは、上位チップほどP-coreの比率が高いこと。プロ向けのワークロードは「効率より性能」を求めるから。


macOSはどうやって使い分けているのか

「自分で指定しなくても、勝手にいい感じにやってくれる」

これがAppleの設計思想。でも、その「いい感じ」がどう実現されているか知っておくと、より効率的なコードが書ける。

スケジューラの基本動作

macOSのスケジューラは、タスクの特性を見てコアを割り当てる。

バックグラウンドタスク
メール同期、Spotlight検索インデックス作成、iCloudアップロードなど。E-coreで実行。ユーザーが気づかないうちに、省電力で処理される。

フォアグラウンドタスク
ユーザーが触っているアプリ。P-coreが使われる傾向がある。レスポンスが重要だから。

バースト的な負荷
動画エンコードの開始時など。最初はP-coreがフル稼働し、安定したらE-coreに移行することも。

QoS(Quality of Service)

ここが開発者のコントロールポイント。

macOS/iOSでは、スレッドやキューに「QoS」を設定できる。これがスケジューラへのヒントになる。

QoSレベルは5段階。

userInteractive(最高)
UIの更新、アニメーション。即座に応答が必要。P-coreを優先使用。

userInitiated(高)
ユーザーが開始した処理。ボタンを押して結果を待っている状態。P-core寄り。

default(中)
明示的にQoSを指定しない場合のデフォルト。

utility(低)
長時間かかる処理。進捗バーが出るようなやつ。E-core寄り。

background(最低)
ユーザーが意識しない処理。インデックス作成、バックアップなど。E-core優先。


実践:QoSを意識したコーディング

Grand Central Dispatch(GCD)

// バックグラウンドQoSのキュー
let backgroundQueue = DispatchQueue.global(qos: .background)

// ユーザーインタラクティブQoSのキュー
let interactiveQueue = DispatchQueue.global(qos: .userInteractive)

// バックグラウンドで重い処理
backgroundQueue.async {
    // この処理はE-coreで実行される傾向がある
    let result = heavyComputation()
    
    // UIの更新はメインキューで
    DispatchQueue.main.async {
        self.updateUI(with: result)
    }
}

// 即座に応答が必要な処理
interactiveQueue.async {
    // この処理はP-coreで実行される傾向がある
    let result = quickComputation()
}

Swift Concurrency(async/await)

// Taskの優先度を指定
Task(priority: .background) {
    // E-core寄りで実行
    await performBackgroundWork()
}

Task(priority: .userInitiated) {
    // P-core寄りで実行
    await performUserRequestedWork()
}

// detached taskで明示的に優先度を下げる
Task.detached(priority: .utility) {
    // 長時間処理
    await indexAllDocuments()
}

OperationQueue

let queue = OperationQueue()
queue.qualityOfService = .utility  // キュー全体のQoS

let operation = BlockOperation {
    // この処理はutility QoSで実行
    processFiles()
}
operation.qualityOfService = .userInitiated  // 個別にオーバーライド可能

queue.addOperation(operation)

バッテリーへの影響:実測してみよう

「本当にQoSでバッテリー持ちが変わるの?」

変わる。powermetricsで確認できる。

# 管理者権限で実行
sudo powermetrics --samplers cpu_power -i 1000 -n 60

これで1秒ごと、60回のCPU消費電力をサンプリングできる。

同じ処理を、QoSを変えて実行してみよう。

import Foundation

func runComputation(qos: DispatchQoS.QoSClass) {
    let queue = DispatchQueue.global(qos: qos)
    let group = DispatchGroup()
    
    for _ in 0..<4 {
        group.enter()
        queue.async {
            var sum: Double = 0
            for i in 0..<10_000_000 {
                sum += Double(i).squareRoot()
            }
            print("Result: \(sum)")
            group.leave()
        }
    }
    
    group.wait()
}

// テスト1: background QoS
print("Running with background QoS...")
runComputation(qos: .background)

// 少し待つ
Thread.sleep(forTimeInterval: 5)

// テスト2: userInteractive QoS
print("Running with userInteractive QoS...")
runComputation(qos: .userInteractive)

powermetricsの出力で「E-Cluster Power」と「P-Cluster Power」を見ると、QoSによってどちらのコアクラスターが使われているかわかる。

background QoSでは E-Cluster の消費が上がり、userInteractive では P-Cluster が上がる。消費電力の差は数倍になることもある。


アクティビティモニタで確認する

GUI派には、アクティビティモニタがおすすめ。

「表示」→「列」で以下を追加すると、プロセスごとのコア使用状況が見える。

  • %CPU:CPU使用率
  • スレッド:スレッド数

さらに「ウインドウ」→「CPU履歴」を開くと、各コアの稼働状況がリアルタイムで見える。P-coreとE-coreで色が分かれているわけではないが、コア番号でおおよそ判別できる(若い番号がP-core、後ろがE-core)。


開発者が気をつけるべきこと

1. 過剰なuserInteractiveの乱用を避ける

「速くしたいから全部userInteractiveで」

これは悪手。本当にユーザーが待っている処理だけに限定すべき。

乱用すると、バッテリー消費が増えるだけでなく、スケジューラの優先度判断が歪む。本当に重要な処理が埋もれてしまう。

2. バックグラウンド処理は素直にbackground QoS

「ユーザーに見えないから、ちょっと遅くてもいいや」

その発想は正しい。インデックス作成、ログ送信、キャッシュクリーンアップなどは、遠慮なくbackground QoSを使おう。

macOSは賢いから、システムがアイドル状態になったときにまとめて処理してくれる。

3. 優先度の逆転に注意

低優先度のスレッドが高優先度のスレッドをブロックする「優先度逆転」問題。

// 悪い例:ロックの取り方
let lock = NSLock()
var sharedResource = 0

// 低優先度タスク
DispatchQueue.global(qos: .background).async {
    lock.lock()
    // 長い処理...
    Thread.sleep(forTimeInterval: 5)
    sharedResource = 42
    lock.unlock()
}

// 高優先度タスク(低優先度のロック解放を待つことに...)
DispatchQueue.global(qos: .userInteractive).async {
    lock.lock()  // ここで待たされる
    print(sharedResource)
    lock.unlock()
}

GCDのDispatchSemaphoreやActor(Swift Concurrency)は、この問題を軽減する仕組みを持っている。生のNSLockより推奨。

4. QoS伝播を理解する

親タスクから子タスクにQoSが伝播するルールがある。

Task(priority: .background) {
    // ここはbackground
    
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            // これもbackgroundを継承
            await someWork()
        }
        
        group.addTask(priority: .userInitiated) {
            // 明示的に指定すればオーバーライド可能
            await urgentWork()
        }
    }
}

P-core / E-coreの「クラスター」構造

M1以降のApple Siliconでは、コアは「クラスター」単位でグループ化されている。

M4 Maxの例:

  • P-cluster 1:4 P-cores(L2キャッシュ共有)
  • P-cluster 2:4 P-cores(L2キャッシュ共有)
  • P-cluster 3:4 P-cores(L2キャッシュ共有)
  • E-cluster:4 E-cores(L2キャッシュ共有)

同じクラスター内のコアは、L2キャッシュを共有する。つまり、関連するスレッドを同じクラスターで動かすと、キャッシュヒット率が上がって効率的。

macOSのスケジューラはこれを考慮してスレッドを配置するが、完璧ではない。マルチスレッドのパフォーマンスチューニングでは、このクラスター構造を意識すると良い結果が出ることがある。


ベンチマーク:同じ処理、異なるQoS

実際にどれくらい差が出るか、シンプルなベンチマークを取ってみよう。

import Foundation

func benchmark(qos: DispatchQoS.QoSClass, label: String) {
    let queue = DispatchQueue(label: "benchmark", qos: qos)
    let iterations = 50_000_000
    
    let start = CFAbsoluteTimeGetCurrent()
    
    let semaphore = DispatchSemaphore(value: 0)
    queue.async {
        var sum: Double = 0
        for i in 0..<iterations {
            sum += sin(Double(i) * 0.0001)
        }
        print("\(label): sum = \(sum)")
        semaphore.signal()
    }
    semaphore.wait()
    
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("\(label): \(elapsed) seconds")
}

// 実行
benchmark(qos: .background, label: "background")
benchmark(qos: .userInteractive, label: "userInteractive")

結果例(M4 MacBook Pro):

background: 2.3 seconds
userInteractive: 0.8 seconds

同じ処理なのに、約3倍の速度差。これがP-coreとE-coreの違い。

ただし、バッテリー消費は userInteractive の方が大きい。速度とエネルギーのトレードオフだ。


まとめ:適材適所の美学

P-core と E-core は、Apple Silicon の「使い分け」戦略の核心だ。

P-core(Performance Core)

  • 高クロック、高性能、高消費電力
  • ユーザーが待っている処理に使う
  • userInteractive, userInitiated QoS

E-core(Efficiency Core)

  • 低クロック、省電力、それでも十分速い
  • バックグラウンド処理に使う
  • utility, background QoS

開発者としてやるべきこと

  • 適切なQoSを設定する
  • バックグラウンド処理は遠慮なくbackgroundに
  • 過剰なuserInteractiveは避ける
  • powermetricsやアクティビティモニタで確認

macOSが自動でやってくれること

  • タスク特性に応じたコア割り当て
  • 温度・バッテリー状況に応じた動的調整
  • 優先度逆転の緩和(ある程度)

Apple Siliconの省電力と高性能の両立は、このP/Eコアアーキテクチャなしには実現しなかった。

コードを書くとき、「この処理は本当に急ぎなのか?」と一度考えてみよう。その一手間が、ユーザーのバッテリーを1時間延ばすかもしれない。


参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?