상위 목록: 하위 목록: 작성 날짜: 읽는 데 3 분 소요

메모리 레이아웃(Memory Layout)

메모리 레이아웃이란 Numpy 배열의 요솟값이 메모리에 정렬되는 방식을 의미합니다.

컴퓨터의 메모리는 1차원으로 이어진 공간이므로, 2차원 이상의 배열을 저장하려면 요소들을 한 줄로 나열하는 순서를 정해야 합니다.

다차원 배열의 요솟값 정렬 방식은 크게, 행 우선(Row-Major) 방식과 열 우선(Column-Major) 방식이 있습니다.

행 우선 방식은 첫 번째 행을 메모리에 넣은 다음 두 번째 행을 메모리에 넣습니다.

열 우선 방식은 첫 번째 열을 메모리에 넣은 다음 두 번째 열을 메모리에 넣습니다.

이런 방식으로 행 또는 열의 순서대로 메모리 레이아웃의 구조가 형성됩니다.



행 우선(Row-Major)

행 우선 방식은 위 그림과 같이 행 인덱스를 고정한 채, 열 인덱스가 높아지는 순서로 정렬됩니다.

즉, 마지막 색인이 가장 빠르게 변화하는 순서로 할당됩니다. 행 우선 방식은 C 언어 스타일의 메모리 순서이며, Numpy 배열의 기본값입니다.

만약, 3차원 배열일 때 [i][j][k] 형태로 색인이 구성돼 있다면 k의 값부터 순차적으로 증가하고, k 색인이 최댓값에 도달하면 j 색인이 증가하는 구조입니다.

메모리 레이아웃 구조를 볼 때, 같은 행의 요소들이 메모리에 연속적으로 놓입니다.



열 우선(Column-Major)

열 우선 방식은 위 그림과 같이 열 인덱스를 고정한 채, 행 인덱스가 높아지는 순서로 정렬됩니다.

즉, 첫 번째 색인이 가장 빠르게 변화하는 순서로 할당됩니다. 열 우선 방식은 Fortran 언어 스타일의 메모리 순서이며, MATLAB이나 R 등도 이 방식을 사용합니다.

만약, 3차원 배열일 때 [i][j][k] 형태로 색인이 구성돼 있다면 i의 값부터 순차적으로 증가하고, i 색인이 최댓값에 도달하면 j 색인이 증가하는 구조입니다.

메모리 레이아웃 구조를 볼 때, 같은 열의 요소들이 메모리에 연속적으로 놓입니다.



Numpy의 메모리 레이아웃

import numpy as np

array = np.arange(10)

print(array)

arrayC = np.reshape(array, (2, -1), order='C')
arrayF = np.reshape(array, (2, -1), order='F')

print(arrayC)
print(arrayF)
결과
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
 [5 6 7 8 9]]
[[0 2 4 6 8]
 [1 3 5 7 9]]

Numpy 배열에서는 order 매개변수의 인수를 통해 요소를 읽고 쓰는 순서를 설정할 수 있습니다.

arrayC는 C 언어 스타일의 순서로 요소를 채우며, arrayF는 Fortran 언어 스타일의 순서로 요소를 채웁니다.

arrayC는 0 ~ 4를 첫 번째 행에 먼저 채우고, arrayF는 0, 1을 첫 번째 열에 먼저 채우는 것을 확인할 수 있습니다.

np.reshape의 order는 색인을 읽고 쓰는 순서를 의미하며, 결과 배열이 실제로 어떤 메모리 레이아웃을 갖는지와는 별개의 개념입니다.

order 매개변수는 C 언어 스타일과 Fortran 언어 스타일 외에도 원본 배열의 레이아웃을 따르는 두 종류의 값을 추가로 사용할 수 있습니다.

단, 함수마다 허용하는 값이 다릅니다. np.reshape는 'C', 'F', 'A'를 허용하며, np.array, ndarray.copy, np.ravel 등은 'K'까지 허용합니다.



메모리 레이아웃 정렬 플래그

order 의미
‘K’ 원본 배열의 레이아웃에 최대한 일치
‘A’ 원본이 Fortran 연속 배열이면 ‘F’, 아닐 경우 ‘C’
‘C’ C 언어 스타일
‘F’ Fortran 언어 스타일



스트라이드(Strides)

import numpy as np

array = np.arange(6, dtype=np.int32).reshape(2, 3)

