Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 3146 – CPython으로의 Unladen Swallow 병합입니다.

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:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 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(적시) 컴파일러를 추가하여 선택된 Python 코드를 최적화된 기계어 코드로 컴파일할 수 있도록 합니다. Unladen Swallow의 JIT 컴파일러는 고전적인 정적 컴파일러 최적화를 넘어 런타임에 수집한 데이터를 활용하여 코드 동작에 관한 검증된 가정을 세우므로, 더 빠른 기계어 코드를 생성할 수 있습니다.

이 PEP는 Unladen Swallow를 별도의 py3k-jit 브랜치로 CPython 개발 트리에 통합하고, 최종적으로 주 py3k 브랜치에 병합하는 것을 목표로 합니다. Unladen Swallow가 결코 완성되었거나 완벽한 것은 아니지만, CPython의 로드맵에 포함할 만한 충분한 성숙도에 도달했다고 판단합니다. 더 광범위한 CPython 개발 팀이 기반으로 삼을 수 있고, 앞으로 수년 동안 성능이 계속 향상될 안정적인 플랫폼을 만들고자 했습니다.

이 PEP에서는 Unladen Swallow의 구현과 CPython 2.6.4와의 차이점, 성능 측정에 사용한 벤치마크, 정확성과 호환성을 보장하는 데 사용한 도구, CPython의 현재 플랫폼 지원에 미치는 영향, CPython 핵심 개발 프로세스에 미치는 영향을 자세히 설명합니다. 이 PEP는 제안된 병합 계획과 향후 작업의 가능한 방향에 관한 간략한 기록으로 결론을 맺습니다.

BDFL에 다음 사항을 요청합니다:

  • 아래에 제시된 설계에 따라 CPython에 JIT 컴파일러를 추가한다는 전체 개념을 승인해 주십시오.
  • CPython 소스 트리에서 JIT 컴파일러 작업을 계속할 수 있도록 허가해 주십시오.
  • 모든 차단 이슈 [31]가 해결되면 최종적으로 JIT 컴파일러를 py3k 브랜치에 병합할 수 있도록 허가해 주십시오.
  • 조랑말 한 마리입니다.

근거, 구현입니다.

많은 기업과 개인은 더 많은 프로젝트에서 Python을 사용할 수 있도록 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에 다시 병합할 수 있도록 설계하는 것입니다.

Google은 내부적으로 CPython 2.4를 사용하고 있으며 CPython 2.4에서 CPython 3.x로 직접 전환하는 것은 실현 불가능하다고 판단했기 때문에 2.6.4를 기준선으로 선택했습니다.

