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