퀵, 힙, 인트로 정렬의 캐시 친화성

댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
지난 글에서 캐시 히트와 캐시 미스에 대해 공부했는데, AI가 지나치게 하드웨어에 집중해서 작성했던 글이기 때문에 다소 어렵게 느껴졌다. 그래서 AI가 언급한 여러 하드웨어의 이름들은 차치하고서, 큰 흐름만 여기에 요약해본다.
퀵정렬, 힙정렬의 상세한 알고리즘은 이 글에서 다루지 않습니다.
퀵정렬의 경우 Hoare 방식의 탐색을 사용한다고 가정합니다.
캐시 용량이란 이름 그대로 캐시가 저장할 수 있는 용량이며, 캐시 라인은 캐시가 데이터를 로드하는 단위이다.
보통 캐시 라인은 64Byte크기이다.
여기서는 캐시가 단 하나의 캐시 라인만을 저장할 수 있다고 가정하여, 캐시 용량을 64Byte로 설정하겠다.
- 캐시 용량은 보통 kbyte단위이다
따라서 단 하나의 캐시 라인에 대해서 캐시 히트, 그 외의 데이터에 대해서는 캐시 미스가 발생한다.
위는 힙정렬과 퀵정렬의 캐시 친화성의 차이를 극단적으로 보여주기 위한 예시이고, 실제로는 배열의 길이가 매우 길 때 이와 비슷한 현상이 발생할 것이다.
길이가 32인 int배열의 크기는 32 * sizeof(int) = 128Byte로, 전체 데이터가 캐시에 보관될 수 없다
힙정렬의 경우 index == 1을 root 노드로 사용하여 데이터 32개를 담으려면 배열의 크기가 33이 되어야 하는데, 이 경우 캐시 라인 3개를 사용하게 되어 글의 취지에 어긋나므로 데이터를 31개만 사용하였다.
이 점에 유의하여 두 정렬 방식이 몇 번의 캐시 미스를 유발하는지 비교한다.
do { left++; } while(arr[left] < pivot); 을 수행한다. 즉, left가 arr[0]에서 시작하여 최대 arr[31]까지를 참조하는 과정에서 두 번의 미스가 발생할 것이다.
do { right--; } while(arr[right] > pivot); 을 수행한다. right가 arr[31]에서 시작하여 최대 arr[0]를 참조한다. 이 과정에서 right가 left를 추월하면 파티션 단계가 종료된다.
left와 right의 교차 지점이 어떤 캐시라인이냐와는 무관하게 최소 2회 미스가 발생한다.
두 포인터가 교차 이전에 멈추면 둘을 swap하는데, 둘이 다른 CL(Cache line)에 있으면 최대 4회의 미스가 추가로 발생한다.
이는 데이터의 구조에 따라 천차만별의 횟수가 된다. 구조적 약점만 보기 위해 0회로 간주한다.
이제 각 파티션 [0..k]와 [k+1..31]을 탐색한다.
따라서 최대 2회 캐시 미스가 발생한다.
다음은 파티션이 총 4개인 경우이다. 전체 데이터가 캐시 라인 2개로 구성되므로, 각 레벨 당 2회의 미스가 발생할 것이다.
따라서 2회 * 3레벨 = 6회 캐시 미스가 발생한다.
(파티션 32개인 경우는 없다. 배열의 크기가 32이므로 배열 크기가 1인데, left와 right의 교차 조건이 즉시 달성되므로 데이터 접근이 없기 때문이다)
위 과정에서 발생한 캐시 미스를 전부 합치면...
앞서 언급했듯이, 힙정렬은 데이터를 31개만 사용한다. 트리 구조는 아래와 같을 것이다.
이 경우 캐시 라인의 경계는 깊이 3과 깊이 4의 사이가 된다. 앞의 퀵 정렬과 동일하게 arr[15..16]가 경계이기 때문이다.
편의를 위해 arr[0..15]를 CL0, arr[16..31]을 CL1라고 부르자. CL은 Cache Line의 약자이다.
먼저, sift-down 방식으로 힙을 build한다. 주어진 배열을 힙 구조에 맞게 바꾸는 과정이다. Max, Min Heap 모두 동일하다.
그래서 총 몇 번의 캐시 미스가 발생하느냐...
지금까지 한 것은 힙을 구성하는 과정이다. 이제 힙을 만들었으니 하나씩 데이터를 빼보자.
데이터를 뺄 때마다, arr[1]과 arr[힙 크기]의 swap이 발생한다.
처음 16회의 추출은 아래와 같다.
서로 다른 캐시 라인의 요소 간 swap 한 번에 4회의 미스가 발생한다
- A읽기
- B읽기
- A쓰기
- B쓰기
구조적 약점만을 보기 위해 swap을 제외하면 3회의 미스가 발생한다.
이제 다음 16회의 추출을 보자.
그럼 총 16회 * 3번 + 1번 = 49회의 캐시 미스가 발생할 수 있다. 스왑을 제외한 경우이고, 퀵정렬에 비해 스왑이 더 빈번하게 발생하는 구조적 특징으로 인해 실제 캐시 미스는 훨씬 많을 것이다.
퀵정렬은 10회, 힙정렬은 79회가 발생했다. 이는 swap시 발생할 수 있는 캐시 미스 횟수를 제외한 수치이므로 실제와는 큰 차이가 있다.
그러나 구조적으로 swap을 유발하는 빈도는 힙정렬 쪽이 더 잦다.
좌우지간 퀵정렬보다 힙정렬이 더 많은 캐시 미스를 유발함은 변함이 없을 것이다.
다음은 위키백과에서 가져온 인트로정렬 의사코드이다.
procedure sort(A : array):
let maxdepth = ⌊log2(length(A))⌋ × 2
introsort(A, maxdepth)
procedure introsort(A, maxdepth):
n ← length(A)
if n < 16:
insertionsort(A)
else if maxdepth = 0:
heapsort(A)
else:
p ← partition(A) // assume this function does pivot selection, p is the final position of the pivot
introsort(A[1:p-1], maxdepth - 1)
introsort(A[p+1:n], maxdepth - 1)
잘 보면 insertionsort, heapsort, partition의 세 정렬 방법을 섞어 쓰는 것을 볼 수 있다. partition은 퀵정렬을 통해 도출한 피벗을 반환한다고 볼 수 있겠다.
삽입 정렬(insertionsort)이 섞여있는데, 삽입 정렬의 알고리즘 아이디어는 간단하다.
이 알고리즘의 단점은 데이터의 개수인데, 시간 복잡도가 최악 O(n^2) 로 크지만 n이 작으면 아래의 장점이 부각된다.
index == 0에 삽입 하여 뒤의 요소를 다 한 칸 씩 밀어야 할 때, 하나 씩 미는 게 아니라 여러 개를 동시에 밀어버리니 O(n)이 걸리던 것을 O(n/8)로 줄이는 느낌이다.인트로 정렬의 depth가 log2(len) * 2 이상이 되면 퀵정렬에서 힙정렬로 전환하는데, 이는 퀵정렬의 피벗 선택이 연속해서 잘못되는 상황을 피하기 위함이다.
아까 예시로 든 32개의 int 정렬에서 퀵정렬의 재귀 깊이가 5였는데, 이는 이상적인 경우 퀵정렬이 log2(n)만큼의 재귀를 하기 때문이다. 그러나 피벗 선택의 운이 나빠서 16:16 분할이 아니라 1:31 등의 극단적인 분할을 계속하게 되면, 재귀 깊이가 계속해서 증가하므로 최악의 경우 O(n^2)만큼의 시간이 걸린다.
재귀 깊이 log2(len) * 2의 기준은 그 불리한 상황을 피하기 위해 설정된 값으로, 다소 운이 나쁜 것은 감안하도록 *2가 붙었다. 참고로 median-of-3 로 피벗을 선택하므로 어느 정도 안전장치가 있긴 하다.
median-of-3: arr[0], arr[n - 1], arr[n/2] 중 중앙값을 피벗으로 선택
이 정렬 방식은 삽입 정렬, 퀵정렬, 힙정렬의 장점만을 취하도록 설계된 복합적인 정렬 - 하이브리드 정렬이다. 결론적으로 이 정렬 알고리즘은 최악의 경우에도 O(nlogn)의 성능을 유지할 수 있다.
퀵, 힙정렬은
O(nlogn)으로 알려져있다.
삽입 정렬은 O(n^2)로 알려져 있으나, n < 16에서 상수 계수가 퀵, 힙에 비해 작아서 O(nlogn)보다 좋은 성능이 나온다. 캐싱 효율은 덤
캐시 미스 횟수를 일일이 세는 일은 다시는 하지 않으련다
FNV-1a 알고리즘
A Star 알고리즘 개념 정리
![[Unity] CPU 내부 구조와 메모리 상호작용: 캐시 히트와 캐시 미스](https://data.develog.develrocket.com/upload/develog/user_1776858831954/1777192918989-zc1yzr/tumb2.png)
캐시 히트와 캐시 미스에 대하여 - Unity 최적화
![[Unity] CPU 내부 구조와 메모리 상호작용: 캐시 히트와 캐시 미스](https://data.develog.develrocket.com/upload/develog/user_1776858831954/1777192918989-zc1yzr/tumb2.png)