블룸 필터 적용후에 언제까지 블룸 필터만 쓸 것인가: Ribbon Filter라는 글을 접하게 되었다.(뜨금)
리본 필터의 데이터 구조는 선형적인 메모리 접근을 가능하게 하여, CPU 캐시 미스를 줄이는데 도움이 될 있다는 내용이 포함되어있었는데...
CPU 캐시 계층 구조에 대해 자세히 알아보고, 테스트 코드로 실제 성능 차이를 측정해보자!

메모리 접근 패턴은 애플리케이션의 성능에 큰 영향을 미친다.
멀티코어 시스템에서 캐시 일관성을 유지하는 핵심 메커니즘이다. 프로세서 간의 캐시 동기화에 사용된다.
Modified (수정됨)
Exclusive (독점)
Shared (공유)
Invalid (무효)
상황: 프로세서가 자신의 캐시에 있는 데이터를 읽으려 할 때
Core1 캐시: Modified (값: 100) 일 때
Core1이 해당 값을 읽으려 하면
- 캐시에서 바로 100을 읽음
- 상태는 Modified 유지
- 버스 트래픽 발생하지 않음
상황: 프로세서가 캐시에 없는(Invalid) 데이터를 읽으려 할 때
1) 프로세서가 자신의 캐시가 Invalid 상태임을 확인
2) 버스에 읽기 요청 신호를 브로드캐스트
3) 두 가지 경우 발생:
a) 다른 캐시가 Modified 상태로 가지고 있는 경우:
- 해당 캐시가 데이터를 제공
- 메모리도 갱신
- 양쪽 모두 Shared 상태가 됨
b) 다른 캐시가 가지고 있지 않은 경우:
- 메모리에서 데이터 읽어옴
- 상태는 Exclusive가 됨
상황: 프로세서가 자신의 캐시에 있는 데이터를 수정하려 할 때
경우 1) Modified/Exclusive 상태인 경우:
- 바로 쓰기 가능
- Modified 상태로 변경/유지
- 버스 트래픽 없음
경우 2) Shared 상태인 경우:
1) 버스에 Invalid 신호 브로드캐스트
2) 다른 캐시들이 해당 라인을 Invalid로 변경
3) 자신의 캐시를 Modified로 변경
4) 데이터 수정
상황: 프로세서가 캐시에 없는(Invalid) 데이터를 수정하려 할 때
1) 버스에 Read-With-Intent-To-Modify 신호 브로드캐스트
2) 데이터 소스 확인:
a) 다른 캐시가 Modified 상태로 가지고 있는 경우:
- 해당 캐시가 데이터 제공
- 메모리도 갱신
b) 다른 캐시가 없는 경우:
- 메모리에서 데이터 읽어옴
3) 다른 모든 캐시의 해당 라인을 Invalid로 변경
4) 자신의 캐시를 Modified로 변경
5) 데이터 수정
이제 실제로 Java 코드를 통해 캐시 계층 구조가 성능에 미치는 영향을 측정해보자.
public class CacheLevelTest {
private static final int ARRAY_SIZE = 64 * 1024 * 1024; // 64MB 배열
private static final int[] STRIDE_SIZES = {1, 2, 4, 8, 16, 32, 64};
public static void main(String[] args) {
// 테스트할 배열 생성
int[] array = new int[ARRAY_SIZE];
// 배열 초기화
for (int i = 0; i < ARRAY_SIZE; i++) {
array[i] = i;
}
// 순차 접근 테스트
System.out.println("=== 순차 접근 테스트 ===");
testSequentialAccess(array);
// 랜덤 접근 테스트
System.out.println("\n=== 랜덤 접근 테스트 ===");
testRandomAccess(array);
// 스트라이드 접근 테스트
System.out.println("\n=== 스트라이드 접근 테스트 ===");
testStrideAccess(array);
}
private static void testSequentialAccess(int[] array) {
long startTime = System.nanoTime();
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
long endTime = System.nanoTime();
System.out.printf("순차 접근 시간: %.2f ms%n", (endTime - startTime) / 1_000_000.0);
}
private static void testRandomAccess(int[] array) {
long startTime = System.nanoTime();
int sum = 0;
for (int i = 0; i < array.length; i++) {
int randomIndex = (int) (Math.random() * array.length);
sum += array[randomIndex];
}
long endTime = System.nanoTime();
System.out.printf("랜덤 접근 시간: %.2f ms%n", (endTime - startTime) / 1_000_000.0);
}
private static void testStrideAccess(int[] array) {
for (int stride : STRIDE_SIZES) {
long startTime = System.nanoTime();
int iterations = array.length / stride; // 실제 접근 횟수 계산
int sum = 0;
for (int i = 0; i < array.length; i += stride) {
sum += array[i];
}
long endTime = System.nanoTime();
double timePerAccess = (endTime - startTime) / (double)iterations; // 접근당 평균 시간 계산
System.out.printf("스트라이드 %d 접근당 평균 시간: %.2f ns%n",
stride, timePerAccess);
}
}
=== 순차 접근 테스트 ===
순차 접근 시간: 24.89 ms
=== 랜덤 접근 테스트 ===
랜덤 접근 시간: 9753.09 ms
=== 스트라이드 접근 테스트 ===
스트라이드 1 접근당 평균 시간: 0.58 ns
스트라이드 2 접근당 평균 시간: 0.74 ns
스트라이드 4 접근당 평균 시간: 0.65 ns
스트라이드 8 접근당 평균 시간: 0.65 ns
스트라이드 16 접근당 평균 시간: 1.19 ns
스트라이드 32 접근당 평균 시간: 3.88 ns
스트라이드 64 접근당 평균 시간: 4.90 ns
실제 테스트를 실행해보면 다음과 같은 패턴을 관찰할 수 있다.
순차 접근 성능
랜덤 접근 성능
스트라이드 접근 성능
테스트 결과를 바탕으로, 다음과 같은 최적화 전략을 적용할 수 있다.
안녕하세요, 선생님. 블로그 글 잘 보고 있습니다.
혹시 선생님 멘토링 같은 건 안 하시나요?