Valgrind 사용 가이드 및 캐시 성능 분석

Valgrind은 포괄적인 코드 진단 도구로, Ubuntu 시스템에서 간단히 설치할 수 있습니다.

sudo apt-get install valgrind

공식 웹사이트에서 Manuel.pdf 문서를 다운로드할 수 있습니다.

메모리 누수를 진단할 수 있습니다

g++ 소스파일.cpp
valgrind --tool=memcheck ./실행파일

이 명령어는 메모리 누수 지점을 보고합니다.

캐시 적중률을 진단할 수도 있습니다

g++ 소스파일.cpp
valgrind --tool=cachegrind ./실행파일

이 명령어는 1차 캐시 데이터 적중률, 명령어 적중률, 최종 캐시 적중률 등의 정보를 보고합니다.

다음은 예제 코드입니다

#include<iostream>
#include<chrono>

constexpr size_t SIZE = 1E3;

int main(){
    double sum=0, temp=0;
    auto start = std::chrono::high_resolution_clock::now();
    
    double *matrixA = new double [SIZE*SIZE];
    for(size_t i=0;i<SIZE*SIZE;i++) matrixA[i] = i;
    
    double *matrixB = new double [SIZE*SIZE];
    for(size_t i=0;i<SIZE*SIZE;i++) matrixB[i] = i;
    
    double *matrixC = new double [SIZE*SIZE];
    
    for(size_t i=0;i<SIZE;i++)
    for(size_t j=0;j<SIZE;j++){
        temp = 0;
        for(size_t row=0;row<SIZE;row++)
            temp += matrixB[row*SIZE+j];
        sum = 0;
        for(size_t col=0;col<SIZE;col++){
            sum += matrixA[col*SIZE+i] * temp;
        }
        matrixC[i*SIZE+j] = sum;
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> diff = end - start;
    std::cout << diff.count() << " s" << std::endl;
    
    delete [] matrixA; 
    delete [] matrixB; 
    delete [] matrixC;
    return 0;
}

이 코드의 내부 루프에서, row 변수가 행 인덱스를 사용하여 메모리 로컬리티가 좋지 않습니다. Valgrind의 검 보고서에서 이는 1차 캐시 데이터 미스 비율이 높게 나타납니다 (D1 miss rate: 11.8%).

==2322== Cachegrind, a cache and branch-prediction profiler
==2322== Copyright (C) 2002-2017, and GNU GPL'd, by Nicholas Nethercote et al.
==2322== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2322== Command: ./a.out
==2322== 
--2322-- warning: L3 cache found, using its data for the LL simulation.
276.428 s
==2322== 
==2322== I   refs:      31,053,190,692
==2322== I1  misses:             1,976
==2322== LLi misses:             1,928
==2322== I1  miss rate:           0.00%
==2322== LLi miss rate:           0.00%
==2322== 
==2322== D   refs:      17,025,701,244  (15,018,537,430 rd   + 2,007,163,814 wr)
==2322== D1  misses:     2,001,266,444  ( 2,000,014,098 rd   +     1,252,346 wr)
==2322== LLd misses:       125,490,381  (   125,113,840 rd   +       376,541 wr)
==2322== D1  miss rate:           11.8% (          13.3%     +           0.1%  )
==2322== LLd miss rate:            0.7% (           0.8%     +           0.0%  )
==2322== 
==2322== LL refs:        2,001,268,420  ( 2,000,016,074 rd   +     1,252,346 wr)
==2322== LL misses:        125,492,309  (   125,115,768 rd   +       376,541 wr)
==2322== LL miss rate:             0.3% (           0.3%     +           0.0%  )

반면, 다음 코드의 내부 루프에서 col 변수가 열 인덱스를 사용하여 메모리 로컬리티가 더 좋습니다.

#include<iostream>
#include<chrono>

constexpr size_t SIZE = 1E3;

int main(){
    double sum=0, temp=0;
    auto start = std::chrono::high_resolution_clock::now();
    
    double *matrixA = new double [SIZE*SIZE];
    for(size_t i=0;i<SIZE*SIZE;i++) matrixA[i] = i;
    
    double *matrixB = new double [SIZE*SIZE];
    for(size_t i=0;i<SIZE*SIZE;i++) matrixB[i] = i;
    
    double *matrixC = new double [SIZE*SIZE];
    
    for(size_t i=0;i<SIZE;i++)
    for(size_t j=0;j<SIZE;j++){
        temp = 0;
        for(size_t col=0;col<SIZE;col++)
            temp += matrixB[j*SIZE+col];
        sum = 0;
        for(size_t row=0;row<SIZE;row++){
            sum += matrixA[i*SIZE+row] * temp;
        }
        matrixC[i*SIZE+j] = sum;
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> diff = end - start;
    std::cout << diff.count() << " s" << std::endl;
    
    delete [] matrixA; 
    delete [] matrixB; 
    delete [] matrixC;
    return 0;
}

이는 cachegrind 보고서에서 1차 캐시 데이터 적중률이 더 높게 나타납니다 (D1 miss rate: 0.7%).

==2334== Cachegrind, a cache and branch-prediction profiler
==2334== Copyright (C) 2002-2017, and GNU GPL'd, by Nicholas Nethercote et al.
==2334== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2334== Command: ./a.out
==2334== 
--2334-- warning: L3 cache found, using its data for the LL simulation.
202.343 s
==2334== 
==2334== I   refs:      31,053,190,658
==2334== I1  misses:             1,974
==2334== LLi misses:             1,926
==2334== I1  miss rate:           0.00%
==2334== LLi miss rate:           0.00%
==2334== 
==2334== D   refs:      17,025,701,233  (15,018,537,423 rd   + 2,007,163,810 wr)
==2334== D1  misses:       125,517,445  (   125,140,099 rd   +       377,346 wr)
==2334== LLd misses:       125,510,970  (   125,134,429 rd   +       376,541 wr)
==2334== D1  miss rate:            0.7% (           0.8%     +           0.0%  )
==2334== LLd miss rate:            0.7% (           0.8%     +           0.0%  )
==2334== 
==2334== LL refs:          125,519,419  (   125,142,073 rd   +       377,346 wr)
==2334== LL misses:        125,512,896  (   125,136,355 rd   +       376,541 wr)
==2334== LL miss rate:             0.3% (           0.3%     +           0.0%  )

Valgrind을 사용했을 때, 두 코드의 실행 시간은 각각 276.428s와 202.342s입니다. Valgrind 없이 실행했을 때의 실행 시간은 각각 24.0162s와 7.99471s입니다. 보고서에서 D1 miss rate의 10% 차이(그 외에는 거의 차이 없으며, LL refs는 10배 차이가 나지만 LL misses는 거의 비슷)가 몇 배의 효율 차이를 발생시킵니다. 이는 CPU 계산 작업이 10%의 D1 miss로 인한 추가 작업보다 훨씬 적다는 것을 의미합니다.

CPU 캐시에 대해 설명하면, CPU가 데이터를 필요로 할 때 1차 캐시 → 2차 캐시 → ... → 최종 캐시 → 메모리 순서로 데이터를 찾습니다. 1차 캐시에서 데이터를 찾으면 검색을 중단하고 데이터를 사용합니다. 찾는 깊이가 깊어질수록 시간 비용이 증가합니다. 마지막에 메모리에서 데이터를 찾아야 하는 경우(캐시 미스), 한 번에 "데이터 블록"을 가져와 캐시에 저장합니다. 다음에 동일한 데이터 블록에 있는 데이터를 찾을 경우 시간 비용을 절약할 수 있습니다. 따라서 캐시 적중률이 높을수록 프로그램 성능이 향상되므로 메모리 로컬리티 매우 중요합니다.

태그: valgrind memcheck cachegrind memory-leak cache-performance

5월 22일 14:33에 게시됨