Categories
[Python] 파이썬의 성능적 한계, GIL
핵심 요약
GIL (Global Interpreter Lock) : 여러 쓰레드가 아닌 오직 하나의 쓰레드만 단일 파이썬 명령어를 제어할 수 있도록 한 기술, 동적 타입 언어인 파이썬에서 생길 수 있는 메모리 관리 문제 (race condition)를 해결하고 안정성을 개선하고자 한 기술이다.
오늘 시험이 끝나 하루 정도 그냥 내가 하고 싶은거 하기 위해 다른 관심있는 책을 골라 읽었다.
미샤 고렐릭, 이안 오스발트가 지은 “고성능 파이썬”이라는 책인데, 파이썬의 성능을 어떻게 하면 더 높일 수 있을까에 대해 적혀진 책이다.
읽고 나서 머리가 지끈지끈한데 면접때 나올 수도 있는 질문 같아서 정리는 해둬야겠다고 생각했다.
이 책의 1장을 읽고 얻은 정보들을 바탕으로 정리해보았다.
GIL이 무엇이고, 왜 GIL이 필요한 것인지 작성하기 위해 기본 개념들부터 정리해보았다.
1. 동적 타입 (Dynamic typing) 언어
파이썬은 동적 타입 언어다. 즉 사용자가 직접 자료형 (data type)을 코드에다가 지정해두지 않아도 파이썬은 실행 시 (컴파일 시 자료형을 정하는 것이 아님)에 그 자료형이 무엇인지 알아서 결정한다. 당장 아래 코드를 봐도 파이썬 사용이 편리해보인다.
// c
int num = 1;
float num2 = 2.123;
char str1 = "abc";
# python
num = 1
num2 = 2.123
str1 = "abc"
하지만 동적 타입 언어도 단점이 있다. 이렇게 파이썬은 컴파일 시 자료형을 정하는 것이 아닌, 실제 코드가 작동하는 중에 자료형이 어떤 타입인지 결정하기 때문에, 모든 변수에 대해 이러한 자료형의 유형을 일일이 체크해주어야 한다. 성능이 떨어질 수 밖에 없다.
2. Garbage Collection (GC)
한편, 그때그때 코드를 읽을 때마다 들어오는 자료형의 타입에 따라 필요한 메모리는 달라진다. 필요한 메모리가 계속해서 달라지는 것이 그렇게 안정적이진 않고, 따라서 파이썬은, 더 효율적인 메모리관리를 위해 Garbage Collection (GC)을 사용한다. 정적 타입 언어에서는 컴파일이 진행되는 동안 변수의 타입을 미리 결정하고, 메모리의 할당 및 해제를 명시적으로 제어할 수 있기 때문에 이러한 GC는 필요가 없다.
GC 방식 중 하나는 Reference counting이다. 다음 코드를 보자.
a = [] # 리스트라는 개체를 생성, 참조 카운트 1
b = a # b가 a라는 메모리 주소를 참조, 참조 카운트는 2
del a # a 삭제, 참조 카운트는 1
del b # b 삭제, 참조 카운트는 0 => 이 코드가 읽혀질 때 a의 메모리 공간 할당 해제
즉 간단하게 GC는 프로그램이 동적으로 할당했던 메모리 영역중에서 필요없게 된 영역을 해제하는 기능이다. 메모리 관리가 수월해질 것이다 (하지만 이는 계속해서 임의의 메모리공간들이 할당되고 해제됨에 따라 결과적으로 RAM 안의 메모리 공간이 작은 공간으로 나뉘어지는 메모리 단편화가 발생하게 될수도 있고, 그러면 결국에 데이터가 작게작게 쪼개지기 때문에, RAM안의 데이터를 연산장치인 CPU에 옮겨 연산을 요청하려고 할 때에는, 데이터를 한번에 모두 보낼 수 없고 따로따로 하나씩 보내야 하기 때문에 파이썬의 속도가 더 느려지게 된다).
3. Race condition => reference counting의 문제점
Reference counting은 멀티쓰레딩 (하나의 파이썬 코드를 여러 논리프로세서가 함께 연산하려고 시도)시 한계가 있다. 아래 논리 코드를 보자.
# 이 코드는 파이썬 코드가 아닌 단순한 논리 코드이다.
a = 0
# 카운터를 증가시키는 함수
def increment():
for i in range(1000):
a += 1
increment()
위 코드는 단순히 카운터의 값을 1000만큼 증가시켜주는 하나의 명령 코드이다. reference counting을 해보면 결과적으로 a에는 1의 값이 들어가게 되고, 컴퓨터에는 a의 자료형에 해당하는 메모리 공간만큼이 할당되게 된다.
그런데 만약, 위 코드를 하나의 프로세서가 아닌 두개의 프로세서가 돌리게 된다면 어떻게 될까?
일어날 수 있는 가능성으론, 만약 A 프로세서가 a라는 메모리 공간을 참조해서, a 변수의 reference count를 1증가 시켰다고 해보자. 그 다음 B 프로세서도 위 명령어를 수행하기 위해 해당 a 변수의 메모리를 얘기치 않게 건드려 reference count를 1 증가시켰다. 하나의 명령어만 봤을 때는 최종 reference count가 1이 되었어야 할 a 변수가, 두 개의 프로세서의 경쟁으로 인해 결과적으로는 reference count = 2라는 상황이 발생하게 된다. 메모리 누수 (memory leak) 현상이다.
즉 하나의 공유된 자원에 대해 여러 프로세스가 동시에 접근을 시도하는 race condition이 발생하게 된다.
4. 성능보다 메모리 관리의 안정성을 택하기 위한 GIL
따라서 결과적으로 GIL (Global Interpreter Lock)은, 상기 문제점을 해결하기 위해, 여러 쓰레드가 아닌 오직 하나의 쓰레드만 단일 파이썬 명령어를 제어할 수 있도록 한 기술이다.
나는 “왜 하필 GIL이라는 기술을 만들어서 일하는 사람들을 힘들게 만드는지” 궁금했는데, 다 이유들이 있었던 것이다. 성능보다는 메모리 관리의 안정성을 선택한 것이다.
책에서 이러한 GIL을 회피할 수 있는 기술들이 적혀있는데, 다음에.. 다시 읽을 기회가 있을 때 추가적으로 적어보려고 한다.