본문 바로가기

Python/Basic

Python - Int의 크기가 28bytes인 이유

 

*바쁘신 분들을 위한 3줄 요약

1. 28 bytes = refcnt (8 bytes) + Type pointer (8 bytes) + PySize (8 bytes) + digits (4 bytes)

2. refcnt는 GC를 위한 참조수, Type pointer는 Type을 표현하기 위한 포인터입니다.

3. Pysize, digits의 Array 구조 덕분에 큰 수에 대한 Overflow 걱정이 없습니다.

 

 

*목차

 - Python의 int는 쓸데없이 많은 메모리를 차지한다?

 - CPython의 코드를 살펴보자

 - PyLongObject 분석

 - Python의 int는 Overflow 되지 않는다

 - Python에서 표현 가능한 가장 큰 숫자는?

 

 

 


Python의 Int는 쓸데없이 많은 메모리를 차지한다?

혹시 위와 같은 불만을 가지고 계신 분이 있으신가요? 하지만 정말로 python으로 만든 프로그램이 과도한 메모리 사용으로 문제를 일으켰던 경험보다는 "비효율적인 파이썬!" 이라는 주변의 평가를 그대로 따라 하시는 분들이 대다수일 것입니다. 그리고 python의 int가 얼마나 많은 메모리를 차지하는지, 왜 많은 메모리를 차지하는지 아시는 분들은 별로 없을 겁니다.

 

C/C++과 비교해 보겠습니다. C/C++은 메모리에 데이터만 저장합니다. 만약 정수 하나를 저장한다고 하면 C++에서는 얼마만큼의 메모리가 필요할까요? 정수는 보통 int 형식으로 저장하고, int의 크기는 64bit OS기준으로 4 bytes입니다. 아래는 int(4 bytes) 크기의 숫자 5개를 메모리에 저장하는 그림입니다. int값 5개가 저장된 크기는 총 20bytes이겠군요.

 

arr배열에 int(4 bytes)가 아름답게 차곡차곡 저장되어 있습니다.

 

그런데 python은 int 하나에 28 bytes를 차지합니다. C++에서 int 7개를 저장할 수 있는 공간과 같습니다.

>>> import sys       	## 변수의 메모리 할당 크기를 알아내기 위한 시스템 라이브러리
>>> a = 1024         	## a라는 변수에 1024(int값)을 저장했다.

>>> sys.getsizeof(a) 	## 메모리 사이즈를 확인하면 28 bytes임을 확인할 수 있다.
28

>>> type(a)           	## a의 type을 검사하면 int인 것을 확인할 수 있다.
<class 'int'>

 

이게 어찌 된 일일까요? C/C++처럼 int값 하나에 4 bytes만 사용하지 않고 왜 28 bytes라는 크기를 할당하는 것일까요? 그것은 바로, 28 bytes에는 int 데이터뿐만 아니라, 그 데이터를 설명하기 위한 Metadata도 포함되어 있기 때문입니다.

* metadata : 데이터를 설명하는 데이터

 

Metadata가 실제로 어떻게 구성 되어있는지는 python의 내부 구조를 살펴봐야 합니다. 필자는 Python의 가장 유명한 Interpreter인 CPython의 Code를 확인하면서 설명드리도록 하겠습니다.

 

 

 


CPython의 코드를 살펴보자

CPython의 코드는 https://github.com/python/cpython에서 받아보실 수 있습니다. 

vscode에서 CPython코드를 열어보았습니다.

 

 

Code가 워낙 방대하여, int 객체가 어떻게 표현되어 있는지 쉽게 알기 어렵습니다. docs.python.org에서 int객체에 대한 정보를 확인할 수 있습니다.

docs.python.org에서 설명하고 있는 정수 객체(int object)

 

Python 설명서에는 모든 정수는 PyLongObject로 구현된다고 합니다. 이 키워드를 가지고 CPython Code에서 검색하면, PyLongObject는 pytypedefs.h에 정의된 것을 확인할 수 있습니다.

