아마도 소프트웨어 혹은 컴과 전공생이라면 아직 찌들지 않은 푸릇한 1~2학년 시절 자료구조나 이산수학을 배우면서 행렬(배열)의 row-major, col-major를 눈 깜짝할 새 배우고 지나갔을 것이다.
그것이 어언 몇 년이 지나 지금이 되면.. 그게 뭔지는 대충 아는데 왜 배웠는지 기억이 나는 새럼 손 드세요. 손 드신 분들은 그대로 가슴에 손을 얹고 양심에게 여쭈어보시면 됩니다. 정녕 잘 기억이 나는가!
암튼 이번 포스트에서는 행렬(배열)의 row-major, col-major의 차이점과 중요성에 대해서 알아보도록 하자.
row-major, column-major가 뭔데?
얘네들은 2차원 이상의 배열을 저장할 때 적용이 되는 것들이다.
어떤 배열을 2차원으로 한정하여 생각해 보자.
아무리 배열이 2차원 3차원 4차원 n차원이라 하더라도, 저장 장치에 저장될 때에는 1차원으로 저장된다.
고로, 2차원 이상의 것들을 한 줄로 쫙 펴야 한다는 소리이다.
이때, 내 바로 옆에 어떤 놈이 저장될 것인가? 를 결정하는 것이 row-major, col-major 이다.
앞으로 이어질 예제에서 다음과 같은 2차원 배열(행렬)을 저장 장치에 저장한다고 해 보자.
$$
A = \begin{bmatrix}
a_{11} &a_{12} &a_{13} \
a_{21} &a_{22} &a_{23} \
a_{31} &a_{32} &a_{33}
\end{bmatrix}
$$
row-major (행 우선)
row-major는 말 그대로 row 단위로 저장하겠다는 것이다.
다시 말해, 하나의 row를 완전히 저장하고 다음 row를 저장할 것이라는 뜻.
고로, 위의 그림에서와 같이 $a_{11}$이 저장된 메모리 다음 칸에는 $a_{12}$가 저장될 것이다. $a_{13}$이 저장된 메모리 다음 칸에는 다음 row의 첫 번째 값인 $a_{21}$이 저장될 것이다.
따라서, 메모리에는 다음과 같이 1차원으로 펴져서 저장된다.
$$
\begin{bmatrix}
a_{11} &a_{12} &a_{13} &a_{21} &a_{22} &a_{23} &a_{31} &a_{32} &a_{33}
\end{bmatrix}
$$
만약, a[3][4] 크기인 int형 2차원 배열의 base address(a[0][0]의 addr)가 1000이며, row-major로 저장된 경우 a[x][y]의 메모리 주소는 어떻게 될까?
생각을 해 보면, row-major 이므로 a[x][y]가 저장된 곳 앞에는 x개의 꽉 채운 row가 저장되어 있을 것이다. 꽉 찬 하나의 row에는 4개의 원소가 있으므로, 일단 얘 앞에 $(x-0)*4$개의 원소가 있다. 0을 뺀 것은 base address가 row 0부터 시작하기 때문이다.
그러다가 x+1번째 row에서는 꽉 채우지 못하고 y개만 저장되어 있는 것이므로, a[x][y]의 메모리 주소는 다음과 같다.
$$
a[x][y]addr = 1000+((x-0)*4)+y)*4
$$
마지막에 4를 곱한 것은 지금 a 배열이 int형이므로, 하나의 원소가 4byte를 차지할 것이기 때문이다.
column-major (열 우선)
col-major는 row-major와 같은 맥락으로 col 단위로 저장하겠다는 의미이다.
다시 말해, 하나의 온전한 col을 저장한 뒤 다음 col을 저장하겠다는 것.
고로, $a_{11}$을 저장한 메모리 다음 칸에는 $a_{21}$이 올 것이다. 이와 마찬가지로, $a_{32}$를 저장한 메모리 다음 칸에는 다음 column의 첫 번째 원소인 $a_{13}$이 저장될 것이다.
따라서, 메모리에는 다음과 같이 1차원으로 퍼져서 저장된다.
$$
\begin{bmatrix}
a_{11} &a_{21} &a_{31} &a_{12} &a_{22} &a_{32} &a_{13} &a_{23} &a_{33}
\end{bmatrix}
$$
만약, a[3][4] 크기인 int형 2차원 배열의 base address(a[0][0]의 addr)가 1000이며, col-major로 저장된 경우 a[x][y]의 메모리 주소는 어떻게 될까?
생각을 해 보면, col-major 이므로 a[x][y]가 저장된 곳 앞에는 y개의 꽉 채운 col이 저장되어 있을 것이다. 꽉 찬 하나의 col에는 3개의 원소가 있으므로, 일단 얘 앞에 $(y-0)*3$개의 원소가 있다. 0을 뺀 것은 base address가 col 0부터 시작하기 때문이다.
그러다가 y+1번째 col에서는 꽉 채우지 못하고 x개만 저장되어 있는 것이므로, a[x][y]의 메모리 주소는 다음과 같다.
$$
a[x][y]addr = 1000+ ((y-0)4) + x)4
$$
마지막에 4를 곱한 것은 앞에서와 같이 지금 a 배열이 int형이므로, 하나의 원소가 4byte를 차지할 것이기 때문이다.
row-major, col-major 가 중요한 이유는?
언어마다 기본적으로 row-major, col-major 중 어떤 것을 native하게 쓸 것인지가 정해져 있다.
구분 | 언어 |
---|---|
row-major | C/C++/Objective-C (for C-style arrays), PL/I, Pascal, Speakeasy, SAS, Rasdaman |
col-major | Fortran, MATLAB, GNU Octave, S-Plus, R, Julia, Scilab |
그래 그렇다 치는데, 그래서 왜 row-major, col-major 가 중요한 거냐고?
그건 바로 연속적인 배열 참조 시 cache memory 운용 때문이다.
엥 왜 갑자기 cache memory냐고? cache memory는 어디에나 숨 쉬고 어디에나 존재하는 문제이다.
cache memory 하면 떠올라야 하는 것이 바로 locality(지역성)다.
locality는 크게 보았을 때, spatial locality와 temporal locality로 나눌 수 있다.
locality에 대해 설명을 하고자 하는 포스팅이 아니므로, 두 locality의 차이점을 간단히 설명하겠다.
구분 | 특징 |
---|---|
spatial locality | 그 메모리 주변에 있는 것들이 앞으로 참조가 될 가능성이 높음 |
temporal locality | 그 메모리를 참조하였을 때 (시간) 같이 참조했던 것들이 앞으로 참조가 될 가능성이 높음 |
이름에서 알 수 있듯이, 위치(?)냐 시간이냐의 차이이다.
row-major와 col-major가 중요한 것은 spatial locality 때문이다.
C를 많이 쓰니까 C를 떠올려 보자. C는 row-major native이다.
고로, 배열 A는 다음과 같이 메모리에 저장될 것이다.
$$
\begin{bmatrix}
a_{11} &a_{12} &a_{13} &a_{21} &a_{22} &a_{23} &a_{31} &a_{32} &a_{33}
\end{bmatrix}
$$
cache memory에 최대 3개의 원소들이 올라간다고 가정하며 실제로는 그럴 리가 없지만, 처음에는 cache memory가 비어 있다고 가정하자. 또한, cache miss가 일어난 경우, 해당 원소를 포함하여 다음 원소 2개를 포함한 총 3개의 원소들을 cache에 올린다고 가정해 보자.
다시 말해 그냥 miss가 일어나면 cache 갈아치우는 셈이다.
이제 배열 A를 탐색하는 row-major와 col-major로 각각 탐색할 경우를 살펴보자.
- row-major 탐색
현재 cache 읽어야 하는 것 cache hit? - $a_{11}$ X $a_{11},a_{12},a_{13}$ $a_{12}$ O $a_{11},a_{12},a_{13}$ $a_{13}$ O $a_{11},a_{12},a_{13}$ $a_{21}$ X $a_{21},a_{22},a_{23}$ $a_{22}$ O ... ... ... - 총 9번 원소를 읽는 동안 3번의 cache miss, 6번의 cache hit이 일어난다.
- col-major 탐색
현재 cache 읽어야 하는 것 cache hit? - $a_{11}$ X $a_{11},a_{12},a_{13}$ $a_{21}$ X $a_{21},a_{22},a_{23}$ $a_{31}$ X $a_{31},a_{32},a_{33}$ $a_{12}$ X ... ... ...
근데 뭐 실제 cache가 저렇게 쫌쫌따리일 리는 없지만.. 그냥 방법 차이에 따라 cache가 잘 쓰이지 않아 비효율적일 수 있겠구나를 생각하면 된다.
이렇게 하나씩 쫌쫌따리로 탐색을 할 때도 문제이지만, CPU에서 병렬 처리로 SIMD(Single Instruction Multiple Data)를 이용할 때도 문제가 생길 수 있다.
SIMD는 대표적으로 행렬 곱셈에 이용될 수 있는데, 하나의 operation에 vector data를 가지고 뭉텅 곱셈을 해야 하는데 데이터를 vector data로 뭉텅 가져올 수 없으면 SIMD를 이용할 수 없다. 효율성에 문제가 생기겠지!
따라서, row-major와 col-major native 언어 사이에 array를 주고받을 일이 있다면, 각별히 유의해야 한다. 또한, 하나의 언어 안에서 사용하더라도, 내가 사용하는 언어가 어떤 것을 native로 쓰느냐에 따라 코드르 조금 더 효율적으로 짜도록 머리를 3초라도 더 굴려봐서 이득을 볼 수도 있겠다.
'잡다한 지식 > CS 베이스' 카테고리의 다른 글
[프로그래밍언어론] 객체지향 프로그래밍 언어의 특징 (0) | 2021.03.26 |
---|---|
[Data Structure] Heap (0) | 2021.03.19 |
#define 의 함정... 나만 빠짐 (1) | 2021.03.19 |
정수 범위 무한대 입력 방법 (0) | 2021.03.19 |
[Data Structure] Priority Queue (우선순위 큐) (0) | 2021.03.19 |