PEP 353 – ssize_t를 인덱스 타입으로 사용하기
- Author:
- Martin von Löwis <martin at v.loewis.de>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 18-Dec-2005
- Python-Version:
- 2.5
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Python 2.4에서 시퀀스의 인덱스는 C 타입 int로 제한됩니다. 따라서 64비트 시스템에서는 시퀀스가 전체 주소 공간을 사용할 수 없으며, 2**31개 요소로 제한됩니다. 이 PEP는 플랫폼별 인덱스 타입 Py_ssize_t를 도입하여 이를 변경할 것을 제안합니다. 제안된 변경 사항의 구현은 http://svn.python.org/projects/python/branches/ssize_t 에 있습니다.
근거
64비트 시스템이 점점 더 널리 사용되고 있으며, 주 메모리의 크기는 4GiB를 초과하여 증가하고 있습니다. 이러한 시스템에서 Python은 현재 제한되어 있으며, 시퀀스(문자열, 유니코드 객체, 튜플, 리스트, array.arrays 등)는 2Gi 요소보다 많이 포함할 수 없습니다.
오늘날 더 큰 리스트를 표현할 수 있는 메모리를 갖춘 시스템은 매우 적습니다. 64비트 시스템에서는 각 포인터가 8B이므로 이러한 리스트의 포인터만 보유하는 데도 16GiB가 필요하며, 리스트에 데이터가 포함되면 메모리 사용량은 더욱 증가합니다. 그러나 현재 사용자들이 개선을 요청하는 컨테이너 타입은 세 가지입니다.
- 문자열(현재 2GiB로 제한됨)
- mmap 객체(마찬가지로 제한되며, 시스템은 일반적으로 전체 객체를 동시에 메모리에 유지하지 않음)
- Numarray 객체(Numerical Python에서 제공)
제안된 변경 사항은 64비트 시스템에서 비호환성을 초래하므로, 이러한 시스템이 널리 사용되지 않는 동안(즉, 가능한 한 일찍) 수행해야 합니다.
사양
새로운 타입 Py_ssize_t가 도입되며, 컴파일러의 size_t 타입과 동일한 크기를 가지지만 부호가 있습니다. 사용 가능한 경우 ssize_t의 typedef가 됩니다.
표준 배포판에 포함된 모든 타입에 대해 모든 컨테이너 타입의 길이 필드 내부 표현을 int에서 ssize_t로 변경합니다. 특히 PyObject_VAR_HEAD가 Py_ssize_t를 사용하도록 변경되므로, 해당 매크로를 사용하는 모든 확장 모듈에 영향을 미칩니다.
타입 객체의 시퀀스 슬롯과 버퍼 인터페이스를 포함하여, 인덱스 및 길이 매개변수와 결과의 모든 사용을 Py_ssize_t를 사용하도록 변경합니다.
새로운 변환 함수 PyInt_FromSsize_t 및 PyInt_AsSsize_t를 도입합니다. PyInt_FromSsize_t는 값이 LONG_MAX를 초과하면 투명하게 long int 객체를 반환하며, PyInt_AsSsize_t는 long int 객체를 투명하게 처리합니다.
새로운 함수 포인터 typedef인 ssizeargfunc, ssizessizeargfunc, ssizeobjargproc, ssizessizeobjargproc 및 lenfunc를 도입합니다. 버퍼 인터페이스 함수 타입은 이제 readbufferproc, writebufferproc, segcountproc 및 charbufferproc라고 부릅니다.
PyArg_ParseTuple Py_BuildValue, PyObject_CallFunction 및 PyObject_CallMethod에 새로운 변환 코드 ‘n’을 도입합니다. 이 코드는 Py_ssize_t에서 작동합니다.
매크로 PY_SSIZE_T_CLEAN이 Python.h를 포함하기 전에 정의되어 있으면 변환 코드 ‘s#’ 및 ‘t#’는 Py_ssize_t를 출력하고, 해당 매크로가 정의되어 있지 않으면 계속 int를 출력합니다.
size_t/Py_ssize_t에서 int로 변환해야 하는 부분에서는 사례별로 변환 전략을 선택합니다(다음 절을 참조하십시오).
32비트 크기 타입을 가정하는 확장 모듈이 64비트 크기 타입을 사용하는 인터프리터에 로드되는 것을 방지하기 위해 Py_InitModule4의 이름을 Py_InitModule4_64로 변경합니다.
변환 지침
모듈 작성자는 자신의 코드에서 이 PEP를 지원할지 여부를 선택할 수 있으며, 지원하는 경우에도 서로 다른 수준의 호환성 중에서 선택할 수 있습니다.
이 PEP를 지원하도록 모듈을 변환하지 않으면, 32비트 시스템에서 수정하지 않은 상태로 계속 작동합니다. 64비트 시스템에서는 컴파일 시 오류와 경고가 발생할 수 있으며, 경고를 무시하면 모듈이 인터프리터를 충돌시킬 수 있습니다.
모듈을 변환할 때는 int 인덱스를 계속 사용하도록 시도하거나, 전체에서 Py_ssize_t 인덱스를 사용할 수 있습니다.
모듈이 int 인덱스를 계속 사용해야 한다면, Py_ssize_t 또는 size_t를 반환하는 함수를 호출할 때, 특히 객체의 길이를 반환하는 함수(strlen 함수와 sizeof 연산자 포함)를 호출할 때 주의해야 합니다. 좋은 컴파일러는 Py_ssize_t/size_t 값이 int로 잘릴 때 경고합니다. 이러한 경우에는 세 가지 전략을 사용할 수 있습니다.
- 크기가 int를 절대 초과할 수 없음을 정적으로 판단합니다(예: 구조체의 sizeof를 구하거나 파일 경로명의 strlen을 구하는 경우). 이 경우 다음과 같이 작성하십시오.:
some_int = Py_SAFE_DOWNCAST(some_value, Py_ssize_t, int);
이렇게 하면 디버그 모드에서 값이 실제로 int에 들어맞는지 확인하는 어설션이 추가되고, 그렇지 않은 경우에는 캐스트만 추가됩니다.
- C 코드 어딘가에 버그가 없는 한 값이 int를 오버플로하지 않아야 함을 정적으로 판단합니다. 값이 INT_MAX보다 작은지 테스트하고, 그렇지 않으면 InternalError를 발생시킵니다.
- 그렇지 않으면 값이 int에 들어맞는지 확인하고, 들어맞지 않으면 ValueError를 발생시킵니다.
tp_as_sequence 슬롯에도 동일한 주의를 기울여야 하며, 추가로 이러한 슬롯의 시그니처가 변경되므로 슬롯을 명시적으로 다시 캐스트해야 합니다(예: intargfunc에서 ssizeargfunc로). 다음 테스트를 사용하면 이전 Python 버전과의 호환성을 확보할 수 있습니다.:
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
#define PY_SSIZE_T_MIN INT_MIN
#endif
그런 다음 나머지 코드에서 Py_ssize_t를 사용합니다. tp_as_sequence 슬롯에는 추가 typedef가 필요할 수 있으며, 또는 다음과 같이 바꾸면 됩니다.:
PyObject* foo_item(struct MyType* obj, int index)
{
...
}
다음과 같이 바꾸면:
PyObject* foo_item(PyObject* _obj, Py_ssize_t index)
{
struct MyType* obj = (struct MyType*)_obj;
...
}
캐스트를 완전히 생략할 수 있습니다. 그러면 foo_item의 형식은 모든 Python 버전에서 sq_item 슬롯과 일치해야 합니다.
모듈을 확장하여 Py_ssize_t 인덱스를 사용해야 한다면, int 형식의 모든 사용처를 검토하여 Py_ssize_t로 변경해야 하는지 확인해야 합니다. 컴파일러가 해당 위치를 찾는 데 도움을 주지만, 수동 검토도 여전히 필요합니다.
PyArg_ParseTuple 호출에는 특히 주의를 기울여야 합니다. 모든 호출에서 s# 및 t# 변환기를 확인해야 하며, 호출을 그에 맞게 업데이트했다면 Python.h를 포함하기 전에 PY_SSIZE_T_CLEAN을 정의해야 합니다.
Fredrik Lundh는 서명이 변경된 API의 사용 여부를 C 모듈의 코드에서 검사하는 scanner를 작성했습니다.
논의입니다.
size_t를 사용하지 않는 이유입니다.
이 기능을 구현하려는 초기 시도에서는 size_t를 사용하려고 했습니다. 그러나 이는 작동할 수 없다는 사실이 곧 밝혀졌습니다. Python은 여러 곳에서 음수 인덱스를 사용하여 끝에서부터 세는 것을 나타내기 때문입니다. size_t를 사용할 수 있는 경우에도 코드를 너무 많이 다시 작성해야 했습니다. 예를 들어 다음과 같은 루프에서는:
for(index = length-1; index >= 0; index--)
index를 int에서 size_t로 변경하면 이 루프는 절대 종료되지 않습니다.
Py_intptr_t를 사용하지 않는 이유입니다.
개념적으로 Py_intptr_t와 Py_ssize_t는 서로 다른 것입니다. Py_intptr_t는 void*와 같은 크기여야 하고, Py_ssize_t는 size_t와 같은 크기여야 합니다. 포인터에 세그먼트와 오프셋이 있는 시스템 등에서는 이 크기가 서로 다를 수 있습니다. 현재의 플랫 주소 공간 시스템에서는 차이가 없으므로, 실질적인 목적에서는 Py_intptr_t를 사용해도 동일하게 작동했을 것입니다.
이로 인해 많은 코드가 손상되지 않습니까?
제안된 변경 사항을 적용하면 코드 손상은 상당히 적습니다. 32비트 시스템에서는 Py_ssize_t가 int에 대한 typedef일 뿐이므로 코드가 손상되지 않습니다.
64비트 시스템에서는 컴파일러가 여러 곳에서 경고를 표시합니다. 이러한 경고를 무시하면 컨테이너 크기가 2**31을 초과하지 않는 한 코드가 계속 작동하며, 즉 현재와 거의 동일하게 작동합니다. 이 설명에는 두 가지 예외가 있습니다. 확장 모듈이 시퀀스 프로토콜을 구현하는 경우에는 업데이트해야 하며, 그렇지 않으면 호출 규약이 잘못됩니다. 다른 예외는 Py_ssize_t가 반환값이 아니라 포인터를 통해 출력되는 부분이며, 이는 특히 코덱과 슬라이스 객체에 적용됩니다.
코드를 변환하면 동일한 코드가 이전 Python 릴리스에서도 계속 작동할 수 있습니다.
이로 인해 메모리를 너무 많이 사용하지 않습니까?
모든 튜플, 문자열, 리스트 등에 Py_ssize_t를 사용하는 것이 공간 낭비라고 생각할 수도 있습니다. 하지만 이는 사실이 아닙니다. 32비트 시스템에서는 변경 사항이 없습니다. 64비트 시스템에서도 많은 컨테이너의 크기는 변경되지 않습니다. 예를 들면 다음과 같습니다.
- 리스트와 튜플에서는 ob_size 멤버 바로 뒤에 포인터가 옵니다. 이는 현재 컴파일러가 4개의 패딩 바이트를 삽입한다는 의미이며, 변경 후에는 이 패딩 바이트가 크기의 일부가 됩니다.
- 문자열에서는 ob_shash 필드가 ob_size 뒤에 옵니다. 이 필드는 대부분의 64비트 시스템(Win64 제외)에서 64비트 타입인 long 타입이므로, 컴파일러는 이 필드 앞에도 패딩을 삽입합니다.
미해결 이슈
- Marc-Andre Lemburg은 기존 소스 코드와의 완전한 하위 호환성이 유지되어야 한다고 언급했습니다. 특히, Py_ssize_t* 출력 인자를 갖는 함수는 호출자가 int*를 전달하더라도 계속 올바르게 동작해야 합니다.
이 요구 사항을 구현하는 데 어떤 전략을 사용할 수 있을지는 명확하지 않습니다.
Copyright
This document has been placed in the public domain.