// Forward declarations of types of the Python C API.
// Declare them at the same place since redefining typedef is a C11 feature.
// Only use a forward declaration if there is an interdependency between two
// header files.

#ifndef Py_PYTYPEDEFS_H
#define Py_PYTYPEDEFS_H
#ifdef __cplusplus
extern "C" {
#endif

typedef struct PyModuleDef PyModuleDef;
typedef struct PyModuleDef_Slot PyModuleDef_Slot;
typedef struct PyMethodDef PyMethodDef;
typedef struct PyGetSetDef PyGetSetDef;
typedef struct PyMemberDef PyMemberDef;

typedef struct _object PyObject;
typedef struct _longobject PyLongObject;
typedef struct _typeobject PyTypeObject;
typedef struct PyCodeObject PyCodeObject;
typedef struct _frame PyFrameObject;

typedef struct _ts PyThreadState;
typedef struct _is PyInterpreterState;

#ifdef __cplusplus
}
#endif
#endif   // !Py_PYTYPEDEFS_H

* include/pytypedefs.h

 

typedef struct _longobject PyLongObject;가 바로 28 bytes를 차지하는 정수 객체입니다. 이 객체의 구조를 확인하면 28 bytes 메모리 크기의 비밀을 알 수 있습니다.

 

 


PyLongObject 분석

PyLongObject는 _longobject로 정의됩니다.

typedef struct _longobject PyLongObject;

* Include/pytypedefs.h

 

_longobject는 다음과 같이 정의됩니다.

struct _longobject {
    PyObject_HEAD
    _PyLongValue long_value;
};

* Include/cpython/longintrepr.h

 

PyObject_HEAD 모든 PyObject의 initial segment을 의미합니다. Type은 PyObject입니다.

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   PyObject ob_base;

* Include/object.h

 

PyObject는 다시 pytypedefs.h에서 정의됩니다.

typedef struct _object PyObject;

* include/pytypedefs.h

 

_object는 참조 횟수와 Type의 포인터로 정의됩니다.

/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
};

* Include/object.h

 

_object의 구조는 다음과 같습니다.

 1. _PyObject_HEAD_EXTRA는 Py_TRACE_REFS가 define되어 있지 않아 동작하지 않습니다.

 2. ob_refcnt는 참조 횟수로, GC(garbage collector)가 메모리 관리를 하기 위한 용도입니다. Py_ssize_t는 long long type입니다. (8 bytes)

 3. *ob_type은 Type형식의 주소(64bit OS)를 저장합니다. (8 bytes)

 

위 분석을 통해 PyObject_HEAD는 8 bytes + 8 bytes = 16 bytes임을 확인할 수 있습니다.

struct _longobject {
    PyObject_HEAD  	// 16bytes
    _PyLongValue long_value;
};

* Include/cpython/longintrepr.h

 

 

*포인터가 Python의 int 크기에 영향을 미친다면 32bit OS에서의 Python int 크기는 어떻게 될까요? 32bit OS에서의 Point 크기는 4bytes입니다. 이는 *ob_type가 4 bytes이라는 것을 의미합니다. 따라서 32bit python에서 int의 크기는 24 bytes가 됩니다.

 

 

다음은 _PylongValue를 알아보겠습니다. longintrepr.h에서 정의됩니다.

typedef struct _PyLongValue {
    Py_ssize_t ob_size; /* Number of items in variable part */
    digit ob_digit[1];
} _PyLongValue;

* Include/cpython/longintrepr.h

 

_PyLongValue구조는 다음과 같습니다.

 1. ob_size는 아래 보이는 ob_digit의 배열 크기를 의미합니다. Py_ssize_t는 long long입니다. (8 bytes)

 2. ob_digits[1]은 digit 배열 1개를 할당합니다. digit은 uint32_t입니다.(4 bytes)

 * ob_digits가 unsigned인데 음수를 어떻게 표현할까요? ob_size가 음수로 표현됩니다.

 * 예 : a = -10 이라 하면 ob_digits[0] = 10, ob_size = -1이 됩니다.

 

이제 모든 bytes를 합쳐보면 다음과 같습니다.

