ARM Performance Counter

코드가 느린 게 캐시 미스 때문인지, 분기 예측 실패 때문인지, 아니면 그냥 명령어 수가 많아서인지는 로그나 타이머만으로는 알기 어렵다. wall clock 시간은 “얼마나 걸렸는지”만 알려줄 뿐, “왜 걸렸는지”는 알려주지 않는다. ARM 프로세서는 이 질문에 답하기 위해 PMU(Performance Monitoring Unit)라는 하드웨어 블록을 코어마다 두고, CPU 사이클·명령어 실행 수·캐시 미스·분기 예측 실패 같은 이벤트를 실행 중인 코드에 개입하지 않고 그대로 카운팅한다. 이 글에서는 ARM PMU의 기본 구조, 리눅스 perf를 통한 사용법, 그리고 AArch64에서 사용자 공간이 PMU 레지스터에 직접 접근할 때의 제약을 정리한다.

ARM Performance Counter(PMU)란

PMU는 CPU 코어 내부에 있는 하드웨어 카운터 집합으로, 명령어 실행·메모리 접근·캐시 미스 같은 이벤트를 소프트웨어 개입 없이 실시간으로 센다. 구성 요소는 크게 네 가지다.

  • Event Counter: 지정한 하드웨어 이벤트 하나를 카운팅하는 범용 레지스터.
  • Cycle Counter: CPU 클럭 사이클을 전용으로 세는 레지스터(AArch64에서는 PMCCNTR_EL0).
  • Event Select Register: 각 Event Counter가 어떤 이벤트를 셀지 지정한다.
  • Overflow Interrupt: 카운터가 오버플로우되면 인터럽트를 발생시켜, 샘플링 기반 프로파일링(perf record)에서 이 인터럽트를 트리거로 스택을 수집한다.

실제로 자주 쓰는 이벤트는 명령어 실행 수(instructions retired), L1/L2 캐시 미스, 분기 예측 실패(branch misprediction), CPU 사이클 정도다. 어떤 이벤트를 지원하는지는 코어 마이크로아키텍처마다 다르므로, 뒤에서 볼 perf list로 실제 지원 목록을 확인하는 게 정확하다.

perf로 PMU 데이터 수집하기

리눅스 커널은 perf_event 서브시스템을 통해 PMU를 표준화된 방식으로 노출하므로, 대부분의 경우 레지스터를 직접 건드릴 필요 없이 perf 명령줄 도구만으로 충분하다. Ubuntu/Debian 계열에서는 커널 버전에 맞는 패키지를 설치한다.

sudo apt update
sudo apt install linux-tools-$(uname -r) linux-tools-generic

설치한 뒤에는 이런 명령으로 이벤트를 측정한다.

# 지원 이벤트 목록 확인 (코어마다 다를 수 있음)
perf list

# CPU 사이클만 측정
perf stat -e cycles ./your_program

# 여러 이벤트를 한 번에 측정
perf stat -e cycles,instructions,cache-misses ./your_program

# 샘플링 기반 프로파일링
perf record -g ./your_program
perf report

perf stat의 출력은 대략 다음과 같은 형태로 나온다.

 Performance counter stats for './your_program':

         1,234,567      cycles
         2,345,678      instructions              #    1.90  insn per cycle
           123,456      cache-misses

       0.003456789 seconds time elapsed

insn per cycle(IPC)이 낮다면 파이프라인이 자주 멈춘다는 뜻이고, cache-misses 비중이 크다면 메모리 접근 패턴부터 의심해볼 수 있다.

perf_event_paranoid와 권한 설정

비root 사용자로 perf stat을 실행하면 perf_event_paranoid 값에 따라 권한 오류가 날 수 있다. 현재 값은 다음으로 확인한다.

cat /proc/sys/kernel/perf_event_paranoid

값이 클수록 제한이 엄격하다. 하드웨어 카운터까지 비root 사용자에게 허용하려면 1 이하로 낮추면 된다.

sudo sysctl kernel.perf_event_paranoid=1

사용자 공간에서 PMU 레지스터 직접 접근하기 — AArch64의 제약

32비트 ARMv7에서는 MRC p15, 0, %0, c9, c12, 0 같은 코프로세서 명령으로 사용자 공간에서도 비교적 쉽게 PMU 레지스터에 접근할 수 있었다. AArch64(ARMv8-A 64비트)에서는 사정이 다르다. PMU 레지스터가 PMCR_EL0, PMCCNTR_EL0 같은 시스템 레지스터로 옮겨갔고, 기본적으로 EL0(사용자 공간)에는 접근 권한이 없다. 커널이 PMUSERENR_EL0 레지스터로 EL0 접근을 명시적으로 열어줘야만 mrs/msr 명령으로 직접 읽고 쓸 수 있다.

