perf stat로 cycles, instructions, cache-misses 몇 개를 조합해 IPC와 미스율을 계산하는 건 코어 하나짜리 애플리케이션이라면 충분하다. 그런데 병목이 프론트엔드인지 백엔드인지까지 확인한 다음, 백엔드라면 그게 L1인지 L2인지 라스트레벨 캐시인지 TLB인지까지 계속 파고들려면 매번 다른 이벤트 조합을 손으로 골라야 하고, 미스율·MPKI 같은 값도 직접 계산해야 한다. ARM은 이 과정을 표준화하기 위해 Arm Telemetry Solution의 일부로 topdown-tool을 만들어 배포하고 있다. 이 글에서는 topdown-tool 설치, ARM이 제시하는 2단계 top-down 분석 방법론, 그리고 실제 예제 애플리케이션을 이 방법론으로 진단해 최적화한 공식 사례를 정리한다.
topdown-tool이란
topdown-tool은 Arm Telemetry Solution 프로젝트에 포함된 명령줄 도구로, 내부적으로는 perf를 호출하지만 raw 이벤트 코드 대신 General, Cycle_Accounting, L1D_Cache_Effectiveness 같은 이름이 붙은 지표 그룹을 지정해서 실행한다. 각 그룹에 필요한 이벤트 조합과 미스율·MPKI 계산식은 도구 안에 코어별 텔레메트리 스펙으로 미리 정의되어 있어서, 사용자는 “무엇을 볼지”만 고르면 된다. Neoverse N1/V1/V2/N2/N3 같은 서버급 코어는 물론, Cortex-A76처럼 보급형 보드에 들어가는 코어에서도 쓸 수 있다.
설치
AArch64 리눅스, Python 3.7 이상, Linux perf가 필요하다. Debian/Ubuntu 기준 설치 과정은 다음과 같다.
sudo apt update
sudo apt install python-is-python3 python3-pip python3-venv \
python3-packaging linux-tools-generic linux-tools-$(uname -r) -y
git clone https://git.gitlab.arm.com/telemetry-solution/telemetry-solution.git
python -m venv topdown-venv
source topdown-venv/bin/activate
cd telemetry-solution/tools/topdown_tool
pip install -e .
설치가 끝나면 도움말과 함께 지금 시스템에서 인식되는 CPU 목록을 확인할 수 있다.
topdown-tool --help
topdown-tool --list-cpus
# 설치 확인용 간단 실행
topdown-tool -m Cycle_Accounting -a sleep 5
CPU 자동 인식이 안 되거나 perf.data를 다른 머신에서 가져와 분석하는 경우에는 --cpu neoverse-n1처럼 --list-cpus 출력에 나온 이름을 직접 지정해야 한다.
2단계 top-down 분석 방법론
ARM이 제시하는 방법론은 두 단계로 나뉜다.
- 1단계: IPC, frontend 정체율, backend 정체율을 먼저 수집해 병목이 명령어 공급 쪽인지 실행/메모리 쪽인지 큰 범주를 확인한다.
- 2단계: 1단계에서 확인한 방향에 따라 세부 지표로 좁힌다. frontend가 지배적이면 명령어 TLB, L1 명령어 캐시, 분기 예측 지표를 보고, backend가 지배적이면 데이터 TLB, 메모리 시스템, 데이터 캐시, 명령어 믹스를 본다.
MPKI(1,000개 명령어당 캐시 미스 수)나 미스 비율처럼 매번 손으로 계산해야 했던 값도 그룹별 출력에 이미 계산되어 나온다. 지표 그룹과 실행 명령은 다음과 같다.
| 단계 | 목적 | 명령어 |
|---|---|---|
| 1단계 | 전반적 지표(IPC 등) | topdown-tool --cpu neoverse-n1 -m General ./app |
| 1단계 | 사이클 회계(frontend/backend 정체율) | topdown-tool --cpu neoverse-n1 -m Cycle_Accounting ./app |
| 2단계(backend) | L1 데이터 캐시 | topdown-tool --cpu neoverse-n1 -m L1D_Cache_Effectiveness ./app |
| 2단계(backend) | L2 캐시 | topdown-tool --cpu neoverse-n1 -m L2_Cache_Effectiveness ./app |
| 2단계(backend) | 라스트레벨 캐시 | topdown-tool --cpu neoverse-n1 -m LL_Cache_Effectiveness ./app |
| 2단계(backend) | 데이터 TLB | topdown-tool --cpu neoverse-n1 -m DTLB_Effectiveness ./app |
| 2단계(backend) | 명령어 믹스 | topdown-tool --cpu neoverse-n1 -m Operation_Mix ./app |
| 2단계(frontend) | 명령어 TLB | topdown-tool --cpu neoverse-n1 -m ITLB_Effectiveness ./app |
| 2단계(frontend) | 분기 예측 | topdown-tool --cpu neoverse-n1 -m Branch_Effectiveness ./app |
실전 사례 — stride 예제로 보는 소프트웨어 프리페칭 최적화
ARM 공식 튜토리얼은 stride라는 예제 애플리케이션으로 이 방법론을 그대로 시연한다. 1단계에서 Cycle_Accounting을 돌려보면 backend 정체가 압도적으로 높게 나오고, 2단계에서 데이터 TLB와 메모리 시스템 지표를 확인하면 메모리 접근 지연이 원인이라는 게 드러난다. 여기서 택한 해법은 컴파일 시점에 소프트웨어 프리페치 힌트를 넣는 것이다.
# 최적화 전
g++ -g -O3 stride.cpp -o stride
# 최적화 후 — 소프트웨어 프리페칭 활성화
g++ -g -O3 -DENABLE_PREFETCH -DDIST=100 stride.cpp -o stride
DIST는 몇 번째 다음 접근을 미리 당겨올지 정하는 프리페치 거리다. 이 예제에서는 DIST를 늘려가며 측정했을 때 40 이상부터 개선 효과가 포화됐고, 최종적으로 100을 썼다. 최적화 전후 지표는 다음과 같다.
| 지표 | 최적화 전 | 최적화 후 |
|---|---|---|
| 명령어 수 | 100억 | 140억 |
| 사이클 | 450억 | 220억 |
| IPC | 0.22 | 0.63 |
| 실행 시간 | 20.1초 | 9.9초 |
프리페치 명령 자체가 추가되면서 실행된 명령어 수는 오히려 늘었지만(100억 → 140억), 메모리 응답을 기다리며 멈춰 있던 사이클이 크게 줄어든 덕분에 IPC는 약 3배(0.22 → 0.63), 실행 시간은 절반에 가깝게(20.1초 → 9.9초) 줄었다. “backend 정체 확인 → 데이터 TLB/메모리 시스템 지표로 원인 좁히기 → 프리페칭 적용”이라는 흐름이 앞서 설명한 2단계 방법론을 그대로 따른 결과다. 다만 이 개선 폭은 메모리 대역폭이 넉넉한 서버에서는 더 작게 나올 수 있다고 ARM도 명시하고 있다.
주의사항
- 지표 그룹과
--cpu값은 코어별 텔레메트리 스펙에 맞춰야 하므로, 다른 코어에서 실행할 때는topdown-tool --list-cpus로 정확한 이름을 먼저 확인해야 한다. - 가상환경(venv) 안에 설치하는 구조라, 새 셸을 열 때마다
source topdown-venv/bin/activate를 다시 실행해야topdown-tool명령을 찾는다. - 내부적으로 perf를 쓰기 때문에 비root 사용자로 실행할 때는
perf_event_paranoid값에 따라 권한 오류가 날 수 있다.
마무리
topdown-tool은 raw perf 이벤트를 직접 조합하고 미스율을 손으로 계산하던 과정을 지표 그룹 선택으로 대체해준다. 1단계로 큰 범주를 잡고 2단계로 원인을 좁힌 뒤 최적화를 적용하는 흐름 자체는 stride 예제처럼 실제로 IPC를 3배, 실행 시간을 절반으로 줄이는 결과로 이어질 만큼 실용적이다. ARM PMU 이벤트를 직접 다루는 기초 내용은 다른 글에서 다뤘으니, PMU 개념 자체가 낯설다면 그쪽을 먼저 보는 게 순서에 맞다.