struct _longobject {
    PyObject_HEAD               // ref_count number 8 bytes + Type pointer 8 bytes
    _PyLongValue long_value;    // digits count number 8 bytes + digit array 4 bytes
};

* Include/cpython/longintrepr.h

 

초등학생도 할 수 있는 연산인 8+8+8+4를 계산하면 28이 나옵니다. 이 28숫자에는 아래와 같은 의미가 담겨 있습니다.

 1. GC의 메모리 관리를 위한 참조 횟수 저장공간 8 bytes

 2. Type을 가리키는 주소 8 bytes

 3. digit의 개수 저장공간 8 bytes

 4. array로 관리되고 있는 int값 그 자체 4 bytes

 

그림으로 그린다면 다음과 같이 그릴 수 있게 됩니다.

Python int 구조

 

python은 int값을 저장하는 4 bytes 이외에 24 bytes나 metadata로 활용하고 있었습니다. 도대체 어떤 이유로 24bytes나 되는 크기를 metadata로 가지고 있는 것일까요?

 

 


Python의 int는 Overflow 되지 않는다

* Overflow는 무엇일까요? C++의 int를 예를 들어 간단히 설명해 보겠습니다. 4 bytes로 표현할 수 있는 최대 크기 숫자는 2^32 - 1입니다. 이 숫자는 31개의 방에 1이 가득찬 상태입니다. 만약 여기서 1을 더하게 된다면 다음 자리수를 표현할 32번째 방이 필요합니다. 하지만 int를 4bytes로 규정했기 때문에 32번째 방은 존재할 수 없습니다. 따라서 32번째 방에 들어갈 숫자는 사라지고 데이터가 엉망이 됩니다. 이런 현상을 Overflow라 합니다.

 

기존 Int는 4bytes가 넘는 숫자를 다루게 되면 Overflow 현상이 일어나게 됩니다. 하지만 Python의 경우 PyLongObject의 digits array의 크기를 키워 그 숫자를 감당하도록 합니다. 아래는 PyLongObject의 메모리 크기를 정하는 부분이 있는 PyObject_Malloc이 포함된 함수 _PyLong_New입니다.

 

* Objects/longobject.c

/* Allocate a new int object with size digits.
   Return NULL and set exception if we run out of memory. */

#define MAX_LONG_DIGITS \
    ((PY_SSIZE_T_MAX - offsetof(PyLongObject, long_value.ob_digit))/sizeof(digit))

PyLongObject *
_PyLong_New(Py_ssize_t size)
{
    PyLongObject *result;
    if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
        PyErr_SetString(PyExc_OverflowError,
                        "too many digits in integer");
        return NULL;
    }
    /* Fast operations for single digit integers (including zero)
     * assume that there is always at least one digit present. */
    Py_ssize_t ndigits = size ? size : 1;
    /* Number of bytes needed is: offsetof(PyLongObject, ob_digit) +
       sizeof(digit)*size.  Previous incarnations of this code used
       sizeof(PyVarObject) instead of the offsetof, but this risks being
       incorrect in the presence of padding between the PyVarObject header
       and the digits. */
    result = PyObject_Malloc(offsetof(PyLongObject, long_value.ob_digit) +
                             ndigits*sizeof(digit));
    if (!result) {
        PyErr_NoMemory();
        return NULL;
    }
    _PyObject_InitVar((PyVarObject*)result, &PyLong_Type, size);
    return result;
}

 

_PyLong_New에 input값인 size에 따라 int Array의 크기가 정해집니다. python에서 실제 동작은 2^30을 기준으로 array의 크기가 증가합니다.

>>> a = 2**30-1       	## 1073741823
>>> sys.getsizeof(a)
28

>>> a = 2**30        	## 1073741824
>>> sys.getsizeof(a)   	## memory가 4 bytes 늘었다.
32

>>> a = 2**60-1      	## 1152921504606846975
>>> sys.getsizeof(a)  	## memory가 유지된다.
32

