Python, 아주 강력한 인터프리터 언어다. 아주 다양한 곳에 사용되고, Django, FastAPI, Flask를 비롯한 강력한 웹 프레임워크들도 있다. 하지만 한가지 걸리는 점이 있었으니, 바로 이전부터 제기되어왔던 속도 문제였다. 다른 언어와 비교해서 파이썬은 속도에 대해서 약점을 가지고 있다. 물론 태생적인 한계일수도 있다. 아래의 표를 통해 파이썬의 빠르기를 알 수 있을 것이다.
파이썬 3.11의 주된 변경점
이번 Faster CPython(더 빠른 CPython) 방법은 파이썬 특히 CPython의 실행 속도를 높이기 위해 만들어진 프로젝트다. 특히 이번 업데이트에서는 많은 병견점이 있는데 다음과 같은 변경점이 있다.
- 적응형 전문 인터프리터 (PEP 659)
- 연속적으로 할당된 실행 프레임들
- Zero cost try-except
- 더욱 정형화된 오브젝트 레이아웃
- 레이지하게 생성하는 객체 Dict
메모리 접근은 비싸다
- Arithmetic operation: 1 cycle
- L1 cache latency: ~4 cycles
- L2 cache latency: ~10 cycles
- L3 cache latency: ~30 cycles
- RAM latency: ~200+ cycles
5Ghz CPU에서 위와 같은 메모리 접근성을 보인다. 물리적인 접근은 L1, L2, L3 그리고 RAM으로 갈수록 소통이 비싸진다. 즉, 어느 프로그램이든 메모리 접근이 있으면 물리적인 한계로 인해 프로그램은 느려질 수 밖에 없다. 만약 다른 프로그램이 특정 메모리 주소를 접근하려고 할 때, 다른 프로그램이 그 메모리 주소를 점유하고 있다면 더욱더 느려질 수 밖에 없을 것이다.
파이썬의 내부 데이터 구조 실행방식
Linked List vs Array List
Linked List와 Array를 잠시 보고 가겠다. 만약 우리가 2번째 요소를 읽겠다고 하면 Linked List의 경우는 Head에서 2번째 요소까지 4번 메모리 접근을 해야하는 악재가 따른다. 하지만 Array의 경우는 두번의 메모리 접근(즉, Head에서 Array로 그리고 Array에서 각 요소로 한번에 접근)할 수 있다. 메모리 접근에서는 이득을 보인다. 만약 메모리를 더 할당하려면 Linked List는 메모리를 더 할당해야할 것이다. 따라서 이러한 의존성과 메모리 접근을 최대한 피하기 위한 프로그래밍 언어 설계는 힘든 편이기도 하다.
여기서 다른 이야기인 Frame 스택에 대해 이야기하고 넘어가야 할 것 같다. 이 프레임 스택은 파이썬 함수를 부를 때 사용되는 객체이다. 각각의 파이썬 함수를 부를 때 마다, Frame Object를 스택에 넣는다. 이때 이 프레임은 지역 변수, 임시 값을 위한 공간, 이전 프레임, 전역 변수 그외 등등을 위한 참조, 그리고 디버깅 정보를 가지고 있다.
파이썬 3.10 그이하 버전
위에서 설명했다시피 파이썬 프레임은 Linked List로 연결되어 있었고, 이는 Top에 있는 스택만 가져와서 부르기에는 편했다. 하지만 다른 스택을 부르기에는 추가적인 비용도 들고 새로운 공간 할당을 위해 공간을 비워둬야 하는 등 위에서 말한 메모리 접근 측면에서 굉장히 비용을 치루게 된다. 3.10이하 에서는 이러한 공간 할당을 위한 Caching이 없었고, 그리고 있었다 하더라도 그렇게 효과적이지 않았다. 실제로 3.10이하에서는 링크 스택을 위해 많은 메모리 청크가 스레드 마다 할당되어 있었고, 만약 새로운 프레임이 생성될 경우 기존 메모리를 참고해야하는 경우가 생긴다. 이는 곧 L1같은 빠른 메모리에서, L2, L3, 그리고 RAM같은 느린 메모리로 접근할 때 많은 대가를 치루게 된다.
파이썬 3.11
그러한 사항을 파이썬 3.11에서는 아주 큰 메모리 할당을 통해 해결하고자 했다. 일단 미리 큰 메모리를 할당해놓는데, 얼마나 큰 Frame Stack을 얻게 될지 몰라서이다. 그리고 메모리에서 새로운 할당보다는, 재사용을 통해 메모리 활용성을 높이고자 했다. 그리고 이러한 Frame 객체를 재활용할 때, 파이썬 3.11에서 프레임 객체는 느리게(Lazily) 생성된다. 이는 곧 더 적은 메모리를 요구한다. 물론 이러한 Lazily 생성은 무조건 모든 케이스에 맞는 것이 아니지만, 이러한 경우는 적기에 3.10과 비교했을 때 굉장히 큰 이득을 얻게된다.
그래서 3.10 이하와 비교했을 때 3.11은 두가지 변경점을 가진다. 먼저 디버깅 정보가 느리게 생성된다. 왜냐하면 이는 기본적으로 Frame Stack의 직접적인 파트가 아니기 때문이다. 그리고 Exception 스택이 버려졌다. 또한 네임 스페이스 Dict이 키값을 가능할 때마다 공유한다.
Zero Cost Exceptions
위의 그림을 보다보면, Exception 스택이 없어진 것을 확인할 수 있다. 이는 메모리 절약을 위해 사용된다. 그 이유를 알기 위해 Exception이 어떻게 활용되는지를 보면 된다.
3.10에서, try-except는 바이트코드에 명시적으로 구현되어 있다.
- Try는 내부 스택으로 자그마한 데이터를 넣고, 시스템이 예외 처리를 위해 어디로 가야할지, 그리고 얼마나 많은 실행 스택을 사출해야할지를 이야기해준다.
- 이는 160바이트를 매 프레임 객체마다 소모하며, 3.10 미만의 버전에서는 240바이트나 소모한다.
- 실제로 21개의 try, except를 Nested하게 짜면 21 x 160바이트로 메모리 이슈가 나타난다.
3.11에서 정보는 테이블에 저장된다.
- 예외가 발생하지 않는 한, 아무것도 실행되지 않는다
- 예외 발생시, 오프셋과 스택의 깊이는 테이블에서 조회된다.
꽤나 Zero, 하지만 완벽하지 않은 Zero
- 코드 객체의 크기를 조금 늘리게 되었다.
- 그리고 예외가 발생할 때 조금 느려졌다.
참고 자료
'Python > ETC' 카테고리의 다른 글
[공지] 파이썬 웹 프레임워크 정리 페이지 프로젝트 (0) | 2023.04.26 |
---|---|
[번역] 장고(Django)와 플라스크(Flask) : 2023년 어떤 것을 선택해야 하는가? 1편 (0) | 2023.04.20 |
[파이썬 기본 다지기] Python에서 변수를 Print할 때 쓰는 방법들 (0) | 2023.04.15 |
[파이썬 기본 다지기] 파이썬에서 print("Hello, world")는 어떻게 나오는 걸까? 파이썬의 실행 방식 간략하게 알아보기 (0) | 2023.04.06 |
[EuroPython] 어떻게 Python 3.11은 빨라질 수 있었을까? 2편 - dictionary (0) | 2023.01.27 |