원하는 성능을 달성하기 위해 Unladen Swallow는 Self에 관한 Urs Hoelzle의 작업 [52] 전통을 따른 JIT(적시) 컴파일러 [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 코드만 컴파일하는 데 리소스를 사용하고자 하므로, 주어진 함수가 얼마나 자주 실행되는지 평가하기 위해 온라인 휴리스틱을 사용합니다. 함수의 핫니스 값이 주어진 임계값을 넘으면 해당 함수가 컴파일 및 최적화 대상으로 선택됩니다. 그러나 함수가 핫하다고 판단될 때까지는 표준 CPython 평가 루프에서 실행되며, Unladen Swallow에서는 이 루프에 실행된 각 바이트코드에 관한 유용한 데이터를 기록하도록 계측이 추가되어 있습니다. 이 런타임 데이터는 생성된 기계어 코드의 유연성을 줄여 일반적인 경우에 맞게 최적화할 수 있도록 하는 데 사용됩니다. 예를 들어, 다음에 관한 데이터를 수집합니다.

  • 분기문이 실행되었는지 또는 실행되지 않았는지에 관한 데이터입니다. 분기문이 실행되는 일이 전혀 없다면 이를 기계어 코드로 컴파일하지 않습니다.
  • 연산자가 사용하는 타입에 관한 데이터입니다. a + b가 정수만 더하는 것으로 확인되면, 해당 코드 조각을 위해 생성된 기계어 코드는 실수를 더하는 것을 지원하지 않습니다.
  • 각 호출 위치에서 호출되는 함수에 관한 데이터입니다. 특정 foo() 호출 위치가 항상 동일한 foo 함수를 호출하는 것으로 확인되면, 호출을 최적화하거나 해당 호출을 인라인 처리하여 제거할 수 있습니다.

수집되는 데이터 항목의 전체 목록과 그 사용 방법은 [55]을 참조하십시오.

그러나 역사적으로 실행되지 않던 분기문이 우연히 이제 실행되거나, 정수에 맞게 최적화된 a + b 코드 조각에 두 문자열이 전달되는 경우에는 이를 지원해야 합니다. Python의 의미론을 변경할 수는 없습니다. 이러한 최적화된 기계어 코드의 각 부분 앞에는 guard가 있으며, 이 가드는 최적화할 때 세운 단순화 가정이 여전히 성립하는지 확인합니다. 가정이 여전히 유효하면 최적화된 기계어 코드를 실행하고, 유효하지 않으면 인터프리터로 되돌아가 중단했던 지점부터 다시 시작합니다.

코드 생성과 코드 최적화를 위해 LLVM [4]이라고 하는 기존 컴파일러 라이브러리 집합을 재사용하기로 했습니다. 덕분에 소규모 팀이 여러 기계 명령어 집합에 대한 코드 생성을 이해하고 디버깅하거나, 대규모의 고전적인 컴파일러 최적화를 구현할 필요가 없어졌습니다. 이러한 코드 재사용 없이는 이 프로젝트가 가능하지 않았을 것입니다. LLVM은 수정하기 쉬웠으며, LLVM 커뮤니티는 우리의 제안과 수정 사항을 기꺼이 받아들인다는 사실을 확인했습니다.

어느 정도 더 자세히 설명하면, Unladen Swallow의 JIT는 CPython 바이트코드를 LLVM 자체의 중간 표현(IR) [94]로 컴파일하며, CPython 평가 루프의 모든 런타임 데이터를 고려합니다. 그런 다음 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 전용 어노테이션을 수동으로 추가해야 합니다.

    이러한 정적 컴파일러는 참조 카운팅을 걱정하지 않고 확장 모듈을 작성하는 데 유용하지만, 정적 선행 컴파일러이므로 런타임 데이터에 기반하여 동작하는 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는 임베딩 API를 제공하지 않고 CPython과 동일한 API는 더더욱 제공하지 않습니다.
  • PyV8: PyV8 [110]은 V8 위에서 실행되는 알파 단계의 실험적 Python-to-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와 같은 확장 모듈의 핵심 루틴은 C로 구현되어 있어 그 성능에는 관심이 없습니다. 마찬가지로 GUI, 데이터베이스 또는 소켓을 많이 사용하는 애플리케이션처럼 IO가 많은 워크로드는 인터프리터 또는 코드 생성 최적화를 정확하게 측정하지 못한다고 저희는 생각합니다. 그렇다고 해도 표준 라이브러리의 C 언어 확장 모듈 성능을 향상할 여지는 분명히 있으므로, cPicklere 모듈에 대한 벤치마크를 추가했습니다.

CPython 2.6.4와의 성능 비교

아래 차트는 CPython 2.6.4와 Unladen Swallow에 대해 여러 벤치마크 반복 실행의 산술 평균을 비교합니다. perf.py는 이보다 더 많은 데이터를 수집하며, 실제로 산술 평균이 전부를 말해 주는 것은 아니지만, 간결성을 위해 평균만 재현합니다. 결과의 유의성을 나타내기 위해 95% 신뢰 구간에서 Student의 양측 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

이러한 벤치마크 중 다수는 Unladen Swallow에서 성능이 저하되는데, 현재 버전이 Python 함수를 기계어로 컴파일하기 위해 실행을 차단하기 때문입니다. 예를 들어 html5librietveld 벤치마크의 타임라인 그래프에서 볼 수 있는 동작이 나타나며, 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-bit

벤치마크 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_startuphg_startup은 각각 Bazaar와 Mercurial이 도움말 화면을 표시하는 데 걸리는 시간을 측정합니다. startup_nositepython -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의 정적 코드 생성 기능, 도구 및 최적화 패스는 안정적이고 충분한 스트레스 테스트를 거쳤지만, 저스트인타임 인프라는 상대적으로 테스트가 부족했고 버그가 많았습니다. 우리는 이를 해결했습니다.

(우리의 가설은 다른 프로젝트들이 피했던 이러한 문제를 CPython 표준 라이브러리 테스트 스위트의 복잡성과 철저함 때문에 겪었다는 것입니다.)

또한 엔지니어링 역량을 성능에서 gdb와 oProfile 같은 지원 도구로 전환했습니다. gdb는 JIT 컴파일러와 전혀 잘 작동하지 않았으며, LLVM은 이전에 oProfile과 통합되어 있지 않았습니다. JIT를 인식하는 디버거와 프로파일러를 갖춘 것은 프로젝트에 매우 큰 도움이 되었으며, 이러한 방향에 시간을 투자한 것을 후회하지 않습니다. 자세한 내용은 DebuggingProfiling 섹션을 참조하십시오.

LLVM 기반 CPython JIT가 과연 5배의 성능 목표를 달성할 수 있습니까? JIT 기반 JavaScript 구현의 벤치마크 결과는 5배 향상이 실제로 가능함을 보여 주며, PyPy의 JIT가 수치 워크로드에서 달성한 결과도 이를 뒷받침합니다. Self-92의 경험 [52]도 유익한 시사점을 제공합니다.

LLVM이 이를 달성할 수 있습니까? 우리는 LLVM 기반 JIT가 제공할 수 있는 기능의 표면을 이제 막 긁기 시작했을 뿐이라고 생각합니다. 지금까지 이 시스템에 통합한 최적화는 상당한 성과를 거두었습니다(예를 들어, [88], [89], [90]). 현재까지의 경험에 따르면 Unladen Swallow의 성능을 제한하는 요인은 관련 문헌을 구현하는 데 필요한 엔지니어링 주기입니다. LLVM은 다루고 수정하기 쉬웠으며, 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를 사용하므로, intPy_ssize_t로 변환하는 작업과 이와 유사한 API 변경이 필요했습니다.
  • 명시적이면서 잘못된 CPython 버전 번호 테스트 수정 또는 비활성화
  • 이후 수정된 CPython 2.4의 버그를 우회하거나 이에 의존하던 코드를 조건부로 비활성화

이처럼 광범위한 공개 및 독점 애플리케이션과 라이브러리를 대상으로 테스트한 것은 Unladen Swallow의 정확성을 보장하는 데 중요한 역할을 했습니다. 테스트를 통해 버그가 발견되었으며, 이를 적절히 수정했습니다. 자동화된 회귀 테스트 체계 덕분에 프로젝트를 진행해 오면서 변경 사항에 대해 높은 확신을 얻을 수 있었습니다.

제3자 테스트에 더해, 테스트되지 않았거나 사양이 불충분하다고 판단한 언어 또는 구현의 코너 케이스를 위해 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가 가장 많은 테스트를 받은 플랫폼도 이러한 플랫폼입니다. x86 및 x86-64 하드웨어에 대한 LLVM/Unladen Swallow의 지원을 확신합니다. PPC 및 ARM 지원도 존재하지만 널리 사용되지는 않으며 버그가 있을 수 있습니다(예: [99], [83], [100]).

Unladen Swallow는 다음 운영 체제에서 작동하는 것으로 알려져 있습니다: Linux, Darwin, Windows. Unladen Swallow는 Linux와 Darwin에서 가장 많은 테스트를 받았지만, Windows에서도 여전히 빌드되고 테스트를 통과합니다.

LLVM의 JIT가 작동하지 않는 하드웨어 및 소프트웨어 플랫폼을 지원하기 위해 Unladen Swallow는 ./configure --without-llvm 옵션을 제공합니다. 이 플래그는 LLVM에 의존하는 Unladen Swallow의 모든 부분을 제외하여, 작동하고 테스트를 통과하지만 성능상의 이점은 없는 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는 Linux 시스템에서 어셈블리 수준의 프로파일링을 지원하기 위해 oProfile 0.9.4 및 이후 버전 [18]을 통합합니다. 이는 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는 핵심 CPython 개발자에게 더 유용하도록 oProfile 통합을 개선하는 데 중요하다고 판단하는 항목의 작업 목록을 관리합니다 [19].

주요 장점:

  • Linux의 oProfile에서 JIT 컴파일된 프레임의 심볼화가 작동합니다.

개선이 미흡한 점:

  • Apple의 Shark [20] 및 Microsoft의 Visual Studio 프로파일링 도구를 위한 JIT 컴파일 프레임의 심볼화 개선에는 아직 어떠한 작업도 투자하지 않았습니다.
  • oProfile 출력에는 아직 일부 다듬을 부분이 있습니다.

oProfile의 x86-64 플랫폼에서 이미 수정된 버그를 우회하려면 oProfile 0.9.5 이상을 사용할 것을 권장합니다. 그러나 oProfile 0.9.4도 32비트 플랫폼에서는 잘 작동합니다.

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 코드베이스의 상대적으로 작은 일부로 제한됩니다.
  • C++는 ./configure --without-llvm를 통해 비활성화할 수 있으며, 이 경우 libstdc++ 의존성도 제외됩니다.

개선이 미흡한 점:

  • 개발자는 CPython 내부의 모든 영역에서 작업하려면 서로 관련된 두 언어인 C와 C++를 알아야 합니다.
  • C++ 스타일 가이드를 개발하고 시행해야 합니다. PEP 7은 C++까지 포괄하도록 확장되며 [119], Unladen Swallow [69], LLVM [70] 및 Google [71]의 C++ 스타일 가이드에서 관련 부분을 취합니다.
  • 서로 다른 C++ 컴파일러는 서로 다른 ABI를 생성하므로, CPython을 한 C++ 컴파일러로 컴파일하고 확장 모듈을 다른 C++ 컴파일러로 컴파일하면 문제가 발생할 수 있습니다.

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는 역사적으로 LLVM 및 Clang 소스 트리의 사본을 Unladen Swallow 트리에 포함해 왔습니다. 이는 LLVM에 패치를 적용하면서 LLVM 트렁크를 면밀히 추적할 수 있도록 하기 위한 것이었습니다. CPython에는 이러한 개발 모델을 권장하지 않습니다. CPython 릴리스는 공식 LLVM 릴리스를 기반으로 해야 합니다. Darwin용 사전 빌드된 LLVM 패키지는 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]).

