파이썬 성능에 관한 신화와 괴담

ko생성일: 2025. 8. 24.갱신일: 2025. 8. 27.

PyPy 개발자 Antonio Cuni가 EuroPython 2025에서 파이썬 성능을 둘러싼 통념을 반박하고, 동적 특성과 메모리 관리가 만드는 근본적 한계를 짚으면서, 초고속 파이썬을 지향하는 초기 연구 프로젝트 SPy를 소개한다.

LWN 구독자 혜택 LWN를 구독하는 가장 큰 가치는 LWN의 발행을 지속하도록 돕는 데 있지만, 그 외에도 구독자는 사이트의 모든 콘텐츠에 즉시 접근하고 여러 추가 기능을 이용할 수 있습니다. 오늘 가입해 주세요!

오랫동안 파이썬 성능 엔지니어이자 PyPy 개발자인 Antonio Cuni가 프라하에서 열린 EuroPython 2025 첫날, “파이썬 성능을 둘러싼 신화와 동화”라는 주제로 발표를 했다. 제목에서 짐작할 수 있듯, 그는 파이썬 성능에 관한 통념의 상당수가 기껏해야 오해라고 본다. 그는 다수의 예시를 통해 자신이 보는 진짜 문제들이 어디에 있는지를 보여주었다. 궁극적으로는 메모리 관리가 파이썬 성능 개선의 한계를 규정하게 될 것이라는 결론에 이르렀지만, SPy라는 초기 단계 프로젝트가 초고속 파이썬으로 가는 한 경로가 될 수 있다고 본다.

그는 “파이썬은 느리거나 충분히 빠르지 않다”고 생각하는 사람은 손을 들어보라고 청중에게 부탁하며 시작했다. 많은 손이 올라왔고, 그가 PyCon Italy에서 같은 발표를 했을 때와는 꽤 달랐다. 그곳에서는 거의 아무도 손을 들지 않았다고 한다. “아주 다른 청중이네요”라고 그는 미소 지으며 말했다. 그는 여러 해 동안 파이썬 성능에 매달려 왔고, 많은 파이썬 개발자들과 대화해 오면서 고착화된 몇 가지 신화를 들어왔는데, 그것들을 바로잡고 싶어 한다.

신화

첫 번째는 “파이썬은 느리지 않다”는 주장이다. 손들기가 보여주듯, 대부분의 참석자는 이미 그것이 신화임을 알고 있다고 그는 봤다. 요즘은 파이썬이 접착(glue) 언어라서 속도는 별로 중요하지 않다는 말도 듣는다. “요즘은 GPU만 중요하다”는 것이다. 물론 파이썬이 어떤 작업들에는 충분히 빠르다. 그래서 많은 사람들이 파이썬을 쓰고, EuroPython 같은 컨퍼런스에 참석하는 것이다.

파이썬이 충분히 빠른 프로그램들의 집합이 있지만, 그 집합이 쓰이는 모든 파이썬 프로그램을 포함하는 것은 아니다—부분집합에 불과하다. 더 높은 파이썬 성능이 필요한 프로그램들이 인터프리터 최적화의 다양한 노력을 이끌어내고 있으며, 동시에 개발자들이 자신의 프로그램 성능을 높이기 위해 끊임없이 노력하게 만든다. 종종 Cython, Numba 같은 도구를 사용하면서 말이다.

이미지 1: [Antonio Cuni]

그의 슬라이드에서는 두 집합을 원으로 표현했다. “파이썬이 충분히 빠른 프로그램들”이 “파이썬 프로그램들” 원 안에 완전히 들어가 있고, 그 밖을 “가능한 모든 프로그램들”이 완전히 감싸고 있다. 그의 이상적인 세계에서는 가능한 모든 프로그램이 파이썬으로 작성될 수 있어야 한다. 현재로서는 프로세서의 모든 성능을 필요로 하는 프로그램은 파이썬을 사용할 수 없다. 그는 더 많은 프로그램에 파이썬을 쓸 수 있도록 내부의 원들이 커지기를 바란다.

“그저 접착 언어일 뿐”이라는 주장에 대한 당연한 결론은 “핫한 부분만 C/C++로 다시 쓰면 된다”는 것이다. 다만 요즘은 “러스트로 다시 써야 한다”고들 한다. 완전히 틀린 얘기는 아니다. 그것은 코드를 가속하는 좋은 기법이지만, 곧 “벽에 부딪힌다”. 파레토 원리—이유는 불분명하지만 ChatGPT가 만든 슬라이드로 설명—에 따르면, 실행 시간의 80%가 코드의 20%에서 소비된다. 따라서 그 20%를 최적화하면 도움이 된다.