>>> a = 2**60        	## 1152921504606846976
>>> sys.getsizeof(a)   	## memory가 4 bytes 늘었다.
36

 

 

30을 기준으로 Shift되는 것은 PyLong_SHIFT를 30으로 정의했기 때문입니다.

#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
typedef int32_t sdigit; /* signed variant of digit */
typedef uint64_t twodigits;
typedef int64_t stwodigits; /* signed variant of twodigits */
#define PyLong_SHIFT    30
#define _PyLong_DECIMAL_SHIFT   9 /* max(e such that 10**e fits in a digit) */
#define _PyLong_DECIMAL_BASE    ((digit)1000000000) /* 10 ** DECIMAL_SHIFT */

 

 31도 아니고 30으로 정해진 것에는 여러가지 이유가 있습니다. 간략하게 설명드리면...

더보기

1. long_pow() 함수는 PyLong_SHIFT가 5의 배수여야 한다.

 

2. PyLong_{As,From}ByteArray는 PyLong_SHIFT가 8이상이어야 한다.

 

3. long_hash()는 PyLong ↔ long(or unsigned long) 변환함수와 마찬가지로 PyLong_SHIFT가 unsighed long의 bit수보다반드시 작아야 한다. (즉 32보다 작아야 함)

 

4. Python int ↔ size_t/Py_ssize_t 변환함수는 PyLong_SHIFT가 size_t의 bit수보다 반드시 작아야 한다 (즉 32보다 작아야 함)

 

5. marshal code는 PyLong_SHIFT가 15의 배수라고 예상

