News Feed

C파이썬 vs 파이파이, 어떤 파이썬 런타임의 JIT가 더 뛰어날까?

컨텐츠 정보

  • 조회 472

본문

파이썬 코드를 실행하는 런타임 파이파이(PyPy)는 특별히 제작된 JIT 컴파일러를 사용해서 전통적인 파이썬 런타임인 C파이썬(CPython) 대비 잠재적으로 상당한 속도 향상을 달성한다.

그러나 파이파이의 우수한 성능에는 파이썬 생태계 전반, 특히 C 확장과의 호환성 희생이라는 대가가 따른다. 이 문제는 개선되고 있지만 파이파이 런타임 자체가 최신 파이썬 릴리스와 보조를 맞추지 못하고 뒤처지는 경우가 많다.

한편 최근 C파이썬 릴리스에는 C파이썬의 네이티브 JIT 컴파일러 첫 버전이 포함됐다. 장기적으로 성능 개선을 추구하지만 일부 워크로드에서는 이미 상당한 개선을 확인할 수 있다. 또한 C파이썬에는 GIL을 제거해 완전한 자유 스레딩 연산을 가능하게 하는 새로운 대안 빌드도 추가됐다. 이는 성능 향상을 얻을 수 있는 또 하나의 중요한 경로다.

C파이썬이 더 나은 성능으로 파이파이를 대체할 만한 궤도에 올라설 수 있을까? JIT를 지원하고 GIL을 제거한 최신 C파이썬 빌드를 동일한 벤치마크에서 파이파이와 나란히 테스트했는데, 그 결과가 흥미롭다.

순수 계산에서는 파이파이가 여전히 우위

C파이썬의 고질적인 약점은 단순 수치 연산이다. 필요한 온갖 간접 참조와 추상화 때문이다. C파이썬에는 예를 들어 기계 수준의 원시 정수와 같은 것이 없다.

결과적으로 이와 같은 벤치마크에서 C파이썬은 대체로 성능이 매우 낮게 나온다.

def transform(n: int):    q = 0    for x in range(0, n * 500):        q += x    return qdef main():    return [transform(x) for x in range(1000)]main()

6코어 라이젠 5 3600에서 이 벤치마크를 실행하면 파이썬 3.14는 약 9초가 걸리지만 파이파이는 약 0.2초 만에 끝낸다.

또한 이 워크로드는 적어도 아직까지는 파이썬의 JIT가 효과를 발휘하는 종류의 워크로드는 아니기 때문에 3.14에서 JIT를 활성화해도 시간은 약 8초로, 소폭 줄어드는 데 그친다.

그렇다면 동일한 코드의 멀티스레드 버전을 사용하면서 GIL이 없는 파이썬 버전을 실행하면 어떻게 될까?

def transform(n: int):    q = 0    for x in range(0, n * 500):        q += x    return qdef main():    result = []    with ThreadPoolExecutor() as pool:        for x in range(1000):            result.append(pool.submit(transform, x))    return [_.result() for _ in result]main()

차이는 극적이다. 파이썬 3.14에서 이 작업은 1.7초만에 완료된다. 여전히 파이파이의 1초 미만에는 미치지 못하지만 GIL이 없는 스레드 버전을 사용할 만한 가치는 충분한 도약이다.

파이파이와 스레딩은 어떨까? 아이러니하게도 파이파이에서 멀티스레드 버전을 실행하면 성능이 크게 떨어져서, 작업을 완료하는 데 약 2.1초가 걸린다. 파이파이에는 여전히 GIL과 유사한 잠금 메커니즘이 있어 스레드 간 완전한 병렬 처리가 불가능하기 때문이다. 파이파이의 JIT 컴파일은 모든 작업을 싱글스레드에서 실행할 때 가장 효과적이다.

스레드 풀 대신 프로세스 풀로 바꾸면 도움이 될 것이라고 생각할 수 있는데, 그렇게 보기도 애매하다. 위 코드의 프로세스 풀 버전은 실제로 약간의 속도 향상을 가져와서 시간이 1.3초까지 단축되기는 하지만 파이파이의 프로세스 풀과 멀티프로세싱은 C파이썬에 비하면 최적화가 부족하다.