단일 C 소스 파일을 수정한 후의 configure, 전체 빌드 및 증분 빌드를 비롯하여 Python 빌드에 필요한 시간에 Unladen Swallow가 미치는 영향을 조사했습니다.

./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라고 부릅니다. 이는 CPython py3k 브랜치의 브랜치가 됩니다.
  • 이 브랜치를 py3k와 긴밀하게 통합된 상태로 유지합니다. 더 많이 벗어날수록 작업은 더 어려워집니다.
  • JIT 관련 패치는 모두 py3k-jit 브랜치에 적용합니다.
  • JIT와 관련 없는 패치는 py3k 브랜치에 적용한 후(검토 및 승인되면) py3k-jit 브랜치에 다시 병합합니다.
  • 새로운 명령줄 플래그나 환경 변수의 도입과 같이 논쟁의 여지가 있는 사안은 python-dev에서 논의합니다.

Google은 내부적으로 CPython 2.x를 사용하므로 Unladen Swallow는 CPython 2.6을 기반으로 합니다. 컴파일러를 Python 3으로 이식해야 하며, 브랜치가 항상 일관된 Python 3 구현으로 유지되도록 py3k-jit 브랜치에 패치를 적용하면서 이를 수행합니다.

py3k로의 최종 병합을 방해하는 남은 문제를 해결하는 동안 이 접근 방식은 3.2 또는 3.3 릴리스 프로세스를 최소한으로 방해할 것이라고 믿습니다. Unladen Swallow는 최종 병합 전에 해결해야 할 알려진 문제의 목록을 관리하며 [31], 여기에는 이 PEP에서 언급된 모든 문제가 포함되어 있습니다. CPython 커뮤니티에도 자체적인 우려 사항이 있을 것으로 생각합니다. 이 목록은 고정되어 있지 않으며, 향후 py3k 브랜치로의 최종 병합을 방해할 다른 문제가 발생할 수 있습니다.