(marshal에 대한 설명 : https://docs.python.org/3/library/marshal.html)

 

6. NSMALLNEGINTS 및 NSMALLPOSINTS는 한 digit에 맞을 만큼 작아야 한다. 현재 값으로는 PyLong_SHIFT가 9 이상어야 한다.

( NSMALLPOSINTS는 257(=2^8 + 1 → 9 이상 필요), NSMALLNEGINTS는 5로 설정되어 있습니다 )

( NSMALLNEGINTS, NSMALLPOSINTS는 poycoer_interp.h에 정의되어 있습니다.

 

위와 같은 이유때문에 PyLong_SHIFT는 15, 30중 하나로 정의할 수 있는데, 지금은 30으로 지정해서 사용중입니다.

 

이번에는 PyLong_SHIFT를 기준으로 실제 ob_size가 늘어나는 것을 그림으로 표현해 보았습니다.. 이 그림을 보시면 이해에 도움이 많이 될 것입니다.

int memory size가 28bytes에서 32bytes로 증가하는 순간이다.

 

 

int memory size가 32bytes에서 36bytes로 증가하는 모습이다.

 

* 2^60 - 1이 어떻게 3F FF FF FF 3F FF FF FF냐라고 반문하시는 분들이 있을 수 있습니다. python에서 ob_digit[1]은 ob_digit[0]에서 그대로 8 bytes처럼 붙어서 계산되는 것이 아니라 40 00 00 00이 곱해져서 계산된다 생각하시면 됩니다.

2^60 - 1은 실제로 0F FF FF FF   FF FF FF FF입니다. 그리고 이는 (3F FF FF FF x 40 00 00 00) + 3F FF FF FF와 동일합니다.

 

그렇다면 새로운 궁금증이 생겼습니다. 이런 방식으로는 얼마나 큰 숫자를 표현할 수 있을까요? 우리는 Array방의 개수가 PyLongObject 내에 있는 ob_size에 의해 정해져 있다는 것을 위에서 확인했습니다. 그 크기가 8 bytes로 표현 가능한 숫자 (2^64-1)이라는 것까지 말이죠.

 

그림으로 표현하면 다음과 같습니다.

이렇게 되면 Int Memory size는 대략 73.79 EB(엑사 바이트)가 된다...

 

즉, Python에서 큰 수의 이론적인 한계는 다음과 같습니다.

대략 10^10000000000000000000 (10^10^20) 정도 되는 숫자입니다. 음수같은 영역을 제외해서 고려한다 하더라도 너무 큰 숫자입니다. 만약 큰 숫자로 인한 문제가 생긴다면 그 문제는 Max memory limit Error (메모리 초과)가 먼저 일 것입니다. 실질적으로 Overflow를 걱정할 필요는 없겠군요.

 

 


Python에서 표현 가능한 가장 큰 숫자는?

그럼 실제로 python에서는 어떠한 숫자든 표현이 가능할까요? 숫자를 표현한다는 것은 Computer에 있는 2진법(binary)를 10진법(decimal)으로 표현한다는 것입니다.

 

11010(2진법)으로 저장된 데이터를 26(10진법)으로 바꿔야 한다.

 

이러한 표현 방법으로 인해, 결국 26이라는 표현 할 string길이(2글자)를 고려해야 합니다. 그리고 python은 기본적으로 이 string의 길이를 4300자로 제한하고 있습니다.

>>> a = int('9'*4300)   ## 9가 4300개 있는 숫자이다 정확히는 10^4301 - 1
>>> a                 	## a를 출력해도 문제 없다.
99999999999999999999999999 ... 999
>>> a += 1              ## a를 증가시켰다. 동작에 문제는 없다.
>>> a                 	## 하지만 출력에 문제가 된다.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Exceeds the limit (4300) for integer string conversion;
use sys.set_int_max_str_digits() to increase the limit

 

만약에 4300자 이상의 출력을 원하신다면 sys.set_int_max_str_digits() 함수를 사용해서 제한을 풀면 됩니다.

>>> import sys                         	## sys library를 불러온다
>>> sys.set_int_max_str_digits(7000)   	## 길이제한을 7000으로 올린다.
>>> a                                 	## 문제없이 출력된다.
100000000000000...000

 

 


마치며...

python int 객체는 사용자가 Overflow나 메모리 누수 걱정 없이 프로그래밍에 온전히 집중할 수 있도록 설계되어 있습니다. 이런 설계를 구현하기 위해 int size를 28 bytes로 할당했으며, 24byte가 metadata이고 4byte가 정수 data입니다. 좀 더 자세히 설명하면 아래와 같습니다.

 

  1. GC를 위한 참조개수 (8 bytes metadata)

  2. Type 정보 (8 bytes metadata)

  3. 큰 숫자를 위한 int 방 개수 (8 bytes metadata)

  4. 실질적인 4 bytes int array data (가변적임)

 

* 참고로 Java에서도 integer 객체는 16 bytes를 차지하고 있으며 12 bytes가 meta 정보입니다. 또한 Java에서는 BigInteger 객체를 통해 무제한 숫자를 제공합니다. python은 이 2가지 객체를 합친 형태라 할 수 있습니다. 지금은 C/C++과 python을 자주 비교하고 있는데, 나중에는 Java와도 비교할 것 같네요.

 

개발자보다 반도체가 저렴해진 요즘 세상에는, 백엔드 프로그램 메모리공간 최적화보다 Hardware Scale up을 선호하고, 프로그램 몇 kB공간을 줄이는 것보다 빠른 개발속도에 관심이 많습니다. 우리가 직접 C++로 Overflow 걱정 없는 객체를 만드는데 얼마나 걸릴까요? 직접 만든 그 객체는 사칙연산으로부터 안전할까요? 안전한지 검증하는 데는 또 얼마나 걸릴까요? 그 시간이 아깝다면, python을 사용해 보시길 바랍니다.

 


* reference

https://github.com/python/cpython

https://docs.python.org/ko/3/c-api/long.html

https://www.ibm.com/docs/en/ibm-mq/7.5?topic=platforms-standard-data-types 

https://www.jdoodle.com/python-programming-online/

https://docs.python.org/3/library/marshal.html

'Python > Basic' 카테고리의 다른 글

Python - List Comprehension  (0) 2023.03.19
Python - Mutable vs Immutable  (2) 2023.02.26
Python - 기본 문법정리  (0) 2023.02.24
Python - List는 어떻게 데이터를 관리하는가?  (0) 2023.02.15
Python - is와 ==의 차이  (1) 2023.02.10