정리해 보자. ‘기본’ 파이썬 3.14

  • JIT 미사용, GIL 사용 : 9초
  • JIT 사용, GIL 사용 : 8초
  • JIT 미사용, GIL 미사용 : 9.5초

GIL 미사용 빌드는 싱글스레드 연산에서 여전히 정규 빌드에 비해 약간 느리다. JIT는 약간 효과가 있지만 큰 도움은 되지 않는다.

같은 작업을 파이썬 3.14에서 프로세스 풀을 사용해 처리하는 경우는 다음과 같다.

  • JIT 미사용, GIL 사용 : 1.75초
  • JIT 사용, GIL 사용 : 1.5초
  • JIT 미사용, GIL 미사용 : 2초

파이썬 3.14에서 다른 스크립트 형식을 사용하는 경우는 어떨까?

  • GIL 미사용 스레드 버전 : 1.7초
  • GIL 사용 멀티프로세싱 버전 : 2.3초
  • GIL과 JIT 사용 멀티프로세싱 버전 : 2.4초
  • GIL 미사용 멀티프로세싱 버전 : 2.1초

파이파이의 성능은 다음과 같다.

  • 싱글스레드 스크립트 : 0.2초
  • 멀티스레드 스크립트 : 2.1초
  • 멀티프로세싱 스크립트 : 1.3초

n체 문제

기본 파이썬이 취약한 것으로 잘 알려진 또 다른 일반적인 계산 집약적 벤치마크는 n체(n-body) 벤치마크다. 이 문제 역시 병렬 계산으로 속도를 높이기가 어렵다. 가능은 하지만 그 과정이 간단하지 않기 때문에 싱글스레드로 구현하는 편이 가장 쉽다.

n체 벤치마크를 100만 회 반복 실행한 결과는 다음과 같다.

  • 파이썬 3.14, JIT 미사용 : 7.1초
  • 파이썬 3.14, JIT 사용 : 5.7초
  • 파이썬 3.15a4, JIT 미사용 : 7.6초
  • 파이썬 3.15a4, JIT 사용 : 4.2초

JIT 파이썬 버전 치고는 인상적인 성능이다. 그러나 파이파이는 동일한 벤치마크를 0.7초 만에 완료한다.

파이 계산

계산이 많은 파이썬 프로그램에서는 파이파이도 힘겨워한다. 파이(pi)의 자릿수를 계산하는 단순한 구현을 생각해 보자. 마찬가지로 병렬화의 여지가 거의 없어서 싱글스레드 테스트를 사용했다.

2만 자릿수로 실행한 결과는 다음과 같다.

  • 파이썬 3.14, JIT 미사용 : 13.6초
  • 파이썬 3.14, JIT 사용 : 13.5초
  • 파이썬 3.15, JIT 미사용 : 13.7초
  • 파이썬 3.15, JIT 사용 : 13.5초
  • 파이파이 : 19.1초

흔하지는 않지만 파이파이의 성능이 정규 파이썬보다 낮은 상황도 분명히 발생한다. 놀랄 만한 경우는 파이파이가 더 뛰어날 것으로 짐작되는 시나리오에서 정규 파이썬의 성능이 더 높게 나오는 경우다.

다른 종류의 작업에서는 경쟁력을 높이고 있는 C파이썬

필자가 자주 사용하는 또 다른 파이썬 벤치마크는 구글 n-gram 벤치마크의 변형이다. 이 벤치마크는 몇 메가바이트 크기의 CSV 파일을 처리해 관련 통계를 생성한다. 앞선 벤치마크들이 CPU 바운드 성격이 강하다면 이 벤치마크는 I/O 바운드 성격이 강하지만, 런타임 속도에 대한 유용한 정보를 얻을 수 있다.

필자는 이 벤치마크를 싱글스레드, 멀티스레드, 멀티프로세스 3가지 형태로 작성했다. 다음은 싱글 스레드 버전이다.