하지만 그 다음에는 암달의 법칙에 부딪힌다. 코드의 한 부분을 최적화해도 그 부분에 소요되는 시간에 의해 전체 개선 폭이 제한된다. “핫한 부분이 이제는 아주 아주 빨라졌고, 그러면 나머지 전부를 최적화해야 한다”는 것이다. 그는 어떤 inner() 함수가 전체 시간의 80%를 쓰던 다이어그램을 보여주었는데, 그것을 예컨대 원래의 10% 시간으로 줄이면 이제는 프로그램의 나머지 부분이 실행 시간을 지배하게 된다.

또 다른 “신화”는 파이썬이 느린 이유가 인터프리터이기 때문이라는 것이다. 이것도 일면 사실이지만, 인터프리트 자체는 파이썬을 느리게 만드는 요인의 작은 부분일 뿐이다. 그는 간단한 파이썬 표현식 하나를 예로 들었다:

p.x * 2

C/C++/Rust용 컴파일러는 이런 식의 표현을 세 연산으로 바꿀 수 있다: 값

x

을 로드하고, 2를 곱한 뒤, 결과를 저장한다. 하지만 파이썬에서는 해야 할 작업의 목록이 길다. 먼저

p

의 타입을 찾고,

getattribute()

메서드를 호출하며, 언박싱을 통해

p.x

2

를 처리하고, 마지막으로 결과를 박싱해야 하는데, 여기에 메모리 할당이 필요하다. 이 모든 것은 파이썬이 인터프리터인지 여부와 무관하며, 언어의 의미론 때문에 필요한 단계들이다.

정적 타입

이제는 사람들이 파이썬에서 정적 타입을 쓰기 때문에, 언어용 컴파일러가 방금 나열한 단계들을 건너뛰고 곧바로 연산을 수행할 수 있을 것이라고들 말한다. 그는 다음 예시를 들었다:

def add(x: int, y: int) -> int:
    return x + y

print(add(2, 3))

하지만 정적 타입은 런타임에 강제되지 않으므로, 정수가 아닌 인자를 사용해

add()

를 호출하는 방법은 여러 가지다. 예컨대:

print(add('hello ', 'world')) # type: ignore

이는 완전히 유효한 코드이고, 주석 덕에 타입 체커도 만족한다. 하지만 문자열 덧셈은 정수의 덧셈과 다르다. 정적 타입은 “최적화와 성능의 관점에서 완전히 쓸모가 없다”. 게다가 다음 코드도 합법적인 파이썬이다:

class MyClass:
    def __add__(self, other):
        ...

def foo(x: MyClass, y: MyClass) -> MyClass:
    return x + y

del MyClass.__add__

“파이썬의 정적 컴파일은 모든 것이 바뀔 수 있기 때문에 문제적이다”라고 그는 말했다.

그래서 아마도 “JIT 컴파일러가 모든 문제를 해결해줄 것”이라고 생각할 수 있다. JIT은 파이썬이나 어떤 동적 언어든 상당히 빠르게 만들어줄 수 있다. 하지만 그건 “더 미묘한 문제”를 낳는다. 그는 트릴레마 삼각형 슬라이드를 띄웠다: 동적 언어, 속도, 그리고 단순한 구현. 셋 중 둘은 가질 수 있지만, 셋 다는 불가능하다.

파이썬은 역사적으로 동적이며 단순한 구현을 택해왔지만, CPython JIT 컴파일러 같은 프로젝트로 동적이면서도 빠른 언어 쪽으로 움직이고 있다. 그 대가로 단순한 구현을 잃지만, “앞줄에 앉아 그 일을 대신해주는 사람들이 있으니 저는 신경 쓰지 않아도 된다”고 그는 웃었다.

실전에서는 JIT을 쓰면 성능을 예측하기가 어렵다. PyPy에서의 경험, 그리고 고객을 위해 파이썬 성능을 개선하는 컨설턴트로서의 경험에 비추어 보면, 최고의 성능을 내려면 JIT이 무엇을 할지까지 고려해야 한다. 그것은 복잡하고 오류가 나기 쉬운 과정이다. 그는 “코드가 너무 복잡해서 PyPy 컴파일러의 최적화를 촉발하지 못하는” 상황을 겪었다.

