배경
이전 포스팅에서 10개 스레드로 NumberFormatter 성능을 테스트했지만, 최적의 스레드 수에 대한 의문이 남았습니다.
이번에는 1개부터 8개까지 체계적으로 측정하여 진짜 최적점을 찾아보았습니다. 또한 Clock Time 뿐만 아니라 Memory Peak 등에 대해서도 고려했습니다.
테스트 환경
- 하드웨어: MacBook Pro M1 (4P + 4E 코어)
- 측정: XCTest measure() 100,000번 반복
- 메트릭: Clock Time, CPU Time, Memory Peak
- 조건: 모든 테스트에서 동일한 NumberFormatter 설정
성능 테스트 결과
방식 | 스레드 | Clock Time (초) | CPU Time (초) | CPU 효율 | Memory Peak (KB) | 성능 향상 |
매번 생성 | 1개 | 4.121 | 4.098 | 99.4% | 23,642 | 1.00x (기준) |
2개 | 2.391 | 4.599 | 192.3% | 22,040 | 1.72x | |
4개 | 1.587 | 5.812 | 366.2% | 20,467 | 2.60x | |
6개 | 2.515 | 12.569 | 499.8% | 20,044 | 1.64x | |
8개 | 2.554 | 16.866 | 660.3% | 19,586 | 1.61x | |
공유(os_unfair_lock) | 1개 | 0.767 | 0.768 | 100.1% | 15,090 | 5.37x |
2개 | 1.177 | 1.497 | 127.2% | 15,024 | 3.50x | |
4개 | 1.143 | 1.370 | 119.9% | 15,024 | 3.60x | |
6개 | 1.197 | 1.491 | 124.6% | 14,782 | 3.44x | |
8개 | 1.150 | 1.386 | 120.5% | 14,808 | 3.58x |
위 결과를 통해 새롭게 알 수 있는 사실들을 발견하였습니다.
실행 시간 결과
1. 매번 생성 방식: 4개 스레드가 최적
스레드 개수 | 실행시간 | CPU 시간 | CPU 효율 | 성능 향상 |
1개 | 4.121초 | 4.098초 | 99.4% | 1.00x |
2개 | 2.391초 | 4.599초 | 192.3% | 1.72x |
4개 | 1.587초 | 5.812초 | 366.2% | 2.60x |
6개 | 2.515초 | 12.569초 | 499.8% | 1.64x |
8개 | 2.554초 | 16.866초 | 660.3% | 1.61x |
핵심 발견: 4개에서 최고 성능, 6개부터 급격한 저하
2. os_unfair_lock 공유 방식
스레드 개수 | 실행시간 | CPU 시간 | 성능 향상 |
1개 | 0.767초 | 0.768초 | 5.37x |
2개 | 1.177초 | 1.497초 | 3.50x |
4개 | 1.143초 | 1.370초 | 3.60x |
6개 | 1.197초 | 1.491초 | 3.44x |
8개 | 1.150초 | 1.386초 | 3.58x |
의외의 결과
매번 생성하는 방식을 기준으로, 저는 스레드 개수가 많으면 많을 수록 효율적이라고 생각하였으나, 4개일 때 상대적으로 빠른 성능을 보였습니다.
왜 4개 스레드가 최적인가?
Apple Silicon 아키텍처의 특성
Performance 코어: 4개 (고성능, CPU 집약적 작업 최적)
Efficiency 코어: 4개 (저전력, P-코어 대비 25-30% 성능)
Apple Silicon의 경우 프로, 맥스, 울트라를 제외한 기본 모델에서는 성능 코어(Performance Core)의 개수가 4개이고, 효율 코어(Efficiency Core)는 모델마다 다른데, 성능 코어가 CPU 집약적인 작업에 최적화되어 있기 때문입니다.
NumberFormatter 생성의 특성
- CPU 집약적: 로케일 데이터 로딩, 포맷 규칙 파싱 등
- 메모리 집약적: 내부 캐시 및 데이터 구조 생성
- 짧은 버스트: 생성 후 바로 사용하는 패턴
이런 특성 때문에 Performance 코어에서 실행될 때 최고 성능을 발휘한다고 생각합니다.
스레드 수별 코어 할당 패턴
4개 스레드: P-코어 4개 모두 사용 → 최적 성능
6개 스레드: P-코어 4개 + E-코어 2개 → 성능 불균형
8개 스레드: P-코어 4개 + E-코어 4개 → 심각한 불균형
병렬 작업에서 가장 느린 스레드가 전체 완료 시간을 결정하기 때문에, E-코어의 낮은 성능(25-30%)이 전체 성능의 발목을 잡게 됩니다.
실제 측정 결과로 증명:
4개 스레드: 1.587초 (최고 성능)
6개 스레드: 2.515초 (58% 성능 저하)
8개 스레드: 2.554초 (60% 성능 저하)
따라서 NumberFormatter와 같은 CPU 집약적 작업에서는 Performance 코어 수(4개)에 맞춰 스레드 수를 제한하는 것이 최적의 전략입니다.
CPU 효율성 분석
CPU 효율성은 다음과 같은 공식을 가집니다. (https://www.perfmatrix.com/what-is-cpu-time/)
CPU 효율 = (CPU Time / Clock Time) × 100%
매번 생성 방식
1개 스레드: 99.4% (단일 코어 완전 활용)
4개 스레드: 366.2% (4개 코어 동시 활용)
8개 스레드: 660.3% (8개 코어 동시 활용)
- 높은 CPU 효율 = 멀티코어를 효과적으로 활용
- 각 스레드가 독립적으로 NumberFormatter 생성 작업 수행
- 락 경합 없음 → CPU 코어들이 동시에 작업 가능
- 메모리 할당/해제 작업으로 인한 높은 CPU Time
os_unfair_lock 공유 방식
모든 스레드에서 100-127% 유지
- 상대적으로 낮은 CPU 효율 = 락 대기로 인한 CPU 활용도 제한
- 한 번에 하나의 스레드만 작업, 나머지는 대기
- 직렬화된 작업으로 멀티코어 활용 제한
- 메모리 할당 오버헤드 없음 → 낮은 CPU Time
핵심
높은 CPU 효율 ≠ 빠른 성능
- 매번 생성: CPU 효율 660%이지만 실행 시간 2.554초
- SpinLock: CPU 효율 120%이지만 실행 시간 1.150초 (2.2배 빠름)
원인:
- 매번 생성: 메모리 할당 오버헤드가 CPU Time 증가시켜 효율 상승
- SpinLock: 메모리 재사용으로 순수 작업 시간만 측정되어 효율 제한
이는 CPU 효율성과 전체 성능이 서로 다른 지표임을 보여주는 중요한 사례입니다.
메모리 사용량 분석
- 매번 생성: 각 작업마다 새로운 NumberFormatter 객체 생성
- 공유 방식: 하나의 NumberFormatter를 os_unfair_lock으로 보호하여 재사용
방식 | 1개 | 2개 | 4개 | 6개 | 8개 |
매번 생성 | 23.6MB | 22.0MB | 20.5MB | 20.0MB | 19.6MB |
공유 방식 | 15.1MB | 15.0MB | 15.0MB | 14.8MB | 14.8MB |
메모리 패턴 분석
공유 방식 (14.8~15.1MB)
// 단일 객체 재사용
private let sharedFormatter = NumberFormatter()
- NumberFormatter 객체 1개만 메모리 점유
- 스레드 수와 무관하게 일정한 메모리 사용량
- 최대 36% 메모리 절약 (23.6MB → 15.1MB)
- 락 오버헤드는 CPU에만 영향, 메모리는 변화 없음
매번 생성 (19.6~23.6MB)
// 매 호출마다 새 객체 생성
let formatter = NumberFormatter()
- 각 호출마다 새로운 NumberFormatter 객체 생성
- 동시에 존재하는 객체 수에 따라 메모리 사용량 결정
- 스레드 수와 실행 패턴에 따라 메모리 사용량 변동
이를 통해 NumberFormatter처럼 생성 비용이 높은 객체는 공유 방식이 메모리 효율성과 예측 가능성 모든 면에서 우수하다는 것을 알 수 있었습니다.
결론
1. 하드웨어 아키텍처의 중요성
- Apple Silicon M1에서 4개 스레드가 최적점
- Performance 코어 수에 맞춘 스레드 제한이 핵심 전략
2. os_unfair_lock 공유 방식의 우위
- 5.37배 빠른 성능 (0.767초 vs 4.121초)
- 36% 메모리 절약 (15MB vs 24MB)
- 스레드 수와 무관한 안정적 성능
3. CPU 효율성과 성능의 역설
- 높은 CPU 효율(660%) ≠ 빠른 성능
- 메모리 할당 오버헤드가 CPU 효율을 높이지만 전체 성능은 저하
- 실제 성능 측정이 CPU 효율보다 중요
마무리
이번 테스트를 통해 단순히 스레드를 늘리는 것보다 하드웨어 특성을 이해하고 적절한 동기화 전략을 선택하는 것이 훨씬 중요함을 확인했습니다.
특히 CPU 효율성보다는 실제 완료 시간과 메모리 사용량을 종합적으로 고려해야 한다는 교훈을 얻었고, NumberFormatter처럼 생성 비용이 높은 객체는 공유 방식에선 상황에 맞는 락 기법 등을 사용해야 함을 깨달았습니다.
'iOS > NumberterKit' 카테고리의 다른 글
[NumberterKit] 성능 테스트 - 락 기법별 상세 분석 (0) | 2025.07.03 |
---|---|
[NumberterKit] 성능 테스트 결과 (0) | 2025.05.16 |
[NumberterKit] Formatter 최적화: SpinLock 적용기 (0) | 2025.05.09 |
[NumberterKit] NumberterKit을 만들게 된 계기와 컨셉 (0) | 2025.04.20 |