[Python] 리스트에서 메모리 할당에 대한 생각

Python에서 list에 어떻게 메모리를 할당하는가에 대한 나름의 생각을 정리해본다. 확실한 사실을 향해 가도록 공식 문서와 자료들을 더 찾아보고 공부할 것이다.


C, C++에서(아마 Java도) 배열을 할당하고 배열 원소들의 주소를 확인해보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
    int arr[] = {1, 2, 3};
    printf("address of arr : %d\n", &arr);
    printf("address of arr[1] : %d\n", &arr[1]);
    printf("address of arr[2] : %d\n", &arr[2]);
    return 0;
}


// address of arr : 13629912
// address of arr[1] : 13629916
// address of arr[2] : 13629920    

배열의 첫 주소에서 배열의 자료형인 int의 크기 4Byte만큼 더해지면서 차곡차곡 쌓이게 되고, 이를 보면 다음과 같은 공식을 어렵지 않게 생각할 수 있다.

인덱스 i번째 원소의 주소 = 배열 시작 주소 + (i * 자료형 크기)

이처럼 미리 자료형을 특정한다면 배열의 find by index의 원리는 크게 어렵지 않다.

그런데 문제는 Python이다. 이론적으로는 list에 모든 형태의 객체가 들어갈 수 있다.

1
arr = [1, 'hello', 3.14, set(), {'key': 1}]

물론 list를 이렇게 사용하는 사람은 거의 없겠지만, 이런 배열구조에서 어떻게 find by index가 작동하는지 꽤나 궁금하다. 직접 각 원소의 id를 찍어보자.

1
2
3
4
5
6
7
8
9
10
11
arr = [1, 'hello', 3.14, set(), {'key': 1}]
print("list의 시작 주소 : {}".format(id(arr)))
for i in range(len(arr)):
    print("list의 {}번째 원소의 id : {}".format(i, id(arr[i])))
    
# list의 시작 주소 : 2129934832008
# list의 0번째 원소의 id : 140736221323664
# list의 1번째 원소의 id : 2300636231728
# list의 2번째 원소의 id : 2300635046640
# list의 3번째 원소의 id : 2300636528200
# list의 4번째 원소의 id : 2300636240936

배열 원소들간의 주소가 아예 연관성이 없어보이지는 않지만, 그렇다고 해서 이걸 가지고 메모리 할당 위치를 특정하는 규칙이 있다고 보기에는 무리가 있다.

2가지 경우의 수를 생각해보았다.



1. list에 들어오는 값에 따른 메모리 할당

[Python] Python의 자료형과 크기에서도 말했지만, Python은 C로 작성된 object 관련 코드를 거치면서 자료형이 정해진다.

1
2
3
4
5
6
7
8
// Python에서 정수형이 선언될 때 거치는 구조체

struct _longobject {
    long ob_refcnt;
    PyTypeObject *ob_type;
    size_t ob_size;
    long ob_digit[1];
};

이 과정에서 자료형 크기별로 메모리 크기를 알 수 있고, 내부 최적화를 통해 할당 간격 / 위치를 정하는게 아닐까?한 것이 첫번째 생각이다. 사실 위에서 확인한 Python list의 주소를 봤을 때는 이 가설은 믿기 어렵다. 증감도 제멋대로이고 그 간격도 일정하지 않기 때문에 최적화에 따른 규칙이 있다고 보기는 어렵기 때문이다.

하지만 이런 경우에는 조금 다르다.

1
2
3
4
5
6
7
8
9
10
arr = [1, 2, 3, 4]
print('list의 시작 id : {}'.format(id(arr)))
for i in range(len(arr)):
    print('list의 {}번째 원소의 id : {}'.format(i, id(arr[i])))
    
# list의 시작 id : 2068005343624
# list의 0번째 원소의 id : 140736221323664
# list의 1번째 원소의 id : 140736221323696
# list의 2번째 원소의 id : 140736221323728
# list의 3번째 원소의 id : 140736221323760

list 안의 원소들이 모두 같은 자료형을 가지고 있다면 c와 비슷하게 일정 크기만큼 주소의 크기가 증가하는 것을 볼 수 있다. 이 경우에는 확실히 최적화를 통해 효율적인 메모리 관리(c처럼)를 하고 있다고 볼 수 있다.


2. 주소로 이루어진 별도 list의 존재

list 안에서 자료형 크기만큼 메모리를 먹을 수 없는 Python 언어의 특성상, list와는 별도로 주소를 따로 저장하는 list / linkedList가 있을거라 생각한다. 이를 기반으로 list에 값이 할당될때마다 메모리 어딘가에 값을 배치하고, find by index시에는 별도의 주소 list를 이용해 값을 출력해준다고 보는 것이 내 상식 선에서는 합리적인 추론이다. 주소의 크기는 같은 OS 안에서 모두 같기 때문에 관리가 가능할거라 본다.

image

찾아보니 이런 방식이 맞는 듯하다.



결론

위 2가지가 적절하게 이용된다고 보는 것이 맞는듯 하다. 새삼 쓰기 편하고 배우기 쉬운 Python이라는 타이틀 뒤에 얼마나 많은 엔지니어들의 노력과 고민이 있었을지 생각해보게 된다.