이 모든 것은 그가 “최적화 쫓기(optimization chasing)”라고 부르는 현상으로 이어진다. 느린 프로그램이 빠른 경로가 최적화되어 빨라지고 모두가 행복해한다. 그러다 그 추가 속도에 의존하기 시작하는데, 프로그램 어딘가에 겉보기에는 무관한 변경이 생기면 그 속도가 갑자기 사라질 수 있다. 그의 최애 사례는 PyPy(파이썬 2 사용)에서 돌던 프로그램이 갑자기 10배 느려진 일이었다. 문자열 딕셔너리에 유니코드 키가 쓰이면서 JIT이 코드를 디옵티마이즈하게 되었고, 그 결과 모든 것이 훨씬 느려졌다.

동적

그는 그리 흥미롭거나 유용하지는 않지만, 파이썬 컴파일러가 겪는 문제를 보여주는 코드를 내놓았다:

import numpy as np

N = 10

def calc(v: np.ndarray[float], k: float) -> float:
    return (v * k).sum() + N

컴파일러는 이 코드에서 사실상 아무것도 가정할 수 없다. 겉보기에는 평범하게 NumPy를 임포트하고,

calc()

함수는

v

배열의 각 원소에

k

를 곱하고,

sum()

으로 모두 더한 뒤,

N

을 더한다. 하지만 우선,

import

가 반드시 NumPy를 들여온다고 할 수 없다. 어딘가의 임포트 훅이 전혀 예상치 못한 일을 할 수도 있다.

N

이 10이라고도 가정할 수 없다. 코드 어딘가에서 바뀔 수 있기 때문이다. 앞의

add()

함수와 마찬가지로,

calc()

의 타입 선언도 철석같은 보장이 아니다.

하지만 거의 모든 경우에 이 코드는 보이는 대로 작동한다. 개발자들은 언어가 허용한다고 해서 이런 종류의 일을 자주 하지는 않는다. 다만 개발자들이 보통 파이썬을 작성하는 방식과 언어 정의 사이의 간극이 “인터프리터의 삶을 복잡하게 만든다”. 실제로는 파이썬이 허용하는 많은 일들이 일어나지 않는다.

파이썬을 느리게 만드는 것은 극도로 동적인 본성인데, “동시에 그게 파이썬을 아주 멋지게 만드는 이유이기도 하다”. 동적 기능은 99%의 시간에는 필요 없지만, Cuni는 “남은 1%가 파이썬을 훌륭하게 만드는 데 필요하다”고 말했다. 라이브러리들은 종종 언어의 동적 성질에 의존하는 패턴을 사용해 최종 사용자들이 “예쁘게” 쓸 수 있는 API를 만든다. 따라서 그 기능들을 단순히 제거할 수는 없다.

게임

이제 “컴파일러 게임”이다. 그는 점점 더 많은 코드 스니펫을 보여주며 컴파일러가 실제로 코드에 대해 알 수 있는 바가 얼마나 적은지를 지적했다. 이 코드는 뭔가 오류를 낼 것처럼 보인다:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def foo(p: Point):
    assert isinstance(p, Point)
    print(p.name) # ???

foo()

내부에서 컴파일러는

p

Point

임을 안다. 그런데

Point

에는

name

속성이 없다. 하지만 물론 파이썬은 동적 언어다:

def bar():
    p = Point(1, 2)
    p.name = 'P0'
    foo(p)

한편, 다음 예에서는 아예 메서드가 존재한다고 가정할 수도 없다:

import random

class Evil:
    if random.random() > 0.5:
        def hello(self):
            print('hello world')

Evil().hello() # 🤷🏻‍♂️

합법적인 파이썬이지만, “제발 프로덕션에는 이렇게 정의하지 말았으면 한다”고 그는 웃으며 말했다. “절반은 여전히 동작하고, 절반은 예외를 던진다. 컴파일 잘 해보시라.”

또 다른 예에서는 다음 함수를 보여주었다:

def foo():
    p = Person('Alice', 16)
    print(p.name, p.age)
    assert isinstance(p, Person) # <<<

Person

클래스는 아직(여기선) 보이지 않지만, 빈 클래스(그저

pass

)인

Student

가 있다. 이 경우

assert

는 실패한다. 왜냐하면

Person

의 정의가 다음과 같기 때문이다:

class Person:
    def __new__(cls, name, age):
        if age < 18:
            p = object.__new__(Student)
        else:
            p = object.__new__(Person)
        p.name = name
        p.age = age
        return p

“듄더-뉴(즉 new())를 가진 클래스가 전혀 관련 없는, 그 클래스의 인스턴스가 아닌 것을 반환할 수 있다. 최적화 잘 해보시라.”

게임의 마지막 참가자는 다음과 같다:

N = 10

@magic
def foo():
   return N

그는

@magic

데코레이터를 ‘설탕 걷기’(de-sugar)하고 몇 가지 단언을 덧붙였다:

def foo():
   return N

bar = magic(foo)

