はじめに:なぜ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時間延ばすかもしれない。
参考リンク
- Apple Developer - Energy Efficiency Guide for Mac Apps: https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/
- Apple Developer - Prioritize Work with Quality of Service Classes: https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html
- Apple Developer - Dispatch (GCD): https://developer.apple.com/documentation/dispatch
- Apple Developer - Swift Concurrency: https://developer.apple.com/documentation/swift/concurrency
- man powermetrics(ターミナルで
man powermetricsを実行)