이 권한을 여는 방법은 두 가지다. 하나는 커널 공식 인터페이스인 kernel/perf_user_access sysctl을 켜고 perf_event_open()mmap()을 조합해 접근하는 방식인데, 시퀀스 락까지 다뤄야 해서 코드가 꽤 복잡하다. 다른 하나는 특권 모드에서 PMUSERENR_EL0를 직접 열어주는 작은 커널 모듈을 만들어 로드하는 방식으로, 개념 검증이나 벤치마킹 목적이라면 이쪽이 훨씬 단순하다. 커널 모듈에서는 대략 다음과 같은 로직으로 EL0 접근을 열고 카운터를 켠다.

/* 커널 모듈 안, 특권 모드(EL1)에서 실행되는 핵심 로직 */

/* EL0에 PMU 레지스터 접근 권한 부여 */
asm volatile("msr pmuserenr_el0, %0"
             :: "r"((u64)(ARMV8_PMUSERENR_EN_EL0 |
                          ARMV8_PMUSERENR_ER |
                          ARMV8_PMUSERENR_CR)));

/* 사이클 카운터 활성화 */
asm volatile("msr pmcntenset_el0, %0"
             :: "r"(ARMV8_PMCNTENSET_EL0_ENABLE));

/* PMU 전체 시작 (PMCR_EL0의 E 비트 설정) */
u64 val;
asm volatile("mrs %0, pmcr_el0" : "=r"(val));
asm volatile("msr pmcr_el0, %0" :: "r"(val | ARMV8_PMCR_E));

이렇게 EL0 접근이 열린 뒤에는, 사용자 공간 프로그램에서 특권 없이 바로 사이클 카운터를 읽을 수 있다.

#include <stdint.h>
#include <stdio.h>

static inline uint64_t read_cycle_counter(void) {
    uint64_t val;
    asm volatile("mrs %0, pmccntr_el0" : "=r"(val));
    return val;
}

int main(void) {
    uint64_t start = read_cycle_counter();

    for (volatile int i = 0; i < 1000000; i++);

    uint64_t end = read_cycle_counter();
    printf("CPU cycles: %lu\n", end - start);
    return 0;
}

커널 모듈 전체 코드를 직접 작성하기보다는, 검증된 오픈소스 구현을 가져다 쓰는 편이 안전하다. jerinjacobk/armv8_pmu_cycle_counter_el0가 이 패턴을 /dev/pmuctl 디바이스로 감싸 제공하는 참조 구현이다.

활용 사례

  • 애플리케이션 최적화: CPU 사이클, 명령어 수, 캐시 미스를 비교해 실제 병목 지점을 좁힌다.
  • 전력 소모 최적화: 캐시 미스와 분기 예측 실패를 줄이면 불필요한 메모리 접근과 파이프라인 플러시가 줄어 전력 소모도 함께 줄어든다.
  • DPDK 같은 저지연 애플리케이션의 자체 타이머: 시스템 콜 오버헤드 없이 사이클 단위로 시간을 재야 하는 데이터플레인 소프트웨어에서 앞서 본 EL0 직접 접근 방식이 실제로 쓰인다.

주의사항

  • perf_event_paranoid를 낮추면 비root 사용자도 시스템 전반의 성능 카운터 정보를 볼 수 있게 되므로, 공유 서버보다는 개발·벤치마킹 환경에서만 낮추는 게 안전하다.
  • PMUSERENR_EL0을 여는 커널 모듈은 out-of-tree 모듈이라 로드 시 커널을 taint 상태로 만들고, perf 같은 표준 도구가 동시에 같은 카운터를 쓰면 값이 충돌할 수 있다. 프로덕션보다는 통제된 테스트 환경에서 쓰는 게 맞다.
  • big.LITTLE 같은 이기종 코어 구성에서는 코어 클러스터마다 지원하는 PMU 이벤트가 다를 수 있으므로, 측정 전에 perf list로 실제 지원 이벤트를 먼저 확인해야 한다.

마무리

대부분의 성능 분석은 perf 하나로 충분하고, 레지스터를 직접 만져야 할 이유는 많지 않다. 다만 DPDK류의 저지연 애플리케이션처럼 시스템 콜 오버헤드조차 아까운 경우라면, AArch64에서 EL0 직접 접근이 ARMv7의 코프로세서 접근과 완전히 다른 메커니즘이라는 점, 그리고 커널 모듈로 권한을 열어야 한다는 제약을 알고 접근하는 게 중요하다.

참고

답글 남기기