이 글은 PEP703을 읽고 정리 한 글입니다.
CPython의 GIL은 여러 스레드가 동시에 Python 코드를 실행하는 것을 방지하는데, 이로 인해 멀티코어 CPU를 효율적으로 활용하지 못하는 문제가 발생합니다. 이에 대한 대안으로 --disable-gil 빌드 구성을 제안하고, GIL 없이 Python 코드를 실행하며 인터프리터를 스레드 안전하게 만드는 내용을 소개합니다.
GIL은 CPython(파이썬의 표준 구현체)에서 사용되는 메커니즘으로, 여러 스레드가 동시에 Python 코드를 실행하는 것을 제한하는 역할을 합니다. 이는 CPython이 안전하게 다중 스레딩 환경에서 동작하도록 하는 중요한 제어 장치입니다
GIL 때문에 하나의 스레드만이 순수한 Python 코드에 접근할 수 있습니다. 이로 인해 멀티코어 CPU를 사용하여 성능을 향상시키는 것이 어려워지며, GIL이 없는 경우에 비로소 여러 스레드가 동시에 실행될 수 있습니다.
GIL은 Python의 객체에 대한 참조 카운트를 관리함으로써 스레드 간 충돌을 방지합니다. 객체의 참조 카운트는 레퍼런스 여부에 따라 증가하거나 감소하며, 이를 통해 객체의 메모리 할당 및 해제를 관리합니다. GIL은 이러한 참조 카운트를 원자적으로 증가/감소시키는 데 사용됩니다.
GIL을 제거하기 위해서는 참조 카운팅 구현을 변경해야 합니다. 세 가지 구현 방법을 소개합니다.
특정 객체들을 해제되지 않도록 설정하여 참조 카운트 관리를 피하고자 합니다. 예를 들어, true
, false
, none
, small integer(pre allocated
, string(empty, signle character strings
)들이 deacllocate되지 않는것.
참조 카운트 갱신의 지연: 지연 참조 카운팅은 참조 카운트 갱신을 즉시 수행하는 대신, 일정 시간 동안 참조가 변경되지 않으면 한꺼번에 갱신하는 방식을 채택합니다. 이는 특정 시점에 여러 참조 카운트를 동시에 갱신함으로써 오버헤드를 줄이는 것을 목표로 합니다.
트랜잭션 기반 갱신: 지연 참조 카운팅은 트랜잭션(Transaction)과 비슷한 개념을 사용하여 여러 참조 카운트 갱신을 하나의 그룹으로 묶습니다. 특정 시점에서 이 그룹에 속한 모든 참조 카운트를 한 번에 갱신하는 것이 트랜잭션 기반의 갱신입니다.
비동기적 갱신: 참조 카운트의 변경이 비동기적으로 이루어지며, 일정 시간 동안 참조가 변경되지 않으면 해당 시점에서 비동기적으로 갱신이 수행됩니다.
성능 향상: 참조 카운트를 즉시 갱신하는 대신 일정 시간 동안 변경이 없을 경우에만 갱신하므로 오버헤드가 감소하고 성능이 향상됩니다.
트랜잭션 기반 갱신: 트랜잭션 기반의 갱신은 여러 개의 참조 카운트를 한 번에 처리함으로써 일괄 처리의 이점을 제공합니다.
비동기적 갱신: 참조 카운트 갱신이 비동기적으로 이루어지므로, 작업이 블록되지 않고 계속 진행될 수 있습니다.
정확성 유지: 지연 참조 카운팅은 성능 향상을 위해 일정 시간 동안 변경이 없을 경우에만 갱신을 수행하지만, 이로 인해 어느 정도의 정확성 손실이 발생할 수 있습니다. 이는 정확성과 성능 사이의 트레이드오프입니다.
병목 현상: 특정 시점에서 갱신이 수행될 때 오버헤드가 발생할 수 있으며, 이로 인해 병목 현상이 발생할 가능성이 있습니다.
Biased Reference Counting은 참조 카운팅을 특별하게 처리하여 객체의 소유자 스레드에서만 참조 카운트를 증가/감소시켜 경합을 줄이고 처리를 빠르게 하는 기술입니다. 이는 GIL(Globally Interpreter Lock) 없이 참조 카운팅을 보다 효율적으로 관리하기 위한 시도 중 하나입니다.
Biasing (편향): Biased Reference Counting은 객체의 소유자 스레드에 편향됩니다. 즉, 객체를 가지고 있는 스레드에서만 참조 카운트를 증가/감소시킵니다.
경합의 감소: 특정 객체에 대한 작업이 해당 객체를 가지고 있는 스레드에서만 일어나므로 경합(competition)이 줄어들어 더 빠른 처리가 가능합니다.
참조 카운트 증가: 객체를 가지고 있는 스레드에서 해당 객체에 접근하여 참조 카운트를 증가시킵니다. 이는 기존의 참조 카운팅과 동일합니다.
참조 카운트 감소: 객체를 가지고 있는 스레드에서만 참조 카운트를 감소시킬 수 있습니다. 이는 경합을 피하기 위한 특징으로, 다른 스레드에서 참조 카운트를 감소하려면 추가적인 처리가 필요합니다.
객체에 다른 스레드에서 접근하는 경우: 객체에 접근하는 스레드가 객체를 가지고 있지 않은 경우에는 특별한 처리가 필요합니다. 이때는 다른 스레드 또는 글로벌에서 참조 카운트를 감소시켜야 합니다.
경합 감소: Biased Reference Counting은 특정 객체에 대한 작업이 소유자 스레드에서만 발생하므로 경합을 감소시킵니다.
성능 향상: 경합의 감소로 인해 참조 카운팅 관련 작업이 빠르게 처리될 수 있어 성능 향상을 기대할 수 있습니다.
다른 스레드에서의 처리: 객체에 접근하는 스레드가 해당 객체의 소유자가 아닌 경우, 추가적인 처리가 필요하므로 이에 대한 고려가 필요합니다.
Biased Reference Counting은 참조 카운트 기반의 메모리 관리에서 경합을 효과적으로 해결하기 위한 현대적인 기술 중 하나로, 파이썬의 성능 향상을 위한 연구와 개선의 한 예시입니다.
세 가지 방법을 사용하는 이유는, 단일한 접근 방식이 모든 상황에서 최적이 되지 않기 때문입니다. 파이썬의 다양한 사용 사례와 환경에서 최적의 성능을 얻기 위해 다양한 시도와 실험이 필요하며, 이를 통해 참조 카운팅을 GIL 없이도 효율적으로 관리할 수 있도록 노력하고 있습니다
가비지 컬렉션은 메모리에서 더 이상 사용되지 않는 객체들을 해제하여 자원을 관리하는 프로세스입니다. 그 중 Cycle Collection은 모든 객체에 대한 참조 카운트를 정확하게 파악해야 하는데, 이 작업을 수행하려면 다음과 같은 고려사항이 있습니다.
컨테이너의 쓰레드 안전성: 일부 컨테이너 연산은 쓰레드 안전성을 보장하지 않습니다. 특히, 리스트를 확장하거나 리스트에서 집합을 구성하는 등의 작업은 GIL이 해당 스레드의 안전성을 보장하지만, 여러 스레드 간에 안전성을 제공하지 않을 수 있습니다.
락의 사용: GIL이 안전성을 보장하도록 하지만, 락(lock)이 사용되어야 하는 경우 성능 저하가 발생할 수 있습니다. 이는 락을 사용하여 여러 스레드 간에 동기화를 유지하려는 시도로 인한 것입니다.
성능 저하: 특히, 단일 스레드로 작성된 코드에서는 GIL이 없어짐에 따라 5% 정도의 성능 저하가 예상됩니다. 이는 Flask, FastAPI 등이 싱글 스레드로 작성되었으며 문서에서도 이에 대한 언급이 있습니다.
코드의 쓰레드 안전성 보장: 쓰레드 안전성이 보장되지 않은 코드는 수정되어 안전하게 동작하도록 변경되어야 합니다. 여러 스레드가 동시에 접근할 때 발생할 수 있는 경합 조건 등에 대한 고려가 필요합니다.
멀티 프로세스 또는 멀티 스레딩 코드 변경: GIL을 없애면서 멀티 프로세스 또는 멀티 스레딩 코드도 변경이 필요할 수 있습니다. 코드가 여러 스레드 간에 안전하게 동작하도록 보장해야 합니다.
이러한 측면들을 고려하면서 GIL을 없애고 참조 카운팅을 효율적으로 관리하기 위해 다양한 방법과 기술이 도입되고 있습니다.
잘 읽고 갑니다 :)
Pi Malik -> pymalloc의 오타 같습니다!