변경 사항은 py3k-jit 브랜치에 직접 커밋하고, 규모가 크거나 까다롭거나 논쟁의 여지가 있는 변경 사항만 커밋 전 코드 검토를 위해 보냅니다.

비상 계획

메모리 사용량이나 시작 시간을 CPython 커뮤니티가 만족할 수준까지 줄이지 못할 가능성이 있습니다. 이 상황에 대한 주요 비상 계획은 온라인 JIT 컴파일 전략에서, 계측된 CPython 인터프리터 루프를 사용하여 피드백을 얻는 오프라인 AOT 전략으로 전환하는 것입니다. 이는 gcc의 피드백 기반 최적화(-fprofile-generate) [111] 및 Microsoft Visual Studio의 프로파일 기반 최적화 [112]에서 사용하는 것과 동일한 모델입니다. 여기서는 이를 “피드백 기반 최적화” 또는 FDO라고 부릅니다.

Python용 FDO 컴파일러는 JIT 컴파일러보다 열등하다고 생각합니다. FDO에는 고품질의 대표성 있는 벤치마크 모음이 필요하지만, 이는 오픈 소스와 클로즈드 소스 개발 모두에서 비교적 드문 편입니다. JIT 컴파일러는 모든 애플리케이션에서 핫스팟을 동적으로 찾아 최적화할 수 있습니다 – 벤치마크 모음이 있든 없든 – 이를 통해 사람의 개입 없이 애플리케이션 병목 현상의 변화에 적응할 수 있습니다.

선행 컴파일 방식의 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에 관한 모든 작업은 PSF와 체결한 Google의 포괄적 기여자 라이선스 계약의 틀 아래, Python Software Foundation License v2 [56]의 조건에 따라 Python Software Foundation(PSF)에 라이선스가 부여됩니다.

LLVM은 관대한 OSI 승인 라이선스인 University of llinois/NCSA Open Source License [58]에 따라 라이선스가 부여됩니다 [57]. University of Illinois Urbana-Champaign은 LLVM의 유일한 저작권자입니다.

참고 자료