assert foo.__code__ == bar.__code__
assert bar.__module__ == '__main__'
assert bar.__closure__ is None

assert foo() == 10
assert bar() == 20 # 🤯😱

foo()

bar()

의 코드 오브젝트는 동일하지만, 결과는 다르다. 예상할 수 있듯

N

의 값이

magic()

에 의해 바뀌었다. 코드는 다음과 같다:

def rebind_globals(func, newglobals):
    newfunc = types.FunctionType(
        func.__code__,
        newglobals,
        func.__name__,
        func.__defaults__,
        func.__closure__)
    newfunc.__module__ = func.__module__
    return newfunc

def magic(fn):
    return rebind_globals(fn, {'N': 20})

이 함수는 (넘겨진

foo()

를)

전역 변수들의 값을 다르게 바라보는 버전으로 돌려준다. 다소 엉뚱한 예처럼 보일지 모르지만, 그는 수 년 전 pdb++ 파이썬 디버거를 위해 매우 비슷한 코드를 썼다고 한다. “그럴 만한 충분한 이유가 있었다고 주장하겠다”고 그는 웃었다.

추상화

그가 게임에서 보여준 것처럼 언어 차원에서 고려해야 할 부분들이 있지만, 더 근본적인 문제가 있다. “파이썬에서 추상화는 공짜가 아니다.” 코드를 작성할 때 우리는 성능을 원하면서도, 동시에 코드가 이해하기 쉽고 유지보수 가능하길 바란다. 그에는 대가가 따른다. 그는 간단한 함수로 시작했다:

def algo(points: list[tuple[float, float]]):
    res = 0
    for x, y in points:
        res += x**2 * y + 10
    return

부동소수점 쌍의 튜플로 표현된 점들의 리스트를 받아 계산한다. 이어서 계산을 별도 함수로 분리했다:

def fn(x, y):
    return x**2 * y + 10

이것만으로도 원래보다 느려진다. 함수 호출에는 오버헤드가 있기 때문이다. 함수를 찾아야 하고, 프레임 오브젝트를 만들어야 하는 등등. JIT이 도움이 될 순 있지만, 그래도 더 많은 오버헤드가 붙는다. 그는 한 걸음 더 나아가

Point

데이터 클래스를 썼다:

@dataclass
class Point:
    x: float
    y: float

def fn(p):
    return p.x**2 * p.y + 10

def algo(items: list[Point]):
    res = 0
    for p in items:
        res += fn(p)
    return

당연히 이건 더 느려진다. 조작된 예시이긴 하지만, 요지는 모든 추상화에는 비용이 있고, “그러다 보면 아주 느린 프로그램으로 귀결된다”는 것이다. 이것은 그가 “파이썬에서 파이썬으로(Python to Python)” 하는 추상화의 예로, 언어 내부에서만 리팩터링을 하는 경우다.

“파이썬에서 C로(Python to C)” 하는 추상화, 즉 코드의 핫한 부분을 C나 다른 컴파일된 언어로 떼어내는 방식 역시 비용이 붙는다. 상상해 보자. 파이썬 구현이 점점 더 최적화되어 Point 객체 리스트가 박싱 없이 단순한 부동소수점 선형 배열로 표현된다고 하자. 그런데 fn()이 파이썬의 C API를 대상으로 작성되었다면, 그 숫자들은 (양방향으로) 박싱과 언박싱을 해야 하고, 이는 완전히 낭비다. 이는 “현재 C API로는 피할 수 없다”. PyPy에서 돌아가는 프로그램을 가속하는 방법 중 하나는 C 코드를 제거하고 계산을 파이썬에서 직접 수행하게 하는 것이었는데, PyPy가 이를 잘 최적화할 수 있었기 때문이다.

방 안의 코끼리

파이썬 성능과 관련해 방 안의 코끼리가 하나 있는데, 그는 거의 이 얘기를 듣지 못한다고 한다. 바로 메모리 관리다. 오늘날 하드웨어에서는 “연산은 매우 싸다.” 하지만 메모리가 병목이다. 데이터가 어느 레벨의 캐시에 있으면 접근 비용이 싸지만, RAM 접근은 꽤 느리다. “일반적으로, 아주 아주 좋은 성능을 원한다면 캐시 미스를 가능한 한 피해야 한다.”

하지만 파이썬은 캐시에 친화적이지 않은 메모리 레이아웃을 만들기 쉽다. 그는 간단한 예를 보였다:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = [Person('Alice', 16), Person('Bob', 21)]

Person

