PEP 3146 – Unladen Swallow를 CPython에 병합하기
- Author:
- Collin Winter <collinwinter at google.com>, Jeffrey Yasskin <jyasskin at google.com>, Reid Kleckner <rnk at mit.edu>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 01-Jan-2010
- Python-Version:
- 3.3
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
PEP 철회
Unladen Swallow가 노르웨이안 블루 [1] [2]와 같은 운명을 맞게 됨에 따라, 이 PEP는 철회된 것으로 간주됩니다.
초록
이 PEP는 Unladen Swallow 프로젝트 [3]를 CPython의 소스 트리에 병합할 것을 제안합니다. Unladen Swallow는 성능에 초점을 맞춘 CPython의 오픈 소스 브랜치입니다. Unladen Swallow는 유효한 Python 2.6.4 애플리케이션 및 C 확장 모듈과 소스 호환됩니다.
Unladen Swallow는 CPython에 JIT(just-in-time) 컴파일러를 추가하여, 선택된 Python 코드를 최적화된 기계어 코드로 컴파일할 수 있게 합니다. Unladen Swallow의 JIT 컴파일러는 고전적인 정적 컴파일러 최적화를 넘어, 런타임에 수집한 데이터를 활용하여 코드 동작에 대해 검증된 가정을 세움으로써 더 빠른 기계어 코드를 생성할 수 있게 합니다.
이 PEP는 Unladen Swallow를 별도의 py3k-jit 브랜치로 CPython의 개발 트리에 통합하고, 최종적으로 메인 py3k 브랜치에 병합하는 것을 목표로 할 것을 제안합니다. Unladen Swallow는 결코 완성되었거나 완벽하지 않지만, 저희는 Unladen Swallow가 CPython의 로드맵에 포함될 만큼 충분히 성숙했다고 생각합니다. 저희는 더 넓은 CPython 개발 팀이 그 위에 구축해 나갈 수 있는 안정적인 플랫폼, 앞으로 수년간 점점 더 높은 성능을 가져다줄 플랫폼을 만들고자 노력했습니다.
이 PEP에서는 Unladen Swallow의 구현과 그것이 CPython 2.6.4와 어떻게 다른지, 성능 측정에 사용한 벤치마크, 정확성과 호환성을 보장하기 위해 사용한 도구, CPython의 현재 플랫폼 지원에 미치는 영향, 그리고 CPython 코어 개발 프로세스에 미치는 영향을 자세히 설명합니다. 이 PEP는 제안하는 병합 계획과 향후 작업의 가능한 방향에 대한 간략한 메모로 마무리됩니다.
저희는 BDFL에게 다음을 요청합니다:
- 아래에 제시된 설계에 따라 CPython에 JIT 컴파일러를 추가한다는 전반적인 개념에 대한 승인.
- CPython 소스 트리에서 JIT 컴파일러 작업을 계속할 수 있는 허가.
- 모든 차단 이슈 [31]가 해결되면 최종적으로 JIT 컴파일러를
py3k브랜치에 병합할 수 있는 허가. - 조랑말 한 마리.
근거, 구현
많은 기업과 개인은 Python이 더 많은 프로젝트에서 사용될 수 있도록 더 빨라지기를 바랍니다. Google도 그러한 기업 중 하나입니다.
Unladen Swallow는 Google의 수많은 Python 라이브러리, 도구, 애플리케이션의 성능을 개선하기 위해 시작된, Google이 후원하는 CPython 브랜치입니다. Unladen Swallow를 최대한 쉽게 도입할 수 있도록, 이 프로젝트는 처음에 네 가지 목표를 세웠습니다:
- 단일 스레드 코드에서 CPython 2.6.4 기준 대비 5배의 성능 향상.
- 유효한 CPython 2.6 애플리케이션과의 100% 소스 호환성.
- 유효한 CPython 2.6 C 확장 모듈과의 100% 소스 호환성.
- 최종적으로 CPython에 다시 병합하는 것을 염두에 둔 설계.
저희가 2.6.4를 기준으로 선택한 이유는 Google이 내부적으로 CPython 2.4를 사용하고 있으며, CPython 2.4에서 CPython 3.x로 곧바로 넘어가는 것은 실현 불가능하다고 판단했기 때문입니다.
원하는 성능을 달성하기 위해, Unladen Swallow는 Self에 관한 Urs Hoelzle의 연구 [52]의 전통을 따라 런타임에 피드백을 수집하고 이를 컴파일 타임 최적화에 활용하는 JIT(just-in-time) 컴파일러 [51]를 구현했습니다. 이는 현세대 JavaScript 엔진 [59], [60], 대부분의 Java 가상 머신 [63], Rubinius [61], MacRuby [62] 및 기타 Ruby 구현체, Psyco [64] 등이 취하는 접근 방식과 유사합니다.
저희는 저희의 아이디어가 독창적이라는 어떠한 주장도 명시적으로 거부합니다. 저희는 가능한 모든 곳에서 다른 연구자들이 발표한 연구 성과를 재사용하려고 노력했습니다. 저희가 독창적인 작업을 한 것이 있다면, 그것은 우연입니다. 저희는 학계와 산업계 곳곳에서 좋은 아이디어를 최대한 가져오려고 노력했습니다. Unladen Swallow에 영향을 준 연구 논문의 일부 목록은 Unladen Swallow 위키 [54]에서 볼 수 있습니다.
동적 언어 최적화에 관한 핵심적인 관찰은 동적 언어가 이론상으로만 동적이라는 것입니다. 실제로는 각각의 함수나 코드 조각이 안정적인 타입과 하위 함수 집합을 사용하므로 비교적 정적입니다. 현재 CPython 바이트코드 인터프리터는 실행 중인 코드에 대해 최악의 상황을 가정합니다. 즉, 사용자가 언제든지 len() 함수를 재정의하거나 한 번도 본 적 없는 타입을 함수에 전달할 수 있다고 가정합니다. 실제로는 이런 일이 결코 일어나지 않지만, 사용자 코드는 그러한 지원에 대한 비용을 치릅니다. Unladen Swallow는 사용자 코드의 비교적 정적인 특성을 활용하여 성능을 향상합니다.
개략적으로 말하면, Unladen Swallow JIT 컴파일러는 함수의 CPython 바이트코드를 플랫폼별 기계어 코드로 번역하는 방식으로 동작하며, 생성된 기계어 코드의 품질을 높이기 위해 런타임에 수집한 데이터와 고전적인 컴파일러 최적화를 함께 사용합니다. 프로그램의 런타임에 실제로 이득이 될 Python 코드를 컴파일하는 데에만 자원을 쓰고자 하므로, 주어진 함수가 얼마나 핫한지 평가하는 데 온라인 휴리스틱을 사용합니다. 함수의 핫니스(hotness) 값이 주어진 임계값을 넘으면, 그 함수는 컴파일 및 최적화 대상으로 선택됩니다. 그러나 함수가 핫하다고 판정되기 전까지는 표준 CPython eval 루프에서 실행되며, Unladen Swallow에서는 이 루프가 실행되는 각 바이트코드에 대한 유용한 데이터를 기록하도록 계측되어 있습니다. 이 런타임 데이터는 생성된 기계어 코드의 유연성을 줄이는 데 사용되며, 이를 통해 일반적인 경우에 맞춰 최적화할 수 있습니다. 예를 들어, 저희는 다음과 같은 데이터를 수집합니다.
- 브랜치가 실행되었는지/실행되지 않았는지. 브랜치가 한 번도 실행되지 않으면, 그 브랜치는 기계어 코드로 컴파일하지 않습니다.
- 연산자가 사용하는 타입.
a + b가 항상 정수만 더한다는 것을 발견하면, 해당 코드 조각에 대해 생성된 기계어 코드는 부동소수점 수의 덧셈을 지원하지 않습니다. - 각 호출 지점에서 호출되는 함수. 특정
foo()호출 지점이 항상 같은foo함수를 호출한다는 것을 발견하면, 그 호출을 최적화하거나 인라인 처리하여 제거할 수 있습니다
수집하는 데이터 항목의 전체 목록과 그 사용 방법은 [55]에서 확인하십시오.
그러나 우연히 지금까지 실행되지 않던 브랜치가 이제 실행되거나, 정수에 최적화된 a + b 코드 조각이 문자열 두 개를 받는다면, 이를 지원해야 합니다. 저희는 Python의 의미론을 바꿀 수 없습니다. 최적화된 기계어 코드의 각 구간 앞에는 guard가 있으며, 이 가드는 최적화할 때 저희가 세운 단순화 가정이 여전히 성립하는지 확인합니다. 가정이 여전히 유효하면 최적화된 기계어 코드를 실행하고, 그렇지 않으면 인터프리터로 되돌아가 중단했던 지점부터 이어서 실행합니다.
저희는 코드 생성과 코드 최적화를 위해 LLVM [4]이라는 기존 컴파일러 라이브러리 모음을 재사용하기로 했습니다. 덕분에 저희의 작은 팀은 여러 기계 명령어 집합에서의 코드 생성을 이해하고 디버깅해야 하는 부담과, 수많은 고전적인 컴파일러 최적화를 구현해야 하는 부담을 덜 수 있었습니다. 이러한 코드 재사용이 없었다면 이 프로젝트는 불가능했을 것입니다. 저희가 경험한 바로는 LLVM은 수정하기 쉬웠고, 그 커뮤니티는 저희의 제안과 수정 사항을 기꺼이 받아들였습니다.
조금 더 자세히 설명하면, Unladen Swallow의 JIT는 CPython eval 루프에서 얻은 모든 런타임 데이터를 고려하면서 CPython 바이트코드를 LLVM 고유의 중간 표현(IR) [94]로 컴파일하는 방식으로 동작합니다. 그런 다음 LLVM에 내장된 일련의 최적화 패스를 실행하여, 원래 LLVM IR보다 더 작고 최적화된 버전을 만들어 냅니다. 이어서 LLVM은 레지스터 할당, 명령어 스케줄링, 필요한 모든 재배치를 수행하면서 IR을 플랫폼별 기계어 코드로 낮춥니다. 컴파일 파이프라인이 이렇게 구성되어 있으므로 ./configure에 --without-llvm을 전달하기만 하면 컴파일된 python 바이너리에서 LLVM 기반 JIT를 쉽게 제외할 수 있습니다. 이 플래그의 다양한 사용 사례는 뒤에서 다룹니다.
Unladen Swallow의 동작 방식에 대한 자세한 설명은 Unladen Swallow 문서 [53], [55]를 참고하십시오.
Unladen Swallow는 단일 스레드 순수 Python 코드의 성능을 개선하는 데 집중해 왔습니다. 저희는 CPython의 전역 인터프리터 잠금(GIL)을 제거하려고 시도하지 않았습니다. 이 작업은 저희 작업과 별개이며, 민감한 사안이므로 메인라인 개발 브랜치에서 진행하는 것이 가장 좋다고 생각합니다. GIL 제거를 Unladen Swallow의 일부로 포함하는 방안도 검토했지만, 저희 작업을 CPython 2.6에서 3.x로 이식하는 과정에서 미묘한 버그가 생길 가능성을 우려했습니다.
JIT 컴파일러는 매우 다재다능한 도구이며, 저희는 그 잠재력을 결코 모두 끌어내지 못했습니다. 저희는 더 넓은 CPython 개발 커뮤니티가 앞으로 수년간 그 위에 작업을 쌓아 가며 릴리스마다 성능을 더 끌어올릴 수 있을 만큼 충분히 유연한 프레임워크를 만들고자 노력했습니다.
대안
저희는 Python 성능을 개선하기 위한 여러 대안 전략을 검토했지만, 만족스러운 것은 없었습니다.
- Cython, Shedskin: Cython [101]과 Shedskin [102]은 모두 Python용 정적 컴파일러입니다. 저희는 이들을 오랫동안 좋지 않았던 CPython 성능을 보완하는, 유용하지만 한계가 있는 우회책으로 봅니다. Shedskin은 Python 표준 라이브러리 전체를 지원하지 않으며 [103], Cython은 최적의 성능을 내려면 Cython 전용 어노테이션을 직접 달아야 합니다.
이러한 정적 컴파일러는 참조 카운팅을 신경 쓰지 않고 확장 모듈을 작성하는 데 유용하지만, 정적인 사전(ahead-of-time) 컴파일러이므로 런타임 데이터를 활용하는 JIT 컴파일러가 다룰 수 있는 모든 범위의 코드를 최적화하지는 못합니다.
- IronPython: IronPython [106]은 Microsoft의 .Net 플랫폼에서 동작하는 Python입니다. Mono [107]에서는 활발하게 테스트되지 않으므로 사실상 Windows 전용이며, 따라서 범용 CPython 대체재로는 적합하지 않습니다.
- Jython: Jython [108]은 Python 2.5의 완전한 구현이지만, Unladen Swallow보다 훨씬 느리고(측정한 벤치마크 기준 3~5배) CPython 확장 모듈을 지원하지 않습니다 [109]. 그래서 대규모 애플리케이션을 이전하는 데 감당하기 어려울 만큼 큰 비용이 듭니다.
- Psyco: Psyco [64]는 확장 모듈로 구현된 CPython용 특수화 JIT 컴파일러입니다. 주로 수치 계산 코드의 성능을 개선합니다. 장점: 이미 존재합니다. 일부 코드를 더 빠르게 만듭니다. 단점: 32비트 전용이며 64비트 지원 계획이 없습니다. x86만 지원합니다. 유지 보수가 매우 어렵습니다. 정렬 문제 때문에 SSE2로 최적화된 코드와 호환되지 않습니다.
- PyPy: PyPy [65]는 수치 계산 코드에서 좋은 성능을 보이지만, 일부 워크로드에서는 Unladen Swallow보다 느립니다. 대규모 애플리케이션을 CPython에서 PyPy로 이전하려면 감당하기 어려울 만큼 큰 비용이 듭니다. PyPy의 JIT 컴파일러는 32비트 x86 코드 생성만 지원하고, MySQLdb나 pycrypto 같은 중요한 모듈은 PyPy에서 빌드되지 않으며, PyPy는 CPython과 같은 API는커녕 임베딩 API조차 제공하지 않습니다.
- PyV8: PyV8 [110]은 V8 위에서 동작하는 알파 단계의 실험적인 Python-JavaScript 컴파일러입니다. PyV8은 Python 언어 전체를 구현하지 않으며, CPython 확장 모듈도 지원하지 않습니다.
- WPython: WPython [104]은 CPython의 인터프리터 루프를 워드코드 기반으로 다시 구현한 것입니다. 인터프리터 성능을 어느 정도 개선하기는 하지만 [105], JIT 컴파일러를 대신할 수 있는 양자택일의 대안은 아닙니다. 인터프리터는 결코 최적화된 기계어 코드만큼 빨라질 수 없습니다. 저희는 WPython과 이와 비슷한 인터프리터 개선 작업을 경쟁자가 아니라 저희 작업을 보완하는 것으로 봅니다.
성능
벤치마크
Unladen Swallow는 단일 기능을 테스트하도록 설계된 합성 마이크로벤치마크부터 애플리케이션 전체를 대상으로 하는 매크로벤치마크에 이르기까지 상당히 큰 벤치마크 모음을 개발했습니다. 이 벤치마크들은 서드파티 기여자(html5lib 벤치마크의 경우), Google 자체의 내부 워크로드(slowspitfire, pickle, unpickle), 그리고 더 넓은 Python 커뮤니티 전반에서 널리 사용되는 도구와 라이브러리(django, 2to3, spambayes) 등 다양한 곳에서 착안했습니다. 이 벤치마크들은 perf.py라는 단일 인터페이스를 통해 실행되며, 이 인터페이스는 메모리 사용량 정보 수집, 성능 그래프 작성, 그리고 유의성을 확인하기 위한 벤치마크 결과의 통계 처리를 담당합니다.
사용 가능한 벤치마크의 전체 목록은 벤치마크를 직접 내려받아 실행하는 방법과 함께 Unladen Swallow 위키 [43]에서 확인할 수 있습니다. 저희의 벤치마크는 모두 오픈 소스이며, Google 독점 벤치마크는 하나도 없습니다. 저희는 이 벤치마크 모음이 완전한 Python 구현이라면 어떤 것이든 벤치마크하는 데 유용한 도구가 된다고 믿으며, 실제로 PyPy는 이미 자체 성능 테스트에 이 벤치마크들을 사용하고 있습니다 [80], [95]. 저희는 이를 환영하며, 벤치마크 모음에 추가할 워크로드를 Python 커뮤니티로부터 찾고 있습니다.
저희는 매크로벤치마크를 수집하는 데, 그리고 애플리케이션 전체를 실행하는 것이 불가능한 경우에는 실제 애플리케이션을 최대한 가깝게 시뮬레이션하는 벤치마크를 수집하는 데 노력을 집중했습니다. 다른 관점에서 보면, 저희 벤치마크 모음은 원래 Google의 Python 코드에서 볼 수 있는 종류의 워크로드(웹 애플리케이션, 텍스트 처리)에 초점을 맞추었지만, 이후 Google이 전혀 관심을 두지 않는 워크로드까지 포함하도록 모음을 확장했습니다. 저희는 지금까지 수치 연산 위주의 워크로드는 피해 왔습니다. NumPy [79]가 이미 그러한 코드를 훌륭하게 처리하고 있으므로 수치 연산 성능 개선은 처음부터 팀의 높은 우선순위가 아니었기 때문입니다. 그러나 이제 그러한 벤치마크를 모음에 포함하기 시작했으며 [96], 수치 연산 Python 코드 최적화 작업도 시작했습니다.
이 벤치마크들 외에도, 저희가 명시적으로 벤치마크할 생각이 없는 다양한 워크로드가 있습니다. Unladen Swallow는 순수 Python 코드의 성능 개선에 초점을 맞추고 있으므로, NumPy와 같은 확장 모듈의 성능은 관심 대상이 아닙니다. NumPy의 핵심 루틴은 C로 구현되어 있기 때문입니다. 마찬가지로, GUI, 데이터베이스, 소켓을 많이 사용하는 애플리케이션처럼 IO가 많은 워크로드는 인터프리터나 코드 생성 최적화를 정확하게 측정하지 못할 것이라고 저희는 생각합니다. 그렇기는 하지만, 표준 라이브러리에 있는 C 언어 확장 모듈의 성능을 개선할 여지는 분명히 있으며, 그래서 cPickle과 re 모듈에 대한 벤치마크를 추가했습니다.
CPython 2.6.4 대비 성능
아래 차트는 CPython 2.6.4와 Unladen Swallow에 대해 여러 번 반복한 벤치마크의 산술 평균을 비교합니다. perf.py는 이보다 더 많은 데이터를 수집하며, 사실 산술 평균만으로는 전체를 설명할 수 없지만 간결함을 위해 평균만 싣습니다. 결과의 유의성을 나타내기 위해 95% 신뢰 구간에서의 스튜던트 양측 T-검정 [44]으로 얻은 t 점수를 함께 싣습니다. 대부분의 벤치마크는 100회 반복 실행하지만, 실행 시간이 더 긴 일부 애플리케이션 전체 벤치마크는 그보다 적은 횟수로 반복 실행합니다.
각 벤치마크에 대한 설명은 Unladen Swallow 위키 [43]에서 확인할 수 있습니다.
명령:
./perf.py -r -b default,apps ../a/python ../b/python
32비트; gcc 4.0.3; Ubuntu Dapper; Intel Core2 Duo 6600 @ 2.4GHz; 코어 2개; 4MB L2 캐시; 4GB RAM
| 벤치마크 | CPython 2.6.4 | Unladen Swallow r988 | 변화 | 유의성 | 타임라인 |
|---|---|---|---|---|---|
| 2to3 | 25.13초 | 24.87초 | 1.01배 빠름 | t=8.94 | http://tinyurl.com/yamhrpg |
| django | 1.08 s | 0.80 s | 1.35배 빠름 | t=315.59 | http://tinyurl.com/y9mrn8s |
| html5lib | 14.29 s | 13.20 s | 1.08배 빠름 | t=2.17 | http://tinyurl.com/y8tyslu |
| nbody | 0.51 s | 0.28 s | 1.84배 빠름 | t=78.007 | http://tinyurl.com/y989qhg |
| rietveld | 0.75 s | 0.55 s | 1.37배 빠름 | 유의하지 않음 | http://tinyurl.com/ye7mqd3 |
| slowpickle | 0.75 s | 0.55 s | 1.37배 빠름 | t=20.78 | http://tinyurl.com/ybrsfnd |
| slowspitfire | 0.83 s | 0.61 s | 1.36배 빠름 | t=2124.66 | http://tinyurl.com/yfknhaw |
| slowunpickle | 0.33 s | 0.26 s | 1.26배 빠름 | t=15.12 | http://tinyurl.com/yzlakoo |
| spambayes | 0.31 s | 0.34 s | 1.10배 느림 | 유의하지 않음 | http://tinyurl.com/yem62ub |
64비트; gcc 4.2.4; Ubuntu Hardy; AMD Opteron 8214 HE @ 2.2 GHz; 4코어; 1MB L2 캐시; 8GB RAM
| 벤치마크 | CPython 2.6.4 | Unladen Swallow r988 | 변화 | 유의성 | 타임라인 |
|---|---|---|---|---|---|
| 2to3 | 31.98 s | 30.41 s | 1.05배 빠름 | t=8.35 | http://tinyurl.com/ybcrl3b |
| django | 1.22초 | 0.94초 | 1.30배 빠름 | t=106.68 | http://tinyurl.com/ybwqll6 |
| html5lib | 18.97초 | 17.79초 | 1.06배 빠름 | t=2.78 | http://tinyurl.com/yzlyqvk |
| nbody | 0.77초 | 0.27초 | 2.86배 빠름 | t=133.49 | http://tinyurl.com/yeyqhbg |
| rietveld | 0.74초 | 0.80초 | 1.08배 느림 | t=-2.45 | http://tinyurl.com/yzjc6ff |
| slowpickle | 0.91초 | 0.62초 | 1.48배 빠름 | t=28.04 | http://tinyurl.com/yf7en6k |
| slowspitfire | 1.01초 | 0.72초 | 1.40배 빠름 | t=98.70 | http://tinyurl.com/yc8pe2o |
| slowunpickle | 0.51 s | 0.34 s | 1.51배 빠름 | t=32.65 | http://tinyurl.com/yjufu4j |
| spambayes | 0.43 s | 0.45 s | 1.06배 느림 | 유의하지 않음 | http://tinyurl.com/yztbjfp |
현재 버전은 Python 함수를 기계어 코드로 컴파일하는 동안 실행을 차단하기 때문에, 이 벤치마크 중 상당수는 Unladen Swallow에서 성능이 떨어집니다. 예를 들어 html5lib과 rietveld 벤치마크의 타임라인 그래프에서 보이는 동작이 이 때문에 나타나며, 2to3의 전체 성능도 이 때문에 느려집니다. 이 문제를 해결하기 위해 개발 브랜치를 활발히 진행하고 있지만([46], [47]), CPython의 현재 스레딩 시스템이 가진 제약 안에서 작업해야 하므로 과정이 복잡해졌고, 처음 예상보다 훨씬 더 많은 주의와 시간이 들었습니다. 저희는 이 문제가 py3k 브랜치로 최종 병합하는 데 결정적이라고 봅니다.
분명히 저희는 5배 성능 향상이라는 초기 목표를 달성하지 못했습니다. 이어지는 Performance Retrospective에서는 초기 성능 목표를 달성하지 못한 이유를 다룹니다. 저희는 아직 구현되지 않은 성능 작업 목록을 관리하고 있습니다 [50].
메모리 사용량
다음 표는 CPython 2.6.4와 Unladen Swallow r988 각각에 대해 Unladen Swallow 기본 벤치마크별 최대 메모리 사용량(킬로바이트 단위)과, 벤치마크 실행 기간 전체에 걸친 메모리 사용량 타임라인을 보여 줍니다. 32비트와 64비트 바이너리 모두에 대한 표를 수록했습니다. 메모리 사용량은 Linux 2.6 시스템에서 커널의 /proc/$pid/smaps 의사 파일에 있는 Private_ 섹션을 합산하여 측정했습니다 [45].
명령:
./perf.py -r --track_memory -b default,apps ../a/python ../b/python
32비트
| 벤치마크 | CPython 2.6.4 | Unladen Swallow r988 | 변화 | 타임라인 |
|---|---|---|---|---|
| 2to3 | 26396 kb | 46896 kb | 1.77x | http://tinyurl.com/yhr2h4z |
| django | 10028 kb | 27740 kb | 2.76x | http://tinyurl.com/yhan8vs |
| html5lib | 150028 kb | 173924 kb | 1.15x | http://tinyurl.com/ybt44en |
| nbody | 3020 kb | 16036 kb | 5.31x | http://tinyurl.com/ya8hltw |
| rietveld | 15008 kb | 46400 kb | 3.09x | http://tinyurl.com/yhd5dra |
| slowpickle | 4608 kb | 16656 kb | 3.61x | http://tinyurl.com/ybukyvo |
| slowspitfire | 85776 kb | 97620 kb | 1.13x | http://tinyurl.com/y9vj35z |
| slowunpickle | 3448 kb | 13744 kb | 3.98x | http://tinyurl.com/yexh4d5 |
| spambayes | 7352 kb | 46480 kb | 6.32x | http://tinyurl.com/yem62ub |
64비트
| 벤치마크 | CPython 2.6.4 | Unladen Swallow r988 | 변화 | 타임라인 |
|---|---|---|---|---|
| 2to3 | 51596 kb | 82340 kb | 1.59x | http://tinyurl.com/yljg6rs |
| django | 16020 kb | 38908 kb | 2.43x | http://tinyurl.com/ylqsebh |
| html5lib | 259232 kb | 324968 kb | 1.25x | http://tinyurl.com/yha6oee |
| nbody | 4296 kb | 23012 kb | 5.35x | http://tinyurl.com/yztozza |
| rietveld | 24140 kb | 73960 kb | 3.06x | http://tinyurl.com/ybg2nq7 |
| slowpickle | 4928 kb | 23300 kb | 4.73x | http://tinyurl.com/yk5tpbr |
| slowspitfire | 133276 kb | 148676 kb | 1.11x | http://tinyurl.com/y8bz2xe |
| slowunpickle | 4896 kb | 16948 kb | 3.46x | http://tinyurl.com/ygywwoc |
| spambayes | 10728 kb | 84992 kb | 7.92x | http://tinyurl.com/yhjban5 |
메모리 사용량이 늘어난 원인은 a) LLVM 코드 생성, 분석 및 최적화 라이브러리, b) 네이티브 코드, c) LLVM의 메모리 사용 문제나 누수, d) 기계어 코드를 최적화하고 생성하는 데 필요한 자료 구조, e) 아직 분류되지 않은 기타 원인입니다.
초기의 단순한 JIT 구현 [42] 이후 메모리 사용량을 줄이는 데 상당한 진전을 이루었지만, 해야 할 일이 분명히 더 남아 있습니다. 성능을 희생하지 않고도 메모리를 더 절약할 여지가 아직 있다고 믿습니다. 지금까지는 순수한 성능에 집중하는 경향이 있었으며, 메모리 사용량을 줄이는 데는 아직 집중적으로 노력하지 않았습니다. 메모리 사용량을 줄이는 일은 py3k 브랜치로 최종 병합하기 전에 반드시 해결해야 하는 문제라고 봅니다. 메모리 사용량이 어느 정도까지 늘어나도 받아들일 수 있는지에 대해 커뮤니티의 의견을 구합니다.
시작 시간
LLVM의 코드 생성, 분석 및 최적화 라이브러리를 정적으로 링크하면 Python 바이너리를 시작하는 데 걸리는 시간이 늘어납니다. LLVM이 사용하는 C++ 정적 초기화 코드도 시작 시간을 늘립니다. Python 코드에 인라인하려고 미리 컴파일해 둔 C 런타임 루틴 모음을 임포트하는 것도 마찬가지입니다.
Unladen Swallow의 startup 벤치마크 결과는 다음과 같습니다.
$ ./perf.py -r -b startup /tmp/cpy-26/bin/python /tmp/unladen/bin/python
### normal_startup ###
Min: 0.219186 -> 0.352075: 1.6063x slower
Avg: 0.227228 -> 0.364384: 1.6036x slower
Significant (t=-51.879098, a=0.95)
Stddev: 0.00762 -> 0.02532: 3.3227x larger
Timeline: http://tinyurl.com/yfe8z3r
### startup_nosite ###
Min: 0.105949 -> 0.264912: 2.5004x slower
Avg: 0.107574 -> 0.267505: 2.4867x slower
Significant (t=-703.557403, a=0.95)
Stddev: 0.00214 -> 0.00240: 1.1209x larger
Timeline: http://tinyurl.com/yajn8fa
### bzr_startup ###
Min: 0.067990 -> 0.097985: 1.4412x slower
Avg: 0.084322 -> 0.111348: 1.3205x slower
Significant (t=-37.432534, a=0.95)
Stddev: 0.00793 -> 0.00643: 1.2330x smaller
Timeline: http://tinyurl.com/ybdm537
### hg_startup ###
Min: 0.016997 -> 0.024997: 1.4707x slower
Avg: 0.026990 -> 0.036772: 1.3625x slower
Significant (t=-53.104502, a=0.95)
Stddev: 0.00406 -> 0.00417: 1.0273x larger
Timeline: http://tinyurl.com/ycout8m
bzr_startup과 hg_startup은 각각 Bazaar와 Mercurial이 도움말 화면을 표시하는 데 걸리는 시간을 측정합니다. startup_nosite는 python -S를 여러 번 실행합니다. -S 옵션은 드물게 사용되지만, 이 벤치마크는 늘어난 시작 시간이 어디에서 비롯되는지 잘 보여 준다고 생각합니다.
Unladen Swallow는 시작 시간 최적화에서 진전을 이루었지만, 아직 해야 할 작업과 구현해야 할 추가 최적화가 남아 있습니다. 시작 시간 개선은 Unladen Swallow의 병합 작업 목록에서 우선순위가 높은 항목 [33]입니다.
바이너리 크기
LLVM의 코드 생성, 분석 및 최적화 라이브러리를 정적으로 링크하면 python 바이너리의 크기가 크게 늘어납니다. 아래 표는 스트립한 바이너리가 디스크에서 차지하는 크기입니다. 시스템 패키지 관리자가 사용하는 구성에 더 가깝게 맞추려고 바이너리를 스트립했습니다. 바이너리 크기 변화를 측정하는 방법으로는 이것이 가장 현실적이라고 생각합니다.
| 바이너리 크기 | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r1041 |
|---|---|---|---|
| 32비트 | 1.3M | 1.4M | 12M |
| 64비트 | 1.6M | 1.6M | 12M |
바이너리 크기가 늘어난 원인은 LLVM의 코드 생성, 분석, 최적화 라이브러리를 python 바이너리에 정적으로 링크하기 때문입니다. 이 문제는 LLVM이 공유 링크를 더 잘 지원하도록 수정한 다음, 현재의 정적 링크 대신 공유 링크를 사용하면 간단히 해결할 수 있습니다. 다만 당분간은 정적 링크를 통해 LLVM에 링크하는 비용을 정확히 파악할 수 있습니다.
정적으로 링크하더라도 Unladen Swallow의 LLVM 의존성을 줄이면 디스크상의 바이너리 크기를 개선할 여지가 아직 남아 있다고 생각합니다. 이 문제는 현재 적극적으로 해결하고 있습니다 [32].
성능 회고
Unladen Swallow의 초기 목표는 CPython 2.6 대비 5배의 성능 향상이었습니다. 이 목표를 달성하지 못했으며, 솔직히 말하면 근처에도 가지 못했습니다. 이 프로젝트는 왜 그 목표를 달성하지 못했으며, LLVM 기반 JIT가 과연 그 목표를 달성할 수 있겠습니까?
Unladen Swallow는 왜 5배라는 목표를 달성하지 못했습니까? 주된 이유는 LLVM에 처음 예상했던 것보다 더 많은 작업이 필요했기 때문입니다. Apple이 LLVM 기반 제품을 출시하고 있었다는 사실 [81]과 다른 고수준 언어들이 LLVM 기반 JIT를 성공적으로 구현했다는 사실([61], [62], [82])을 근거로, 저희는 LLVM의 JIT에 치명적인 버그가 비교적 없을 것이라고 가정했습니다.
그러나 이는 사실이 아닌 것으로 드러났습니다. 저희는 성능에서 관심을 돌려 LLVM의 JIT 인프라에 있는 여러 치명적인 버그를 수정해야 했으며(예: [83], [84]), 여러 측면에서 추가 최적화를 가능하게 하는, 있으면 좋은 개선 작업도 여럿 해야 했습니다(예: [86], [85], [87]). LLVM의 정적 코드 생성 기능, 도구, 최적화 패스는 안정적이고 충분한 스트레스 테스트를 거쳤지만, JIT(just-in-time) 인프라는 상대적으로 테스트가 부족하고 버그가 많았습니다. 저희는 이 문제를 해결했습니다.
(다른 프로젝트들은 피해 갔던 이러한 문제에 저희가 부딪힌 것은 CPython 표준 라이브러리 테스트 스위트가 복잡하고 철저하기 때문이라고 추정합니다.)
또한 엔지니어링 역량을 성능 대신 gdb와 oProfile 같은 지원 도구에 투입했습니다. gdb는 JIT 컴파일러와 전혀 잘 동작하지 않았고, LLVM은 이전에 oProfile과 통합되어 있지 않았습니다. JIT를 인식하는 디버거와 프로파일러를 갖춘 것은 프로젝트에 매우 가치 있는 일이었으며, 이러한 방향에 시간을 쏟은 것을 후회하지 않습니다. 자세한 내용은 Debugging 및 Profiling 절을 참조하십시오.
LLVM 기반 CPython JIT가 과연 5배 성능 목표를 달성할 수 있겠습니까? JIT 기반 JavaScript 구현의 벤치마크 결과는 5배가 실제로 가능하다는 것을 시사하며, PyPy의 JIT가 수치 연산 워크로드에서 보여 준 결과도 마찬가지입니다. Self-92의 경험 [52]도 시사하는 바가 큽니다.
LLVM이 이를 해낼 수 있겠습니까? 저희는 LLVM 기반 JIT가 해낼 수 있는 것의 겉만 이제 막 건드리기 시작했을 뿐이라고 생각합니다. 지금까지 이 시스템에 도입한 최적화는 상당한 성과를 거두었습니다(예: [88], [89], [90]). 지금까지의 경험으로 볼 때, Unladen Swallow의 성능을 제한하는 요인은 문헌에 나온 기법을 구현하는 데 필요한 엔지니어링 시간입니다. LLVM은 다루기 쉽고 수정하기도 쉬웠으며, 내장된 최적화 덕분에 Python 수준의 최적화를 구현하는 작업이 크게 단순해졌습니다.
추가적인 성능 개선 기회에 대한 개요는 Future Work 절에서 다룹니다.
정확성과 호환성
Unladen Swallow의 정확성 테스트 스위트에는 CPython의 테스트 스위트(Lib/test/ 아래)뿐만 아니라 여러 중요한 서드파티 애플리케이션과 라이브러리 [6]도 포함되어 있습니다. 이러한 애플리케이션과 라이브러리의 전체 목록은 아래와 같습니다. zope.interface [34] 같이 이 패키지들에 필요한 의존성 역시 주요 패키지를 테스트하는 과정에서 간접적으로 테스트되므로, 테스트되는 서드파티 Python 코드의 범위가 더욱 넓어집니다.
- 2to3
- Cheetah
- cvs2svn
- Django
- Nose
- NumPy
- PyCrypto
- pyOpenSSL
- PyXML
- Setuptools
- SQLAlchemy
- SWIG
- SymPy
- Twisted
- ZODB
이 애플리케이션들은 Unladen Swallow에서 실행할 때 관련된 모든 테스트를 통과합니다. 단, 기준선인 CPython 2.6.4에서 실패한 일부 테스트는 비활성화했으며, 정확한 바이트코드 번호나 바이트코드 형식처럼 CPython 내부 구현을 전제로 하는 테스트도 비활성화했다는 점에 유의하십시오. 테스트를 비활성화한 패키지에는 모두 변경 사항을 자세히 설명하는 README.unladen 파일이 포함되어 있습니다(예: [37]).
또한 Unladen Swallow는 다양한 Google 내부 Python 라이브러리와 애플리케이션의 배열을 대상으로 자동으로 테스트됩니다. 여기에는 BigTable [35]용 Google 내부 Python 바인딩, Mondrian 코드 리뷰 애플리케이션 [36], Google의 Python 표준 라이브러리 등이 포함됩니다. 이러한 프로젝트를 Unladen Swallow에서 실행하는 데 필요한 변경 사항은 일관되게 다음 세 가지 범주 중 하나에 속했습니다:
- CPython 2.6 C API 호환성 추가. Google은 내부적으로 여전히 주로 CPython 2.4를 사용하므로,
int를 사용하는 부분을Py_ssize_t로 바꾸는 것과 같은 API 변경 사항을 반영해야 했습니다. - CPython 버전 번호를 명시적으로 잘못 검사하는 테스트를 수정하거나 비활성화.
- 이후에 수정된 CPython 2.4의 버그를 우회하거나 그 버그에 의존하던 코드를 조건부로 비활성화.
이처럼 광범위한 공개 및 독점 애플리케이션과 라이브러리를 대상으로 한 테스트는 Unladen Swallow의 정확성을 보장하는 데 중요한 역할을 했습니다. 테스트를 통해 버그가 드러났으며, 저희는 이를 적절히 수정했습니다. 자동화된 회귀 테스트 체계 덕분에 저희는 개발을 진행하면서 변경 사항에 대해 높은 확신을 가질 수 있었습니다.
서드파티 테스트 외에도, 저희는 언어나 구현의 코너 케이스 가운데 테스트되지 않았거나 명세가 불충분하다고 판단한 부분을 다루는 테스트를 CPython 테스트 스위트에 추가했습니다(예: [48], [49]). 이 테스트들은 최적화를 구현할 때 특히 중요했습니다. Python의 잘 드러나지 않는 구석을 실수로 망가뜨리지 않았는지 확인하는 데 도움이 되었기 때문입니다.
또한 LLVM 기반 JIT 컴파일러와 이를 위해 구현한 최적화만을 다루는 테스트 스위트도 구축했습니다 [38]. 최적화 컴파일러를 작성하는 일은 본질적으로 복잡하고 미묘하기 때문에, 저희는 컴파일하고 최적화하는 구문, 시나리오, 코너 케이스를 빠짐없이 열거하려고 노력했습니다. JIT 테스트에는 JIT 핫니스 모델 등에 대한 테스트도 포함되어 있어, 향후 CPython 개발자가 이를 유지 보수하고 개선하기가 더 쉬워집니다.
최근에는 컴파일러에 스트레스 테스트를 하기 위해 퍼즈 테스트 [39]를 사용하기 시작했습니다. 과거에 pyfuzz [40]와 Fusil [41]을 모두 사용한 적이 있으며, 이 도구들을 CPython 테스트 프로세스에 자동화된 단계로 도입할 것을 권장합니다.
알려진 비호환성
CPython 2.6.4에서는 동작하지만 Unladen Swallow에서는 동작하지 않는 것으로 알려진 애플리케이션이나 라이브러리는 Psyco [64]가 유일합니다. PyGame [78] 등 일부 라이브러리는 CPython 2.6.4에서 잘 동작하지만, Unladen Swallow에서 이루어진 변경 때문에 어느 정도 성능 저하를 겪는다는 것을 알고 있습니다. 이 이슈 [47]를 추적하고 있으며, 이러한 성능 저하 사례를 해결하기 위해 작업하고 있습니다.
Unladen Swallow는 CPython 2.6.4와 소스 호환되지만 바이너리 호환되지는 않습니다. 한쪽에 맞춰 컴파일된 C 확장 모듈을 다른 쪽에서 사용하려면 다시 컴파일해야 합니다.
Unladen Swallow를 병합해도 WPython처럼 오래 유지되어 온 CPython 최적화 브랜치에는 영향이 거의 없을 것입니다. WPython [104]과 Unladen Swallow는 대체로 서로 독립적이며, 둘 다 CPython에 병합하지 못할 기술적 이유는 없습니다. WPython을 JIT가 강화된 CPython 버전과 호환되게 만드는 데 필요한 변경은 아주 적을 것입니다 [113]. 다른 CPython 최적화 프로젝트도 마찬가지일 것입니다(예: [114]).
Stackless Python [115] 같이 CPython을 크게 뜯어고친 포크는 지원하기가 더 어렵습니다. Stackless가 CPython에 병합될 가능성은 매우 낮고 [116] 유지 보수 부담이 늘어나는 것은 어떤 포크든 피할 수 없는 일이므로, 저희는 Stackless와의 호환성을 비교적 낮은 우선순위로 봅니다. JIT로 컴파일된 스택 프레임은 C 스택을 사용하므로, Stackless는 이를 확장 모듈을 통한 호출과 같은 방식으로 다룰 수 있을 것입니다. 그 방식을 받아들일 수 없다고 판명되면, Stackless는 JIT 컴파일러를 제거하거나, 힙 기반 스택 프레임을 더 잘 지원하도록 JIT 코드 생성을 개선할 수 있습니다 [117], [118].
플랫폼 지원
Unladen Swallow는 본질적으로 LLVM이 제공하는 플랫폼 지원, 특히 LLVM의 JIT 컴파일 시스템 [7]에 의해 제한됩니다. LLVM의 JIT는 x86 및 x86-64 시스템에서 가장 잘 지원되며, Unladen Swallow도 이 플랫폼들에서 가장 많이 테스트되었습니다. 저희는 LLVM/Unladen Swallow가 x86 및 x86-64 하드웨어를 제대로 지원한다고 확신합니다. PPC와 ARM 지원도 존재하지만 널리 사용되지 않으며 버그가 있을 수 있습니다(예: [99], [83], [100]).
Unladen Swallow는 다음 운영 체제에서 동작하는 것으로 알려져 있습니다: Linux, Darwin, Windows. Unladen Swallow는 Linux와 Darwin에서 가장 많이 테스트되었지만, Windows에서도 빌드되고 테스트를 통과합니다.
LLVM의 JIT가 동작하지 않는 하드웨어 및 소프트웨어 플랫폼을 지원하기 위해, Unladen Swallow는 ./configure --without-llvm 옵션을 제공합니다. 이 플래그는 Unladen Swallow에서 LLVM에 의존하는 부분을 모두 떼어 내어, 동작하고 테스트도 통과하지만 성능상의 이점은 없는 Python 바이너리를 만들어 냅니다. 이 구성은 LLVM이 지원하지 않는 하드웨어나 성능보다 메모리 사용량을 더 중시하는 시스템에 권장됩니다.
CPython 개발에 미치는 영향
Python 또는 CPython 바이트코드 변경 실험하기
Unladen Swallow의 JIT 컴파일러는 CPython 바이트코드를 대상으로 동작하므로, 파서에만 영향을 미치는 Python 언어 변경의 영향을 받지 않습니다.
CPython 바이트코드 컴파일러를 변경하거나 개별 바이트코드의 의미를 변경할 때는 먼저 인터프리터 루프에서 프로토타입을 만든 다음, 의미가 명확해지면 JIT 컴파일러로 이식할 것을 권장합니다. 이를 쉽게 하기 위해 Unladen Swallow는 JIT 컴파일러와 관련된 모든 인프라를 제거하는 --without-llvm configure 시점 옵션을 제공합니다. 이렇게 하면 현재의 실험 부담이 그대로 유지되므로, 개발자는 진입 장벽이 낮은 현재의 인터프리터 루프에서 프로토타입을 만들 수 있습니다.
Unladen Swallow는 바이트코드 구현을 LLVM API 호출로 직관적이고 단순하게 옮기는 방식으로 JIT 컴파일러 구현을 시작했습니다. 저희는 이 과정이 이해하기 쉽다는 것을 확인했으며, CPython에도 같은 접근 방식을 권장합니다. 이러한 개발 방식의 예로 Unladen Swallow 저장소의 몇 가지 변경 사항을 여기에 소개합니다: [26], [27], [28], [29].
디버깅
Unladen Swallow 팀은 JIT 컴파일된 Python 코드를 gdb로 더 쉽게 디버깅할 수 있도록 gdb를 변경했습니다. 이 변경 사항은 gdb 7.0 [17]에서 릴리스되었습니다. 이 변경으로 gdb는 JIT가 생성한 호출 스택 프레임을 식별하고 그 너머까지 스택을 되감을 수 있습니다. 따라서 예를 들어 list 타입이나 내장 함수를 변경하는 경우에도 gdb는 CPython 개발에서 이전과 같이 계속 동작할 수 있습니다.
변경 후의 역추적 예시이며, 여기서 baz, bar, foo는 JIT 컴파일된 함수입니다:
Program received signal SIGSEGV, Segmentation fault.
0x00002aaaabe7d1a8 in baz ()
(gdb) bt
#0 0x00002aaaabe7d1a8 in baz ()
#1 0x00002aaaabe7d12c in bar ()
#2 0x00002aaaabe7d0aa in foo ()
#3 0x00002aaaabe7d02c in main ()
#4 0x0000000000b870a2 in llvm::JIT::runFunction (this=0x1405b70, F=0x14024e0, ArgValues=...)
at /home/rnk/llvm-gdb/lib/ExecutionEngine/JIT/JIT.cpp:395
#5 0x0000000000baa4c5 in llvm::ExecutionEngine::runFunctionAsMain
(this=0x1405b70, Fn=0x14024e0, argv=..., envp=0x7fffffffe3c0)
at /home/rnk/llvm-gdb/lib/ExecutionEngine/ExecutionEngine.cpp:377
#6 0x00000000007ebd52 in main (argc=2, argv=0x7fffffffe3a8,
envp=0x7fffffffe3c0) at /home/rnk/llvm-gdb/tools/lli/lli.cpp:208
이전에는 JIT 컴파일된 프레임 때문에 gdb가 스택을 잘못 되감아, 명백히 잘못된 #6 0x00002aaaabe7d0aa in ?? () 형태의 스택 프레임을 대량으로 생성했습니다.
장점:
- gdb 7.0은 JIT 컴파일된 스택 프레임을 올바르게 파싱할 수 있으므로, JIT 컴파일되지 않은 함수, 즉 CPython 코드베이스의 대부분에서 gdb를 온전히 사용할 수 있습니다.
- JIT 컴파일된 스택 프레임 안에서 역어셈블하면 해당 함수를 구성하는 전체 명령어 목록이 자동으로 출력됩니다. 이는 저희 작업 이전의 gdb 상태보다 발전한 것입니다. 이전에는 개발자가 함수의 시작 주소를 추측하여 어셈블리 코드를 직접 역어셈블해야 했습니다.
- 유연한 기반 메커니즘 덕분에 CPython은 점점 더 많은 정보를 추가할 수 있으며, 결국 JIT 컴파일된 기계어 코드에 대해 gdb의 C/C++ 지원과 동등한 수준에 도달할 수 있습니다.
단점:
- gdb는 JIT 컴파일된 함수 안에서 지역 변수를 출력하거나 현재 실행 중인 줄을 알려 줄 수 없습니다. 또한 명령어 하나씩 실행하는 경우를 제외하면 JIT 컴파일된 코드를 단계별로 실행할 수도 없습니다.
- 아직 Apple의 gdb나 Microsoft의 Visual Studio 디버거와는 통합되지 않았습니다.
Unladen Swallow 팀은 이 변경 사항을 향후 gdb 릴리스에 반영하기 위해 Apple과 협력하고 있습니다.
프로파일링
Unladen Swallow는 oProfile 0.9.4 이상 [18]과 통합되어 Linux 시스템에서 어셈블리 수준의 프로파일링을 지원합니다. 즉, oProfile은 보고서에서 JIT 컴파일된 함수의 심볼을 올바르게 표시합니다.
보고서 예시이며, 여기서 #u# 접두사가 붙은 심볼 이름은 JIT 컴파일된 Python 함수입니다:
$ opreport -l ./python | less
CPU: Core 2, speed 1600 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 100000
samples % image name symbol name
79589 4.2329 python PyString_FromFormatV
62971 3.3491 python PyEval_EvalCodeEx
62713 3.3354 python tupledealloc
57071 3.0353 python _PyEval_CallFunction
50009 2.6597 24532.jo #u#force_unicode
47468 2.5246 python PyUnicodeUCS2_Decode
45829 2.4374 python PyFrame_New
45173 2.4025 python lookdict_string
43082 2.2913 python PyType_IsSubtype
39763 2.1148 24532.jo #u#render5
38145 2.0287 python _PyType_Lookup
37643 2.0020 python PyObject_GC_UnTrack
37105 1.9734 python frame_dealloc
36849 1.9598 python PyEval_EvalFrame
35630 1.8950 24532.jo #u#resolve
33313 1.7717 python PyObject_IsInstance
33208 1.7662 python PyDict_GetItem
33168 1.7640 python PyTuple_New
30458 1.6199 python PyCFunction_NewEx
이 지원은 동작하지만 아직 다듬어지지 않았습니다. Unladen Swallow는 oProfile 통합을 CPython 핵심 개발자에게 더 유용하게 만들기 위해 개선이 중요하다고 판단하는 항목들의 작업 목록을 관리하고 있습니다 [19].
장점:
- Linux의 oProfile에서 JIT 컴파일된 프레임의 심볼화가 작동합니다.
단점:
- Apple의 Shark [20] 또는 Microsoft의 Visual Studio 프로파일링 도구에서 JIT 컴파일된 프레임의 심볼화를 개선하는 작업은 아직 진행되지 않았습니다.
- oProfile 출력은 아직 다듬을 부분이 남아 있습니다.
x86-64 플랫폼에서 발생하던 oProfile 버그(현재는 수정됨)를 피하려면 oProfile 0.9.5 이상을 사용할 것을 권장합니다. 다만 32비트 플랫폼에서는 oProfile 0.9.4도 잘 작동합니다.
oProfile을 LLVM [21] 및 Unladen Swallow [22]와 쉽게 통합할 수 있었던 점을 고려하면, 다른 프로파일링 도구도 유사한 JIT 인터페이스 [23]를 지원하기만 한다면 쉽게 통합할 수 있을 것입니다.
oProfile을 사용하여 Unladen Swallow를 프로파일링하는 절차를 문서화해 두었습니다 [24]. 이 문서는 병합 시 CPython의 Doc/ 트리에 합쳐질 것입니다.
CPython에 C++ 추가
LLVM을 사용하기 위해 Unladen Swallow는 핵심 CPython 트리와 빌드 과정에 C++를 도입했습니다. 이는 LLVM에 의존하는 데 따르는 불가피한 부분입니다. LLVM이 C API [8]를 제공하기는 하지만, 기능이 제한적이며 CPython에 필요한 기능을 노출하지 않습니다. 이 때문에 Unladen Swallow JIT의 내부 세부 사항과 이를 지원하는 인프라를 C++로 구현했습니다. CPython 코드베이스 전체를 C++로 전환하자고 제안하는 것은 아닙니다.
장점:
- LLVM의 완전하고 강력한 코드 생성 기능과 관련 API를 쉽게 사용할 수 있습니다.
- 편리하고 추상적인 자료 구조로 코드가 단순해집니다.
- C++는 CPython 코드베이스의 비교적 작은 부분에만 국한됩니다.
./configure --without-llvm으로 C++를 비활성화할 수 있으며, 이 경우libstdc++에 대한 의존성까지 제거됩니다.
단점:
LLVM 릴리스 관리, C++ API 변경
LLVM은 6개월마다 정기적으로 릴리스됩니다. 따라서 CPython 3.x 릴리스 하나를 개발하는 동안 LLVM이 두세 번 릴리스될 수 있습니다. LLVM 릴리스마다 더 새롭고 강력한 최적화, 개선된 플랫폼 지원, 더 정교한 코드 생성이 제공됩니다.
LLVM 릴리스에는 보통 LLVM C++ API에 대한 호환되지 않는 변경이 포함됩니다. LLVM 2.6 릴리스 노트 [9]에는 의도적으로 도입된 비호환 사항 목록이 포함되어 있습니다. Unladen Swallow는 개발 기간 내내 LLVM 트렁크를 면밀히 추적해 왔습니다. 경험상 LLVM API 변경은 명확하며 쉽게 또는 기계적으로 대응할 수 있었습니다. 참고로 Unladen Swallow 트리에서 그러한 변경 두 가지를 여기에 제시합니다: [10], [11].
API 비호환성 때문에 LLVM 기반 CPython은 한 번에 하나의 LLVM 버전과의 호환성만을 목표로 할 것을 권장합니다. 이렇게 하면 코어 개발 팀의 부담이 줄어듭니다. 패키징 관점에서 특정 LLVM 버전에 고정하는 것은 문제가 되지 않을 것입니다. 미리 빌드된 LLVM 패키지는 일반적으로 LLVM 릴리스 후 상당히 빠르게 표준 시스템 패키지 관리자를 통해 제공되며, 그렇지 않더라도 llvm.org 자체에서 바이너리 릴리스를 제공하기 때문입니다.
Unladen Swallow는 그동안 Unladen Swallow 트리 안에 LLVM과 Clang 소스 트리의 사본을 포함해 왔습니다. 이는 LLVM에 패치를 적용하면서 LLVM 트렁크를 긴밀하게 추적하기 위해서였습니다. CPython에는 이러한 개발 모델을 권장하지 않습니다. CPython 릴리스는 공식 LLVM 릴리스를 기반으로 해야 합니다. 미리 빌드된 LLVM 패키지는 Darwin용으로 MacPorts [12]에서 제공되며, 대부분의 주요 Linux 배포판([13], [14], [16])에서도 제공됩니다. LLVM 자체도 MinGW [25]용과 같은 추가 바이너리를 제공합니다.
LLVM은 현재 정적으로 링크되도록 설계되어 있습니다. 이는 CPython의 바이너리 릴리스에 LLVM의 관련 부분(전부는 아닙니다!)이 포함된다는 의미입니다. 앞서 언급했듯이 이로 인해 바이너리 크기가 커집니다. 다운스트림 패키지 관리를 단순화하기 위해, 공유 링크를 더 잘 지원하도록 LLVM을 수정할 것입니다. 이 이슈가 해결되기 전에는 최종 병합이 이루어지지 않습니다 [97].
Unladen Swallow는 LLVM 2.7 릴리스 전에 LLVM에 남아 있는 치명적인 이슈를 모두 수정하는 일을 전담 엔지니어 한 명에게 맡겼습니다. Unladen Swallow가 해 온 것처럼 LLVM 트렁크를 긴밀하게 추적하는 대신, CPython 3.x가 릴리스된 LLVM 버전에 의존할 수 있어야 한다는 점이 필수적이라고 생각합니다. 2010년 5월로 예정된 LLVM 2.7 릴리스 전에 이 작업 [98]을 마칠 수 있을 것으로 봅니다.
CPython 빌드하기
Unladen Swallow는 LLVM에 대한 런타임 의존성 외에도, LLVM 기반 C/C++ 컴파일러인 Clang [5]에 대한 빌드 타임 의존성을 가집니다. 이를 사용하여 C 언어로 작성된 Python 런타임의 일부를 LLVM의 중간 표현으로 컴파일합니다. 이렇게 하면 언어 간 인라이닝을 수행할 수 있어 성능이 향상됩니다. Unladen Swallow를 실행하는 데 Clang이 필요하지는 않습니다. Clang 바이너리 패키지는 대부분의 주요 Linux 배포판에서 제공됩니다(예: [15]).
configure, 전체 빌드, 그리고 C 소스 파일 하나를 수정한 후의 증분 빌드를 포함하여, Unladen Swallow가 Python 빌드에 필요한 시간에 미치는 영향을 조사했습니다.
| ./configure | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r988 |
|---|---|---|---|
| 1회차 | 0m20.795s | 0m16.558s | 0m15.477s |
| 2회차 | 0m15.255s | 0m16.349s | 0m15.391s |
| 3회차 | 0m15.228s | 0m16.299s | 0m15.528s |
| 전체 make | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r988 |
|---|---|---|---|
| 1회차 | 1m30.776s | 1m22.367s | 1m54.053s |
| 2회차 | 1m21.374s | 1m22.064s | 1m49.448s |
| 3회차 | 1m22.047s | 1m23.645s | 1m49.305s |
전체 빌드는 a) LLVM과 상호 작용하는 데 필요한 추가 .cc 파일, b) LLVM을 libpython에 정적으로 링크하는 작업, c) 언어 간 인라이닝이 가능하도록 Python 런타임 일부를 LLVM IR로 컴파일하는 작업 때문에 느려집니다.
증분 빌드도 메인라인 CPython보다 다소 느립니다. 아래 표는 Objects/listobject.c를 수정한 뒤의 증분 재빌드 시간을 보여 줍니다.
| 증분 make | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r1024 |
|---|---|---|---|
| 1회차 | 0m1.854s | 0m1.456s | 0m6.680s |
| 2회차 | 0m1.437s | 0m1.442s | 0m5.310s |
| 3회차 | 0m1.440s | 0m1.425s | 0m7.639s |
전체 빌드와 마찬가지로, 이 추가 시간은 LLVM을 libpython에 정적으로 링크하기 때문에 생깁니다. libpython이 LLVM에 공유 방식으로 링크된다면 이 오버헤드는 줄어들 것입니다.
제안하는 병합 계획
우리는 궁극적으로 CPython의 3.x 개발 라인에 병합하는 데 노력을 집중할 것을 제안합니다. BDFL은 2.7이 CPython 2.x 개발 라인의 마지막 릴리스가 될 것이라고 밝혔으며 [30], 2.7 알파 1이 이미 릴리스되었으므로 우리는 그 기회를 놓쳤습니다. Python 3이 미래이며, 우리는 바로 그곳에 성능 개선 노력을 집중할 것입니다.
우리는 Unladen Swallow를 CPython 소스 트리에 병합하기 위해 다음과 같은 계획을 권장합니다.
- 작업할 브랜치를 CPython SVN 저장소에 만들고, 임시로
py3k-jit라고 부릅니다. 이 브랜치는 CPythonpy3k브랜치에서 갈라져 나온 브랜치가 됩니다. - 우리는 이 브랜치를
py3k와 긴밀하게 통합된 상태로 유지할 것입니다. 차이가 벌어질수록 우리의 작업은 더 어려워질 것입니다. - JIT 관련 패치는 모두
py3k-jit브랜치에 들어갑니다. - JIT와 관련 없는 패치는 (검토와 승인을 거친 후)
py3k브랜치에 들어가고, 다시py3k-jit브랜치로 병합됩니다. - 새로운 명령줄 플래그나 환경 변수 도입처럼 논란의 여지가 있는 사안은 python-dev에서 논의합니다.
Google은 내부적으로 CPython 2.x를 사용하기 때문에 Unladen Swallow는 CPython 2.6을 기반으로 합니다. 우리는 컴파일러를 Python 3으로 이식해야 합니다. 이 작업은 py3k-jit 브랜치에 패치를 적용하는 과정에서 이루어지므로, 이 브랜치는 항상 일관된 Python 3 구현으로 유지됩니다.
우리는 py3k로의 최종 병합을 가로막는 남은 문제들을 해결하는 동안 이 접근 방식이 3.2 또는 3.3 릴리스 과정을 최소한으로만 방해할 것이라고 믿습니다. Unladen Swallow는 최종 병합 전에 해결해야 할 알려진 문제들의 점검 목록을 관리하고 있으며 [31], 여기에는 이 PEP에서 언급한 모든 문제가 포함되어 있습니다. CPython 커뮤니티에도 나름의 우려 사항이 있을 것이라 생각합니다. 이 점검 목록은 고정된 것이 아닙니다. 앞으로 py3k 브랜치로의 최종 병합을 가로막는 다른 문제들이 새로 나타날 수도 있습니다.
변경 사항은 py3k-jit 브랜치에 직접 커밋되며, 규모가 크거나 까다롭거나 논란의 여지가 있는 변경 사항만 커밋 전 코드 리뷰를 받습니다.
비상 계획
메모리 사용량이나 시작 시간을 CPython 커뮤니티가 만족할 만한 수준으로 줄이지 못할 가능성도 있습니다. 이러한 상황에 대비한 우리의 주된 비상 계획은 온라인 JIT(just-in-time) 컴파일 전략에서, 계측 기능을 넣은 CPython 인터프리터 루프로 피드백을 얻는 오프라인 AOT(ahead-of-time) 전략으로 전환하는 것입니다. 이는 gcc의 피드백 기반 최적화(-fprofile-generate) [111] 및 Microsoft Visual Studio의 프로파일 기반 최적화 [112]에서 사용하는 것과 같은 모델이며, 이 문서에서는 이를 “피드백 기반 최적화”, 즉 FDO라고 부르겠습니다.
저희는 Python용 FDO 컴파일러가 JIT 컴파일러보다 못할 것이라고 생각합니다. FDO에는 고품질의 대표성 있는 벤치마크 스위트가 필요한데, 이는 오픈 소스 개발과 비공개 소스 개발 모두에서 비교적 드문 것입니다. JIT 컴파일러는 벤치마크 스위트가 있든 없든 어떤 애플리케이션에서든 핫스팟을 동적으로 찾아 최적화할 수 있으므로, 사람의 개입 없이 애플리케이션 병목 지점의 변화에 적응할 수 있습니다.
사전(ahead-of-time) FDO 컴파일러가 필요하다면, Unladen Swallow의 JIT 컴파일러를 위해 이미 개발된 코드와 인프라의 상당 부분을 활용할 수 있어야 합니다. 실제로 이 두 컴파일 전략은 나란히 공존할 수 있습니다.
향후 작업
JIT 컴파일러는 매우 유연한 도구이며, 저희는 그 잠재력을 결코 다 끌어내지 못했습니다. Unladen Swallow는 팀이 아직 완전히 구현할 시간을 갖지 못한, 아직 구현되지 않은 성능 최적화 목록 [50]을 관리하고 있습니다. 예시
- Python/Python 인라이닝 [66]. 저희 컴파일러는 현재 순수 Python 함수 사이의 인라이닝을 전혀 수행하지 않습니다. 이에 대한 작업이 진행 중입니다 [68].
- 언박싱 [67]. 언박싱은 수치 연산 성능에 매우 중요합니다. 특히 PyPy는 수치 연산이 많은 워크로드에서 언박싱의 가치를 입증했습니다.
- 재컴파일, 적응. Unladen Swallow는 현재 Python 함수를 그 시점까지의 사용 패턴에 따라 단 한 번만 컴파일합니다. 사용 패턴이 바뀌면, LLVM의 제약 [72] 때문에 새로운 사용 패턴에 더 잘 맞도록 함수를 재컴파일할 수 없습니다.
- 정규 표현식 JIT 컴파일. 최신 JavaScript 엔진은 정규 표현식 성능을 높이기 위해 JIT 컴파일 인프라를 재사용합니다 [73]. Unladen Swallow는 Python 정규 표현식 성능을 위한 벤치마크를 개발했지만([74], [75], [76]), 정규 표현식 성능에 대한 작업은 아직 초기 단계입니다 [77].
- 트레이스 컴파일 [91], [92]. PyPy와 Tracemonkey [93]의 결과를 바탕으로, 저희는 CPython JIT가 어느 정도 트레이스 컴파일을 도입해야 한다고 생각합니다. 저희는 처음에 순수한 트레이싱 JIT 컴파일러를 피하고, 더 단순한 함수 단위 컴파일러를 택했습니다. 하지만 이 함수 단위 컴파일러는 같은 방식으로 구현될 향후 트레이싱 컴파일러의 기반을 마련했습니다.
- 프로파일 생성/재사용. JIT가 수집한 런타임 데이터는 디스크에 저장해 두었다가 이후의 JIT 컴파일에서 재사용하거나, Cython [101] 또는 피드백으로 강화된 코드 커버리지 도구 같은 외부 도구에서 재사용할 수 있습니다.
이 목록은 결코 전부가 아닙니다. 동적 언어 최적화에 관한 방대한 문헌이 있으며, 이러한 최적화는 Unladen Swallow의 LLVM 기반 JIT 컴파일러를 통해 구현될 수 있고 또 구현되어야 합니다 [54].
Unladen Swallow 커뮤니티
Unladen Swallow에 기여해 주신 개발자 커뮤니티, 특히 James Abbatiello, Joerg Blank, Eric Christopher, Alex Gaynor, Chris Lattner, Nick Lewycky, Evan Phoenix, Thomas Wouters께 감사드립니다.
라이선스
Unladen Swallow에 관한 모든 작업은 Google이 PSF와 체결한 포괄적 기여자 라이선스 계약(Contributor License Agreement)의 틀 안에서 Python Software Foundation License v2 [56]의 조건에 따라 Python Software Foundation(PSF)에 라이선스됩니다.
LLVM은 OSI가 승인한 자유로운 라이선스인 University of llinois/NCSA 오픈 소스 라이선스(Open Source License) [58] 하에 라이선스 [57] 됩니다. LLVM의 저작권은 University of Illinois Urbana-Champaign이 단독으로 보유합니다.
참고 문헌
Copyright
This document has been placed in the public domain.