print(array.strides)
print(np.asfortranarray(array).strides)
print(array.T.strides)
결과
(12, 4)
(4, 8)
(4, 12)

스트라이드(strides)는 각 축의 색인이 1 증가할 때 메모리에서 몇 바이트를 건너뛰어야 하는지를 나타내는 튜플입니다.

Numpy는 이 값을 이용해 1차원 메모리 공간의 데이터를 다차원 배열처럼 해석합니다.

int32 자료형은 요소 하나가 4 바이트이므로, (2, 3) 형태의 C 언어 스타일 배열은 다음 열로 갈 때 4 바이트, 다음 행으로 갈 때 한 행 전체인 3 × 4 = 12 바이트를 건너뜁니다.

np.asfortranarray로 Fortran 언어 스타일 배열을 만들면 다음 행으로 갈 때 4 바이트, 다음 열로 갈 때 한 열 전체인 2 × 4 = 8 바이트를 건너뜁니다.

전치(T)는 데이터를 복사하지 않고 스트라이드의 순서만 뒤바꾼 배열을 반환합니다. 그러므로 (12, 4)가 (4, 12)로 바뀝니다.

  • Tip : 메모리에서 연속된 요소를 순서대로 읽는 연산이 더 빠릅니다. C 언어 스타일 배열은 마지막 축 방향의 반복이, Fortran 언어 스타일 배열은 첫 번째 축 방향의 반복이 더 효율적입니다.



메모리 연속성(Contiguous)

import numpy as np

array = np.arange(6).reshape(2, 3)
transpose = array.T
contiguous = np.ascontiguousarray(transpose)

print(array.flags['C_CONTIGUOUS'], array.flags['F_CONTIGUOUS'])
print(transpose.flags['C_CONTIGUOUS'], transpose.flags['F_CONTIGUOUS'])
print(contiguous.flags['C_CONTIGUOUS'], contiguous.flags['F_CONTIGUOUS'])
print(np.shares_memory(array, transpose), np.shares_memory(array, contiguous))
결과
True False
False True
True False
True False

flags 속성으로 배열의 메모리 레이아웃 상태를 확인할 수 있습니다.

C_CONTIGUOUS는 배열이 C 언어 스타일로 연속된 메모리에 저장돼 있는지, F_CONTIGUOUS는 Fortran 언어 스타일로 연속된 메모리에 저장돼 있는지를 의미합니다.

array는 기본값인 C 언어 스타일 배열이며, 이를 전치한 transpose는 같은 메모리를 공유하므로 Fortran 언어 스타일의 연속 배열이 됩니다.

np.ascontiguousarray는 입력 배열을 C 언어 스타일의 연속 배열로 반환합니다. 이미 연속 배열이라면 복사하지 않지만, 그렇지 않다면 새로운 메모리에 복사합니다.

np.shares_memory로 두 배열이 같은 메모리를 공유하는지 확인할 수 있으며, transpose는 원본과 메모리를 공유하지만 contiguous는 별도의 메모리를 사용하는 것을 확인할 수 있습니다.

  • Tip : 슬라이싱(array[:, ::2])이나 전치처럼 뷰(View)를 반환하는 연산은 연속성이 깨질 수 있습니다. C 언어 확장 모듈이나 일부 라이브러리는 연속 배열을 요구하므로, 이때 np.ascontiguousarray를 사용합니다.



레이아웃에 따른 평탄화

import numpy as np

array = np.arange(6).reshape(2, 3)

print(array.ravel(order='C'))
print(array.ravel(order='F'))
print(array.T.ravel(order='K'))
결과
[0 1 2 3 4 5]
[0 3 1 4 2 5]
[0 1 2 3 4 5]

np.ravel은 다차원 배열을 1차원 배열로 평탄화하며, order 매개변수에 따라 요소를 읽는 순서가 달라집니다.

order='C'는 행 방향으로, order='F'는 열 방향으로 요소를 읽습니다.

order='K'는 실제 메모리에 저장된 순서대로 요소를 읽습니다. array.T는 [[0, 3], [1, 4], [2, 5]]의 형태이지만, 메모리에는 원본의 순서인 0 ~ 5로 저장돼 있으므로 [0 1 2 3 4 5]를 반환합니다.

메모리 순서대로 읽을 수 있다면 np.ravel은 복사 없이 뷰를 반환하므로, order='K'는 불필요한 복사를 피할 때 유용합니다.

댓글 남기기