같은 코드도 무엇에 시간을 쓰느냐(연산 vs 대기), 데이터가 어디에 있느냐(레지스터~스토리지), 어떻게 빌드했느냐에 따라 체감 성능이 수십 배까지 갈립니다. 이 장은 CPU 관점에서 성능·배터리·발열이 어떻게 연결되는지 원리부터 iOS 실무까지 잇습니다.
Q1. CPU-bound와 I/O-bound 작업은 무엇이 다른가?
작업의 완료 시간을 무엇이 지배하느냐가 기준입니다. CPU-bound는 계산 자체가 병목이라 CPU가 쉴 틈 없이 돌고, 코어를 빨리·많이 줄수록 빨라집니다. I/O-bound는 디스크·네트워크 응답을 기다리는 시간이 병목이라 그동안 CPU는 놀고, 빠른 CPU는 거의 도움이 안 되며 대기를 겹치는(동시성) 전략이 답입니다. 판별은 간단합니다 — CPU 시간이 벽시계 시간과 비슷하면 CPU-bound, 훨씬 작으면 I/O-bound입니다.
CS 원리
둘의 차이는 실행 중 스레드의 상태로 드러납니다. CPU-bound 스레드는 대부분 RUNNING 상태로 코어를 점유하다 스케줄러에 의해 선점(preempt)됩니다. I/O-bound 스레드는 read()·recv() 같은 시스템 콜에서 BLOCKED가 되어 코어를 반납하고, I/O가 끝나면 깨어납니다. 그래서 병렬화 전략이 갈립니다. CPU-bound는 코어 수만큼 병렬(parallelism)로 나눌 때 이득이 코어 수에서 포화되고, I/O-bound는 대기를 겹치는 동시성(concurrency)이면 충분해 코어 수보다 훨씬 많은 요청을 한 스레드로도 소화할 수 있습니다.
iOS에서는
Darwin/XNU 스케줄러에서 I/O로 블록된 스레드는 디스케줄되고, CPU-bound 스레드는 타임 퀀텀을 다 쓰면 선점됩니다. 프레임워크 층에서는 전략이 더 명확히 갈립니다. GCD의 전역 큐에 동기 I/O를 잔뜩 던지면, 블록된 워커가 늘어날 때마다 GCD가 새 스레드를 만들어 thread explosion(스레드 폭발)과 최악의 경우 데드락으로 이어집니다. 반대로 CPU-bound 병렬 계산은 DispatchQueue.concurrentPerform(iterations:)로 코어 수에 맞춰 나누는 게 정석입니다.
Swift Concurrency의 협력적 스레드 풀(cooperative thread pool)은 코어 수만큼만 스레드를 둡니다(WWDC21 Swift concurrency: Behind the scenes). 그래서 이 풀 위에서 동기 I/O로 스레드를 블록하거나, 오래 걸리는 CPU 계산을 await 지점 없이 붙잡고 있으면 전진 보장(forward progress)이 깨져 다른 태스크가 굶습니다. I/O는 URLSession의 async API처럼 논블로킹으로 쓰고, 긴 계산은 잘게 쪼개 await Task.yield()로 양보하거나 별도로 오프로딩해야 합니다. 그리고 둘 중 무엇이든 메인 스레드에서 하면 프레임 드랍(hitch)이 납니다.
실험 · 도구
가장 확실한 판별법은 벽시계 시간(wall-clock)과 CPU 시간을 함께 재는 것입니다. 둘이 비슷하면 CPU가 내내 일한 것(CPU-bound), CPU 시간이 훨씬 작으면 그 차이만큼 기다린 것(I/O-bound)입니다.
import Foundation
func classify(_ label: String, _ work: () -> Void) {
let wallStart = ContinuousClock.now
let cpuStart = clock() // 프로세서가 실제로 돈 시간(근사)
work()
let wall = wallStart.duration(to: .now)
let cpu = Double(clock() - cpuStart) / Double(CLOCKS_PER_SEC)
print("\(label): wall=\(wall), cpu≈\(cpu)s")
// cpu ≈ wall → CPU-bound (계산이 병목)
// cpu << wall → I/O-bound (대부분 대기)
}도구로는 Instruments의 Time Profiler가 CPU-bound의 핫 함수를 보여줍니다. I/O-bound는 Time Profiler상 CPU가 낮게 나오므로, System Trace의 Thread State로 스레드가 언제 Blocked였는지 보거나 os_signpost로 대기 구간을 직접 계측하는 편이 낫습니다.
프로젝트 적용
- CPU-bound: 백그라운드 QoS(
.userInitiated/.utility)로 옮기고, 병렬화 가능한 반복은concurrentPerform. 스레드를 코어 수보다 많이 만들지 말 것 — 오버서브스크립션은 컨텍스트 스위치 낭비만 늘린다. - I/O-bound: 동기 API로 스레드를 붙잡지 말고 async/await·논블로킹으로 대기를 겹칠 것.
- 하지 말 것: 협력적 스레드 풀이나 GCD 전역 큐에서 동기 블로킹 I/O 대량 투입(스레드 폭발), 메인 스레드에서의 무거운 연산.
"스레드를 더 많이 만들면 항상 빨라진다"는 틀립니다. CPU-bound는 코어 수에서 이득이 포화되고 그 이상은 오히려 스위칭 비용으로 느려집니다. I/O-bound가 많은 동시 요청으로 빨라지는 것도 요청당 스레드 때문이 아니라 대기를 겹치기 때문이며, 이는 동기 스레드가 아니라 논블로킹/async로 달성해야 합니다.
꼬리 질문
- 한 작업이 "다운로드 후 디코딩"처럼 I/O와 CPU가 섞여 있으면, 파이프라인을 어떻게 설계해야 처리량이 최대가 되나?
- 코어가 8개인데 CPU-bound 스레드를 16개 만들면 총 처리 시간과 지연시간은 각각 어떻게 되나?
- 동기 네트워크 호출 100개를 GCD 전역 concurrent 큐에 던지면 무슨 일이 벌어지고, 왜 데드락까지 갈 수 있나?
Q2. Register, CPU Cache, RAM, Storage의 비용은 어떻게 다른가?
메모리는 피라미드형 계층입니다. 위로 갈수록 빠르고 작고 비싸며(레지스터), 아래로 갈수록 느리고 크고 쌉니다(스토리지). 대략적인 접근 지연은 레지스터 1ns 미만, 캐시 수 ns, RAM 수십~100ns, SSD 수십~수백 µs로 단계마다 자릿수가 바뀝니다. 특히 L1 캐시와 RAM은 약 100배 차이라, 실무 성능은 알고리즘보다 데이터가 캐시에 얼마나 잘 들어오느냐(지역성)가 좌우하는 경우가 많습니다.
CS 원리
캐시가 작동하는 이유는 프로그램의 지역성(locality)입니다. 방금 쓴 데이터를 곧 또 쓰고(시간 지역성), 이웃한 주소를 연달아 쓰는(공간 지역성) 경향이 있어서, 자주 쓰는 것만 빠른 곳에 두면 평균 접근 시간이 크게 줄어듭니다. 캐시는 바이트 단위가 아니라 캐시라인이라는 고정 블록 단위로 메모리를 끌어옵니다(x86은 보통 64바이트, Apple Silicon은 128바이트). 물리적으로 캐시는 빠르지만 비싼 SRAM, RAM은 값싼 DRAM이라 용량/속도/가격이 서로 맞물려 있습니다.
iOS에서는
Apple Silicon은 통합 메모리 아키텍처(Unified Memory)라 CPU·GPU·Neural Engine이 같은 물리 DRAM 풀을 공유합니다. 그래서 전통적인 CPU↔GPU 간 복사 비용이 줄어드는 대신, 이 공유 대역폭을 아끼는 게 중요해집니다. 참고로 Apple Silicon의 캐시라인은 128바이트라, 이웃 데이터를 한 번에 더 많이 끌어오는 만큼 false sharing 같은 문제도 그 단위로 생깁니다.
Swift에서 지역성은 값 타입 vs 참조 타입 선택으로 직결됩니다. struct 배열은 원소들이 메모리에 연속으로 놓여(캐시 친화적) 순회 시 캐시라인을 알뜰히 재사용합니다. 반면 class 인스턴스 배열은 사실상 힙에 흩어진 객체들을 가리키는 포인터 배열이라, 순회할 때마다 포인터를 따라 여기저기 점프하는 포인터 추적(pointer chasing)이 일어나 캐시 미스가 잦고 ARC의 retain/release 트래픽까지 겹칩니다(WWDC16 Understanding Swift Performance). 성능이 중요한 뜨거운 경로에서 데이터 레이아웃을 값 타입 연속 저장으로 두는 것이 정석인 이유입니다.
실험 · 도구
지역성의 위력은 같은 데이터를 순서만 바꿔 훑어 보면 드러납니다. 연속 버퍼를 행 우선(이웃 접근)과 열 우선(큰 스트라이드 점프)으로 각각 더해 보면, 계산량은 똑같은데도 열 우선이 몇 배 느립니다 — 매 접근이 새 캐시라인을 건드리기 때문입니다.
let n = 4_000
var a = [Int](repeating: 1, count: n * n) // 하나의 연속 버퍼
// 행 우선: 이웃 원소를 차례로 → 캐시라인 재사용 (빠름)
var s = 0
for i in 0..<n { for j in 0..<n { s &+= a[i*n + j] } }
// 열 우선: 매번 n칸씩 건너뜀 → 접근마다 새 캐시라인 (느림)
for j in 0..<n { for i in 0..<n { s &+= a[i*n + j] } }iOS 디바이스에는 캐시 미스 카운터를 직접 보여주는 공개 도구가 마땅치 않습니다. macOS에서는 Instruments의 CPU Counters 템플릿이 PMU 하드웨어 카운터(cache miss 등)를 노출하고, 디바이스에서는 Time Profiler로 병목 위치를 잡거나 위처럼 A/B 벤치마크로 상대 비용을 확인하는 게 현실적입니다.
프로젝트 적용
- 뜨거운 루프의 데이터는 값 타입 연속 저장(
Array·ContiguousArray)으로. 링크드 리스트류 포인터 추적 구조는 성능 경로에서 피한다. - 워킹셋을 작게 유지해 캐시에 들어오게 한다. 필요하면 SoA(Struct of Arrays)로 재배치.
- 차가운(cold) 경로까지 과하게 최적화하지 말 것 — 지역성 최적화는 프로파일러가 지목한 핫스팟에만.
"요즘 RAM은 빠르니 캐시나 지역성은 신경 안 써도 된다"는 오해입니다. L1과 RAM은 약 100배 차이라 캐시 미스가 잦으면 실제 성능을 지배합니다. 또 "레지스터·캐시는 개발자가 직접 지정한다"도 틀립니다 — 배치·교체는 대체로 컴파일러와 하드웨어가 관리하고, 개발자는 데이터 레이아웃과 접근 패턴으로 간접적으로만 영향을 줍니다.
꼬리 질문
- 캐시를 그냥 무한히 크게 만들면 되지 않나? 왜 그렇게 못 하나?
- false sharing이 무엇이고, 멀티코어에서 성능을 어떻게 갉아먹나?
- Swift에서 같은 개수의
struct배열과class배열을 순회할 때 속도 차이가 나는 이유를 캐시와 ARC 관점에서 설명한다면?
Q3. 알고리즘 복잡도와 실제 실행 시간은 어떻게 함께 판단하는가?
Big-O는 입력이 커질 때의 증가율만 말하고 상수 항과 하드웨어 효과는 버립니다. 즉 "얼마나 잘 확장되는가"이지 "지금 얼마나 빠른가"가 아닙니다. 그래서 같은 O라도 상수 차이로 몇 배가 나고, 작은 n에서는 O(n²)이 O(n log n)을 이기기도 합니다. 실무 판단은 둘을 겹쳐서 합니다 — Big-O로 예상 n 범위에 맞는 후보를 고르고, 실제 상수(캐시·분기예측·할당·ARC 등)는 측정으로 확인합니다.
CS 원리
Big-O는 n→∞에서 상수 배수와 저차항을 무시한 점근적 상한입니다. 그래서 상수 인자가 큰 O(1)이 상수가 작은 O(n)보다 작은 입력에서 느릴 수 있습니다. 정렬 라이브러리가 작은 구간에서 삽입 정렬로 전환하는 것도 같은 이유입니다 — 점근적으로 나쁜 O(n²) 삽입 정렬이 작은 n에서는 상수가 작아 퀵/머지 정렬보다 빠릅니다. 상수 인자는 메모리 접근 패턴(캐시), 분기 예측, 명령어 수, 숨은 비용(할당·해싱·복사)에서 나옵니다.
iOS에서는
Swift 컬렉션의 상수 인자는 꽤 다릅니다. Array.contains는 O(n)이지만 연속 메모리라 상수가 작고, Set.contains는 평균 O(1)이지만 해싱 비용과 잠재적 캐시 미스로 상수가 큽니다. 그래서 원소가 수십 개 이하로 작으면 배열 선형 탐색이 Set보다 빠른 구간이 실제로 존재합니다. 또 Swift에는 숨은 O(n)이 많습니다 — Array는 copy-on-write라 공유 중인 배열을 수정하면 O(n) 복사가 튀고, removeFirst()는 뒤 원소를 전부 당겨 O(n)입니다. 이런 게 루프 안에 들어가면 겉보기 O(n) 코드가 조용히 O(n²)이 됩니다(Array 문서).
var queue = Array(0..<100_000)
while !queue.isEmpty {
let x = queue.removeFirst() // 앞을 지우면 뒤 전부를 앞으로 당김 → 매번 O(n)
_ = x // 루프 n번 × O(n) = 전체 O(n²)
}
// 개선: 인덱스로 순회하거나 swift-collections의 Deque(앞 제거 O(1)) 사용
실험 · 도구
판단의 마지막은 항상 측정입니다. n을 여러 값으로 바꿔가며 시간을 재서 교차점이 실제로 어디인지 확인합니다. ContinuousClock으로 구간을 재거나 XCTest의 measure { }로 반복 측정하고, 상수 인자가 어디서 나오는지는 Time Profiler로 봅니다.
func bench(_ n: Int, _ body: () -> Void) -> Duration {
let start = ContinuousClock.now
body()
return start.duration(to: .now)
}
for n in [8, 64, 512, 4096] {
print(n, bench(n) { /* 후보 알고리즘 */ })
} // 작은 n과 큰 n에서 승자가 뒤바뀌는지 관찰프로젝트 적용
- 내 n의 현실적 범위를 먼저 안다. 아주 작은 컬렉션은 단순한 알고리즘이 대개 더 빠르고 읽기 쉽다.
- 도달하지도 않을 n을 위한 점근 최적화에 시간을 쓰지 말 것.
- 루프 안의 숨은 O(n)(COW 복사,
removeFirst, 문자열 이어붙이기)을 경계하고, 크기를 아는 배열은reserveCapacity로 재할당을 없앤다.
"O(1)은 언제나 O(n)보다 빠르다"와 "Big-O를 보면 성능을 안다"는 둘 다 틀립니다. O(1)이라도 상수(해싱·할당)가 크면 작은 입력에서 작은 O(n)에 집니다. 그리고 Big-O는 증가율이지 측정된 시간이 아닙니다 — 같은 O(n log n) 두 구현이 상수 때문에 3배 차이 나는 건 흔한 일이라, 결국 재 봐야 압니다.
꼬리 질문
- 입력 n이 항상 10 이하로 보장된다면, 굳이 O(n log n) 대신 O(n²)을 써도 되는 근거는 무엇인가?
- 같은 O(n log n) 정렬 A가 B보다 3배 느리다면, 상수 인자의 원인 후보를 캐시·분기·할당 관점에서 셋 이상 들 수 있나?
- Swift에서 배열 앞에 원소를 자주 넣고 빼야 한다면 어떤 자료구조로 바꿔 O(n²)을 O(n)으로 낮추겠나?
Q4. Debug build와 Release build의 성능 차이는 왜 큰가?
Debug는 디버깅 편의를 위해 최적화를 끄고(-Onone), Release는 최적화를 켭니다(-O). 차이의 핵심은 Swift 특유의 비용입니다 — 최적화가 켜지면 함수 인라인, 불필요한 ARC retain/release 제거, 제네릭 특수화(specialization), 전모듈 최적화(WMO)가 일어나 계산 중심 코드가 수십 배까지 빨라질 수 있습니다. 그래서 Debug에서 잰 성능 숫자는 신뢰할 수 없고, 벤치마크·프로파일링은 반드시 Release로 해야 합니다.
CS 원리
컴파일러 최적화는 관찰 가능한 동작을 보존하면서 더 빠른 기계어로 바꾸는 변환입니다. Debug 빌드는 이런 변환을 거의 끕니다 — 소스와 기계어를 가깝게 유지해야 브레이크포인트가 정확한 줄에 걸리고 변수 값이 그대로 보이기 때문입니다. Release 빌드는 인라인, 죽은 코드 제거, 루프 언롤링, 레지스터 할당, 상수 폴딩, 벡터화 같은 변환을 켭니다. C 세계의 -O0 vs -O2 구도와 같습니다.
iOS에서는
Swift의 최적화 레벨은 빌드 설정 SWIFT_OPTIMIZATION_LEVEL로, Debug는 -Onone, Release는 -O(속도) 또는 -Osize(크기)입니다. Swift에서 Debug가 유독 느린 이유는 C보다 Swift에 최적화로 없앨 수 있는 오버헤드가 훨씬 많기 때문입니다.
| Swift 오버헤드 | Debug (-Onone) | Release (-O) |
|---|---|---|
| 함수 인라인 | 거의 안 함(작은 게터도 실제 호출) | 적극 인라인 |
| ARC retain/release | 중복까지 그대로 방출 | 불필요한 것 대거 제거 |
| 제네릭 | witness table 동적 디스패치(boxing) | 구체 타입으로 특수화 |
| 전모듈 최적화(WMO) | 파일 단위 컴파일 | 모듈 전체를 함께 최적화 |
assert(_:) | 런타임에 실행 | 컴파일 시 제거(비용 0) |
전모듈 최적화(WMO)는 파일 경계를 넘어 인라인과 제네릭 특수화를 가능하게 해 런타임을 크게 끌어올리지만, 컴파일러가 모듈 전체를 한 번에 봐야 해서 빌드는 느려집니다. 제네릭 특수화는 func f<T>(...)를 실제로 쓰인 구체 타입별 버전으로 복제해 동적 디스패치와 boxing을 없애는 것으로, ARC 제거와 함께 Swift에서 가장 큰 이득원입니다(Swift OptimizationTips).
// Debug(-Onone): 이 제네릭은 특수화되지 않고 witness table로 동적 디스패치
func total<T: Numeric>(_ xs: [T]) -> T { xs.reduce(.zero, +) }
// Release(-O + WMO): Int 등 구체 타입으로 특수화 + 인라인 + ARC 제거 → 수십 배↑
assert(index >= 0) // Release(-O)에서는 사라짐 → 런타임 비용 0
precondition(index >= 0) // Release에서도 유지됨 (-Ounchecked에서만 제거)실험 · 도구
같은 계산 루프를 Debug와 Release로 각각 실행해 시간을 비교하면 차이가 바로 드러납니다. 핵심 규칙은 성능 측정은 절대 Debug로 하지 않는다입니다. Instruments도 최적화된(Release 또는 -O 프로파일링) 빌드에 붙여야 현실적인 숫자가 나옵니다. Xcode의 Scheme에서 Run/Test 구성을 Release로 바꾸거나, 벤치마크 타깃에 -O를 켜서 재세요.
프로젝트 적용
- 벤치마크·프로파일링은 Release(또는 최적화 켠 구성)로만. Debug 체감 성능으로 판단 금지.
- 불변식 검사는 성격에 맞게 —
assert는 개발 중에만 필요한 검사(Release에서 제거),precondition은 Release에서도 반드시 지켜야 할 검사. -Ounchecked는 배열 경계·정수 오버플로 검사까지 제거하므로, 안전을 증명하지 못하면 켜지 말 것.
"Debug에서 느리면 Release에서도 비슷하게 느리다"는 틀립니다. Swift에서 그 격차는 대부분 ARC·제네릭 오버헤드에서 오고 -O가 이를 없애므로, Debug 숫자는 성능 판단에 무의미합니다. 또 "Release는 Debug에서 로그만 뺀 것"이라는 생각도 틀립니다 — 최적화·WMO·assert 제거가 얽힌 근본적으로 다른 컴파일이라, Release에서만 재현되는 버그(초기화 안 된 값·데이터 레이스·미정의 동작)도 생길 수 있습니다.
꼬리 질문
assert()와precondition()은 Release 빌드에서 각각 어떻게 처리되며, 언제 무엇을 써야 하나?- Debug에선 멀쩡한데 Release에서만 크래시가 난다면, 원인 후보를 최적화·타이밍·미정의 동작 관점에서 무엇부터 의심하겠나?
- WMO를 켜면 왜 빌드는 느려지고 런타임은 빨라지는가? 증분 빌드 관점의 트레이드오프는?
Q5. CPU 사용량은 배터리와 발열에 어떻게 연결되는가?
CMOS 회로의 동적 소비 전력은 대략 P ∝ C·V²·f(정전용량·전압²·주파수)입니다. CPU를 세게 돌리면 주파수·전압이 올라(DVFS) 전력이 급증하고, 그 전력은 배터리 소모이자 곧 발열입니다. 소비 에너지는 전력×시간이라 "얼마나 세게"와 "얼마나 오래"가 모두 중요하고, 열이 쌓여 온도가 임계에 닿으면 스로틀링(주파수↓)으로 성능이 깎입니다. 그래서 CPU 사용량은 배터리·발열·지속 성능이 한 줄로 엮인 문제입니다.
CS 원리
CMOS에서 전력은 스위칭할 때 커패시턴스를 충·방전하며 소모되는 동적 전력과, 가만히 있어도 새는 누설 전력(static/leakage)으로 나뉩니다. 동적 전력이 전압의 제곱에 비례한다는 점이 핵심입니다(CMOS 전력 모델) — 주파수를 올리려면 전압도 올려야 하므로 성능을 조금 더 짜낼 때 전력은 훨씬 가파르게 뜁니다. 소모한 전력은 에너지 보존에 따라 사실상 전부 열이 되고, 열 발생이 방출을 넘어서면 온도가 오릅니다. 여기서 흔한 오해가 갈립니다 — 짧고 굵게 끝내고 깊은 idle로 들어가는 "race to idle"이, 낮은 전력으로 오래 끄는 것보다 총 에너지가 적을 수 있습니다(idle 전력이 매우 낮기 때문). 다만 항상은 아니고 작업 성격에 달렸습니다.
iOS에서는
Apple Silicon은 성능(P) 코어와 효율(E) 코어를 함께 두고 DVFS로 주파수·전압을 조절합니다. 스케줄러는 낮은 QoS·백그라운드 작업을 훨씬 전력이 낮은 E 코어에 배치하므로, 작업의 QoS를 올바로 지정하는 것만으로 에너지가 크게 달라집니다(Energy Efficiency Guide for iOS Apps). 발열이 쌓이면 시스템이 ProcessInfo.thermalState를 .nominal에서 .fair→.serious→.critical로 올리고 CPU/GPU를 스로틀링합니다. 앱은 이 상태를 구독해 프레임 레이트나 백그라운드 작업량을 줄여야 합니다(thermalState 문서). 또 잦은 타이머 웨이크업은 SoC가 깊은 idle로 못 들어가게 막아 배터리를 갉아먹으므로, 웨이크업을 모아 처리(coalescing)하는 게 중요합니다.
실험 · 도구
개발 중에는 Xcode 디버그 내비게이터의 Energy Impact와 CPU 게이지로 대략을 보고, 필드 데이터는 MetricKit의 MXCPUMetric·소비 전력 지표로 수집합니다. 열 상태 변화는 thermalStateDidChangeNotification으로 관찰하고, macOS라면 powermetrics로 코어별 전력을 직접 볼 수 있습니다. 관찰 지표는 CPU%뿐 아니라 어느 코어(P/E)에서 얼마나 오래 도는지와 온도·스로틀 여부를 함께 봐야 합니다.
// 백그라운드 대량 작업은 낮은 QoS → 스케줄러가 효율(E) 코어에 배치 → 전력↓
Task(priority: .utility) { await reindexPhotos() }
// 열 상태에 따라 작업량을 조절
NotificationCenter.default.addObserver(
forName: ProcessInfo.thermalStateDidChangeNotification,
object: nil, queue: .main
) { _ in
switch ProcessInfo.processInfo.thermalState {
case .serious, .critical: renderer.targetFPS = 30 // 프레임·품질 낮추기
default: renderer.targetFPS = 60
}
}프로젝트 적용
- 작업 성격에 맞는 QoS 지정 — 백그라운드/비대화형은
.utility·.background로 E 코어 유도. - busy-wait·타이트 폴링 루프 금지(코어를 100%로 붙잡아 전력·발열 폭증). 폴링 대신 이벤트/푸시, 타이머는 coalescing.
thermalState와ProcessInfo.isLowPowerModeEnabled에 반응해 프레임 레이트·백그라운드 처리량을 낮춘다.
"CPU 사용률(%)만 낮추면 배터리가 좋아진다"는 지나친 단순화입니다. 에너지는 전력×시간이고, 어느 코어(P vs E)에서, 어떤 주파수·전압으로, 얼마나 자주 깨어 도는지가 함께 작용합니다 — E 코어에서 짧게 끝나는 100% 버스트가, SoC를 계속 얕게 깨워 두는 20% 잔업보다 나을 수 있습니다. "발열은 성능과 무관"도 틀립니다 — 지속 발열은 스로틀링을 부르고, 스로틀링은 곧 성능 저하입니다.
꼬리 질문
- 같은 계산을 P 코어에서 빨리 끝내는 것과 E 코어에서 천천히 하는 것, 배터리 관점에서 무엇이 유리한지 어떤 조건이 가르나?
thermalState가.serious로 올라가면 게임·영상 앱은 구체적으로 무엇을 줄여야 하나?- 1초마다 깨는 타이머와 60초마다 한 번 깨는 타이머의 배터리 차이가 사용률(%) 차이보다 클 수 있는 이유는?