에는 두 개의 필드가 있고, 이상적으로는 이들이 메모리에서 서로 인접해야 하며, 리스트의 두 객체 또한 서로 인접해야 캐시 친화적이다. 하지만 실제로는 그 객체들이 메모리 전체에 흩어진다. 그는 Python Tutor의 시각화를 보여주었다. 각 화살표는 따라가야 하는 포인터를 의미하며, 즉 잠재적 캐시 미스다. 이렇게 단순한 자료구조에도 화살표가 10개 가까이 있었다.

“이건 JIT 컴파일러로만 해결할 수 있는 종류의 문제가 아니다. 의미론을 바꾸지 않고는 불가능하다.” 파이썬은 본질적으로 캐시 친화적이지 않다고 그는 말한다. “그리고 솔직히 이 문제를 어떻게 풀어야 할지 모르겠다.” 그의 “슬픈 진실” 결론은 “호환성을 깨지 않고는 파이썬이 초고속이 될 수 없다”는 것이다. 그가 발표에서 설명한 몇 가지 동적 기능(“그걸 광기라고 부르자”)들은 결국 성능 개선을 가로막게 된다. “그 광기를 유지하고 싶다면, 일부 성능은 테이블 위에 남겨둬야 한다.”

그의 다음 슬라이드는 “The end”였고, 슬픔 이모지(“😢💔🥹”)까지 달려 있었으며, 작년 PyCon Italy에서는 여기서 발표를 끝냈다고 한다. 하지만 이번에는 “작은 희망”을 주고 싶어 물음표를 덧붙였고, 호환성을 깨지 않으면 파이썬이 초고속이 될 수 없다는 점을 다시 강조했다.

그는, 파이썬이 최상급 성능을 지향해야 한다고 공동체가 결정한다면(그는 그렇게 되기를 바란다), 커뮤니티에 제안이 있다고 한다. 물론 “아니오”라고 말하는 것도 괜찮다. 그의 제안은 언어 의미론을 약간 손보는 것이다. 동적 기능을 실제로 유용한 곳에 유지하되, 언제든지 어디서나 동적 변화를 허용하는 대신 특정 시점이나 범위로 제한하여, 컴파일러가 일정한 동작과 구조에 의존할 수 있게 하자는 것이다. “지금처럼 언제든 세상이 바뀌어도 되는 상태로 두지 말자.”

한편, 타입 시스템은 성능을 염두에 두고 개편되어야 한다. 현재 타입은 선택적이며 강제되지 않으므로 최적화에 쓸 수 없다. 의도는 성능 지향 코드를 파이썬으로 작성할 수 있게 하는 것이지, 파이썬에서 호출하는 다른 언어로 작성하게 하려는 것이 아니다. 다만 여전히 다른 언어 호출이 바람직한 경우에는 (예: 박싱 같은) 추가 비용을 없애야 한다. “무엇보다도, 우리는 파이썬다운(Pythonic) 무언가를 원한다. 우리가 이 언어를 좋아하지 않았다면 여기 있지 않았을 것이다.”

Cuni는 자신에게 잠재적 해법이 있다고 말했다. “그것은 파이썬을 더 빠르게 만드는 것이 아니다.” 그는 그것이 불가능하다고 본다. SPy는 “Static Python”의 약자로, 성능 문제를 다루려고 그가 몇 년 전 시작한 프로젝트다. SPy에는 모든 표준 주의사항이 적용된다. “진행 중인 작업이고, 연구 및 개발이며, 어디로 갈지 모른다.” 가장 좋은 정보는 위의 GitHub 페이지나 5월 말 PyCon Italy에서의 SPy 발표에서 확인할 수 있다.

그는 카메라에서 실시간으로 에지 검출을 하는 간단한 데모를 보여주었다. PyScript를 이용해 브라우저에서 동작한다. 데모는 왼쪽에 원본 카메라 피드를, 오른쪽에는 처음에는 NumPy로 실행되는 에지 검출 결과를 보여준다. NumPy는 초당 2 프레임(fps)도 못 낸다. SPy 기반 에지 검출 알고리즘으로 전환하면 오른쪽 영상이 카메라를 따라잡아 약 60fps로 동작한다. 데모 코드는 GitHub에서도 볼 수 있다.

관심 있는 참석자들에게 그는 특히 SPy 저장소와 이슈 트래커를 추천했다. 몇몇 이슈는 “good first issue”와 “help wanted”로 태그되어 있다. 프로젝트에 대해 대화를 나눌 디스코드 서버도 있다. 머지않아 이번 발표 영상이 EuroPython 유튜브 채널에 올라올 것이다.

[EuroPython 출장을 지원해준 LWN의 여행 후원사, Linux Foundation에 감사드립니다.]

이 글의 색인 항목
컨퍼런스
Python