import collectionsimport timeimport gcimport systry:    print ("JIT enabled:", sys._jit.is_enabled())except Exception:    ...def main():    line: str    fields: list[str]    sum_by_key: dict = {}    start = time.time()    with open("ngrams.tsv", encoding="utf-8", buffering=2 

다양한 스크립트 버전에서 파이썬 3.14가 이 벤치마크를 처리한 결과는 다음과 같다.

  • 싱글스레드, GIL 사용 : 4.2초
  • 싱글스레드, JIT, GIL 사용 : 3.7초
  • 멀티스레드, GIL 미사용 : 1.05초
  • 멀티프로세싱, GIL 사용 : 2.42초
  • 멀티프로세싱, JIT, GIL 사용 : 2.4초
  • 멀티프로세싱, GIL 미사용 : 2.1초

동일한 작업을 파이파이에서 수행한 결과는 다음과 같다.

  • 싱글스레드 : 2.75초
  • 멀티스레드 : 14.3초(오타 아님)
  • 멀티프로세싱 : 8.7초

즉, 이 시나리오에서는 GIL을 사용하지 않는 C파이썬 멀티스레드 버전이 최적의 조건에서 실행하는 파이파이보다도 더 우수한 성능을 낸다. JIT를 활성화하면서 자유 스레딩을 사용하는 C파이썬 빌드는 아직은 존재하지 않지만 머지않아 나올 예정이고, 그렇게 되면 지금의 성능 구도는 더욱 크게 바뀌게 될 것이다.

결론

요약하면, 가장 기본적이고 최적화되지 않은 상태의 계산 집약적인 스크립트 실행에서는 파이파이가 여전히 C파이썬을 앞선다. 그러나 C파이썬은 자유 스레딩, 그리고 가능한 경우 멀티프로세싱을 활용할 때 상대적인 성능 개선 폭이 상당히 크다.

파이파이는 이와 같은 내장된 기능을 활용할 수 없지만 기본 실행 속도가 충분히 빠르므로 일부 작업에서 스레딩이나 멀티프로세싱의 필요성이 크지 않다. 예를 들어 n체 문제는 제대로 병렬화하기가 어렵고 pi 계산은 병렬화가 거의 불가능하기 때문에 싱글스레드로 빠르게 이러한 알고리즘을 실행할 수 있다는 점은 큰 장점이다.

테스트에서 가장 눈에 띄는 점은 파이파이의 성능상 이점이 무조건적이지 않고, 나아가 일관되지도 않다는 부분이다. 시나리오에 따라 양상이 크게 달라진다. 동일한 프로그램 내에서도 다양한 시나리오가 발생할 수 있다. 프로그램에 따라 파이파이에서 매우 빠른 속도로 실행되지만, 문제는 실행하기 전에는 그러한 특성을 알기 어렵다는 점이다. 유일한 방법은 애플리케이션을 벤치마킹하는 것이다.

또 하나 짚고 넘어갈 부분은 파이썬 전반에서 성능과 병렬성을 개선하는 주요 경로 중 하나인 자유 스레딩을 현재 파이파이에서는 사용할 수 없다는 점이다. 또한 파이파이의 경우 프로세스 간 데이터 직렬화 메커니즘이 C파이썬보다 훨씬 느리기 때문에 멀티프로세싱 역시 파이파이에서는 잘 작동하지 않는다.

파이파이가 상황에 따라 아무리 빠른 속도를 낼 수 있다고 해도, 이번 벤치마크 결과는 일부 시나리오에서 스레드를 사용하는 진정한 병렬 처리가 주는 이점을 잘 보여준다. 파이파이 개발자들이 언젠가는 이를 구현할 방법을 찾을 수도 있지만 파이파이와 C파이썬의 내부 구조가 많은 부분에서 다르다는 점을 감안하면 C파이썬의 방식을 그대로 가져와 활용하기는 아마 어려울 것이다.
dl-itworldkorea@foundryco.com

관련자료

댓글 0
등록된 댓글이 없습니다.