라이브러리는 PyO3로 Python 내부에서 Rust를 실행하지만, 경계가 속도를 좌우한다
한 파서 예제가 네이티브 속도만으로 Python 라이브러리가 더 빨라지는 것은 아니라는 점을 보여주면서 PyO3를 둘러싼 개발자 논의가 새롭게 일어났다.
9월 13일 공개된 이 데모는 수작업으로 만든 JSON 파서를 사례로, 라이브러리가 PyO3를 통해 Python 내부에서 Rust를 실행하는 방식을 설명한다. 먼저 Rust가 입력을 파싱한다. 이어 PyO3가 생성된 트리를 딕셔너리, 리스트, 문자열, 숫자, 불리언, Python 예외로 변환한다.
충돌은 이 두 번째 단계에서 발생한다. 빠른 Rust 알고리즘은 통합 계층이 작업을 마치기 전에 완료될 수 있다. 결과가 클 경우 네이티브 값을 Python 객체로 변환하는 데 개발자가 가속하려 했던 작업보다 더 많은 시간이 들 수 있다.
이는 새로운 Python 기능이나 새로운 PyO3 릴리스가 아니다. CPython은 C, C++, Fortran으로 작성된 모듈을 포함해 수십 년간 네이티브 확장을 지원해 왔다. 현재의 관심은 Rust가 이 오래된 아키텍처를 새로운 세대의 라이브러리 개발자에게 매력적으로 만들었다는 데서 비롯된다.
Pydantic, Polars, cryptography 등 여러 프로젝트는 이미 익숙한 Python 인터페이스 뒤에 Rust를 배치했다. 이들의 성공은 유지보수자들에게 느린 내부 경로를 재검토하도록 압박하지만, 동시에 패키징과 플랫폼 지원 범위에 관한 더 어려운 질문도 제기한다.
따라서 중요한 경쟁은 Rust 대 Python이 아니다. 네이티브 연산 대 경계 오버헤드다. 이 경쟁이 어떤 포팅이 의미 있는 성능 향상을 내고, 어떤 포팅이 단지 복잡성을 컴파일된 패키지로 옮기는 데 그칠지를 결정한다.
라이브러리는 익숙한 import를 통해 PyO3로 Python 내부에서 Rust를 실행한다
PyO3는 애플리케이션 개발자가 Python 문법을 포기하지 않아도, 컴파일된 Rust를 CPython이 import할 수 있는 네이티브 확장으로 바꿔 준다.
Bob Belderbos는 Rust로 작성해 Python 함수로 노출한 JSON 파서로 이 경로를 시연했다. 그의 파서 설명은 과정을 네 단계로 정리한다.
개발자는 일반적인 Rust 모듈을 작성하고, PyO3 어트리뷰트를 추가하며, maturin으로 패키지를 빌드한 뒤 Python에서 생성된 확장을 import한다. Maturin은 Rust 기반 Python 모듈을 위한 빌드 및 패키징 도구다.
컴파일된 산출물은 공유 라이브러리 내부의 네이티브 머신 코드다. 운영체제에 따라 이 파일은 일반적으로 .so, .dylib, 또는 .dll 확장자를 사용한다. Python은 이전 세대의 네이티브 모듈에 쓰인 것과 같은 광범위한 확장 메커니즘을 통해 이를 불러온다.
이 표현은 중요하다. Python은 런타임에 Rust 소스 코드를 해석하지 않는다. Rust 컴파일러가 머신 코드를 생성하고, CPython은 네이티브 애플리케이션 바이너리 인터페이스를 통해 내보낸 함수를 호출한다.
PyO3는 바인딩 계층을 제공한다. 그 매크로는 함수 호출, 참조 관리, 인수 추출, 반환값, 예외 처리에 필요한 접착 코드 상당 부분을 생성한다.
이 데모에서 #[pyfunction]은 Python이 호출할 수 있는 Rust 함수를 표시한다. #[pymodule] 매크로는 Python이 import 중 초기화하는 확장 모듈을 정의한다.
공개된 사용 경험은 일반적인 Python 그대로다. 호출자는 모듈을 import하고 파싱 함수에 문자열을 전달한다. 이 상호작용에서 호출자가 Rust의 소유권, 트레이트, 수명, Cargo를 이해할 필요는 없다.
구현은 다른 경로를 따른다. 입력은 Python 문자열에서 Rust 문자열 참조로 넘어간다. Rust는 파싱을 수행하고 JSON 트리를 나타내는 enum을 구성한다.
enum은 여러 정의된 변형 중 하나를 담을 수 있는 Rust 타입이다. 이 경우 변형은 null, 불리언, 숫자, 문자열, 배열, 객체를 나타낸다.
생성된 트리는 처음에는 전적으로 Rust에 속한다. Python 인터프리터는 Python의 메모리 및 타입 시스템에 따라 관리되는 객체를 기대하므로 이 구조를 직접 사용할 수 없다.
PyO3의 공식 가이드는 프로젝트가 지원하는 양방향 방식을 설명한다. 개발자는 Rust로 Python 모듈을 만들 수도 있고, Rust 애플리케이션 안에 Python 인터프리터를 임베드할 수도 있다.
이 사례의 핵심은 첫 번째 방향이다. 유지보수자는 Python용 인터페이스를 유지하면서 선택한 작업을 컴파일된 코드로 옮길 수 있다.
이 모델은 이미 Python 생태계 전반에서 흔하다. NumPy는 네이티브 연산을 기반으로 편리한 Python 작업을 제공하며 더 큰 패턴을 정립했다. PyO3는 확장을 만드는 데 쓰이는 언어와 도구를 바꾸는 것이지, 근본적인 아키텍처를 바꾸는 것은 아니다.
최근 논의가 중요한 이유는 숨겨진 경계를 드러내기 때문이다. 흥미로운 사건은 Python이 갑자기 네이티브 코드를 실행하게 된 것이 아니다. 더 많은 유지보수자가 CPython 접착 계층을 일일이 수작업으로 작성하지 않고도 이제 이러한 확장을 만들 수 있게 됐다는 점이다.
이처럼 구현 장벽이 낮아지면 네이티브 포팅을 고려할 만한 함수의 범위가 넓어진다. 하지만 Rust로 들어가고 나오는 모든 요소를 포함한 전체 호출을 측정해야 할 필요성까지 없애지는 않는다.
Python 유지보수자는 핫 패스를 Rust로 옮겨야 한다는 압박에 직면한다
성공적인 Rust 기반 패키지는 네이티브 확장을 전문가만의 기법에서 주류 Python 프로젝트를 위한 실질적인 유지보수 전략으로 바꿔 놓았다.
가장 분명한 기준점은 Pydantic이다. 두 번째 메이저 버전에서 이 프로젝트는 검증 기능을 Rust로 구현된 별도 패키지 pydantic-core로 옮겼다.
프로젝트의 초기 Pydantic V2 설계는 재작성된 코어가 첫 번째 버전보다 큰 성능 향상을 제공했다고 밝혔다. 이 수치는 Pydantic 자체의 사전 출시 벤치마크에서 나온 것이므로, 결과는 여전히 워크로드에 따라 달라진다.
한 벤치마크보다 중요한 것은 아키텍처적 신호다. Python 코드는 모델을 정의하고 스키마를 생성한다. Rust 코어는 성능에 민감한 경로에서 검증과 직렬화를 실행한다.
Polars는 같은 패턴을 더 폭넓게 적용한다. 핵심은 Rust로 작성됐으며 Python을 포함한 여러 언어용 인터페이스를 통해 제공된다.
프로젝트의 Polars 아키텍처는 쿼리 계획, 컬럼형 연산, 병렬 실행을 네이티브 데이터 구조에 가깝게 유지한다. Python 사용자는 여전히 표현식을 작성하고 익숙한 고수준 결과를 받는다.
이 프로젝트들은 파서, 검증기, 토크나이저, 압축 도구, 데이터베이스 클라이언트, 데이터 엔진의 유지보수자에게 압박을 준다. 사용자는 이제 Python 패키지가 접근하기 쉬운 인터페이스를 유지하면서도 선택한 내부 구성요소를 교체할 수 있다는 점을 알고 있다.
이 압박은 단순히 벤치마크 순위에 관한 것이 아니다. 네이티브 코드는 명확히 정의된 작업에서 CPU 시간을 줄이고, 처리량을 높이며, 더 예측 가능하게 메모리를 사용할 수 있다.
Rust는 유지보수자에게 또 다른 매력을 제공한다. 소유권 및 타입 시스템이 컴파일 과정에서 여러 범주의 메모리 오류를 잡아내지만, unsafe 코드와 의존성 결함의 가능성은 여전히 남는다.
PyO3는 또한 많은 일반적인 Python 및 Rust 타입 간 변환을 제공한다. Rust 오류를 Python 예외로 바꾸고 Python의 참조 관리에도 참여한다.
이 도구들은 수작업으로 작성한 C 인터페이스보다 확장을 더 쉽게 유지보수하게 할 수 있다. 네이티브 통합을 자동으로 만들어 주지는 않지만, 필요한 맞춤형 장치의 양을 줄여 준다.
그 결과 유지보수자에게는 다른 빌드 대 구매 결정이 생긴다. 이전에는 느린 Python 함수가 네이티브로 재작성하려면 부족한 C 전문성이 필요했기에 Python에 남아 있을 수 있었다.
이제 Rust 경험을 갖춘 팀은 PyO3를 통해 더 작은 네이티브 코어를 노출할 수 있다. 이후 Maturin이 확장을 빌드해 Python 패키지 안에 넣을 수 있다.
이 경로는 좁은 함수가 작고 응집된 입력을 받아 상당한 연산을 수행한 뒤 작고 응집된 결과를 반환할 때 가장 잘 작동한다. 압축, 해싱, 파싱 요약, 검증, 수치 커널은 흔히 이 형태에 들어맞는다.
함수가 언어 경계를 반복적으로 넘나들면 결과는 덜 예측 가능하다. 수많은 작은 호출은 인수 검사, 디스패치, 할당, 변환에 시간을 잃을 수 있다.
큰 결과는 관련된 또 다른 문제를 만든다. 알고리즘은 Rust에서 효율적으로 실행될 수 있지만, 호출자는 여전히 일반적인 Python 객체를 기대한다.
그 기대가 데이터 표현을 결정 요인으로 만든다. 컬럼형 버퍼를 네이티브 메모리에 유지하는 라이브러리는 수천 개의 중첩 딕셔너리를 반환하는 파서와 비용 구조가 다르다.
따라서 유지보수자들은 단순한 언어 재작성보다 아키텍처적 결정으로 내몰리고 있다. 데이터가 어느 쪽에 속할지, Python 객체가 언제 존재해야 할지를 정해야 한다.
사용자는 종단 간 동작을 비교하기 때문에 이러한 압박은 계속될 것이다. 이들은 함수 호출부터 사용 가능한 결과를 받기까지의 시간에 관심이 있으며, 내부 루프만의 고립된 속도에는 관심이 없다.
반환 과정은 네이티브 속도 향상을 지워버릴 수 있다
핵심 메커니즘은 객체 구체화다. 큰 Rust 결과를 Python 객체로 변환하는 작업이 완료된 전체 연산을 지배할 수 있다.
Belderbos의 파서는 먼저 JSON 문서의 모든 값을 포함한 Rust 트리를 만든다. 이 단계는 Rust의 컴파일된 실행과 명시적인 메모리 모델의 이점을 얻을 수 있다.
다음 단계에서는 전체 트리를 다시 순회한다. 각 Rust 객체는 Python 딕셔너리가 되고, 각 배열은 리스트가 되며, 각 리프는 Python 값이 된다.
이 과정을 구체화라고 한다. 호출자가 검사, 수정, 직렬화하거나 다른 곳으로 전달하기를 기대하는 구체적인 Python 객체를 만든다.
PyO3의 IntoPyObject 트레이트는 이 변환을 조정한다. 트레이트는 타입이 구현할 수 있는 동작을 정의해, 각 JSON 변형이 대응하는 Python 표현을 설명하도록 한다.
변환은 재귀적으로 이루어진다. 객체에는 새 딕셔너리가 필요하며, 모든 키와 값에 대한 항목도 필요하다. 배열에는 변환된 자식이 담긴 리스트가 필요하다.
각 Python 객체는 CPython의 메모리 관리 시스템에도 들어간다. 할당, 타입 메타데이터, 참조 카운팅에는 Rust 트리에는 없던 비용이 따른다.
전통적인 CPython 빌드에는 또 다른 제약이 있다. Python 객체를 다루는 코드는 일반적으로 전역 인터프리터 락, 즉 GIL과 연결된 인터프리터 접근 권한이 필요하다.
GIL은 한 번에 하나의 스레드만 전통적인 Python 인터프리터를 구동하도록 한다. Python 객체와 분리된 Rust 작업은 그 락을 보유하지 않고 실행될 수 있다.
PyO3는 Rust가 독립적인 연산을 수행하는 동안 인터프리터 접근 권한을 해제하는 Python::detach 메커니즘을 문서화한다. 병렬 실행 가이드는 그동안 다른 Python 스레드가 어떻게 진행할 수 있는지 설명한다.
이 기법은 Rust 작업이 Python 객체와 떨어져 있는 동안에만 도움이 된다. Python 딕셔너리나 리스트를 다시 만들면 실행은 다시 인터프리터 경계로 돌아온다.
이 파서 예제는 100,000개의 값을 담은 문서가 대략 같은 규모의 Python 객체 생성을 요구한다고 추정한다. 이는 보편적인 성능 벤치마크가 아니라 설명을 위한 관계다.
형태는 크기만큼 중요하다. 평면적인 숫자 버퍼는 때로 공유 표현을 통해 경계를 넘을 수 있다. 깊게 중첩된 문서는 더 많은 객체 할당과 포인터 순회를 요구한다.
호출 빈도는 또 다른 축을 만든다. 큰 연속 버퍼를 처리하는 단일 네이티브 호출은 설정 비용을 상쇄할 수 있다. 단일 값을 처리하는 수천 번의 호출은 그렇지 못한 경우가 많다.
오류 처리도 경계를 넘는다. Rust 파싱 오류는 예상되는 클래스, 메시지, 위치 정보를 갖춘 Python 예외가 되어야 한다.
PyO3는 타입이 지정된 Rust 오류를 PyErr로 매핑할 수 있다. 따라서 잘못된 문자열은 Python 호출자에게 ValueError로 나타날 수 있고, 사용할 수 없는 파일은 FileNotFoundError가 될 수 있다.
그러한 변환은 공개 API를 보호한다는 점에서 가치가 있습니다. 유지 관리자가 구현 언어를 바꿨다는 이유만으로 사용자가 별도의 오류 모델을 익힐 필요는 없어야 합니다.
하지만 Python 의미론을 보존하려면 상당한 작업이 필요합니다. 네이티브 재작성은 엣지 케이스, 예외 유형, 반복 동작, 소유권 규칙, 경우에 따라서는 서브클래스 간 상호작용까지 재현해야 합니다.
따라서 진정한 최적화 대상은 전체 인터페이스입니다. 표현 변환이 그대로 남아 전체 실행 시간을 지배한다면, 파싱 속도만 높여서는 충분하지 않습니다.
라이브러리에는 여러 아키텍처적 대응 방식이 있습니다. 더 작은 요약 결과를 반환하거나, 이터레이터를 노출하거나, 콜백을 배치 단위로 처리하거나, 불투명한 Rust 기반 객체를 계속 유지할 수 있습니다.
지연 뷰는 특히 대규모 트리와 관련성이 높습니다. 모든 Python 객체를 즉시 구성하는 대신, 확장은 호출자가 요청하는 값만 구체화할 수 있습니다.
이 설계는 애플리케이션이 결과의 일부만 읽을 때 불필요한 변환을 줄입니다. 동시에 복잡성은 객체 수명, 캐싱, 변경, API 설계로 이동합니다.
컬럼형 라이브러리는 연속된 네이티브 버퍼에 데이터를 유지함으로써 일부 비용을 피할 수 있습니다. Python은 각 요소를 복제하는 대신 기본 저장소를 참조하는 경량 객체를 받습니다.
이 전략은 Polars가 기계적인 함수별 재작성보다 더 강력한 네이티브 아키텍처를 보여주는 이유를 설명합니다. Polars의 Python 인터페이스는 상당한 작업과 데이터를 함께 유지하는 Rust 엔진을 제어합니다.
일반 딕셔너리를 반환하는 파서는 더 불리한 경계에 놓입니다. 그 출력 형식은 확장이 Python 코드가 기대하는 바로 그 객체 그래프를 생성하도록 요구합니다.
여기서의 교훈은 PyO3 변환이 유난히 비효율적이라는 것이 아닙니다. 어떤 외부 함수 인터페이스든 양쪽 표현, 수명, 오류, 소유권을 조율해야 합니다.
PyO3는 이러한 의무를 더 쉽게 표현하도록 돕습니다. 하지만 그 비용 자체를 없앨 수는 없습니다.
Rust와 Python은 협력 관계지만, 패키징은 비용을 따진다
빠른 확장은 컴파일된 wheel이 운영체제, 프로세서, 인터프리터, 바이너리 인터페이스와 일치해야 하므로 배포 의무를 만듭니다.
순수 Python 패키지는 막대한 이식성 이점을 지닙니다. 단일 범용 wheel 하나로 여러 운영체제와 프로세서 아키텍처에서 실행할 수 있는 경우가 많습니다.
컴파일된 확장은 플랫폼별 기계어 코드를 생성합니다. 패키지 유지 관리자는 호환 가능한 아티팩트를 제공하거나, 사용자에게 프로젝트를 로컬에서 컴파일하도록 요청해야 합니다.
wheel은 Python의 빌드 배포 형식입니다. 설치 가능한 패키지 파일을 포함하며, 인터프리터, 애플리케이션 바이너리 인터페이스, 플랫폼에 대한 호환성 태그를 담습니다.
Python packaging guide는 기본 매트릭스를 설명합니다. 유지 관리자는 흔히 Python 버전, 운영체제, 아키텍처를 아우르는 빌드를 준비해야 합니다.
지속적 통합은 이 작업의 상당 부분을 자동화할 수 있습니다. Maturin과 관련 도구는 wheel을 빌드하고 게시할 수 있으며, cibuildwheel 같은 프로젝트는 대상 환경 전반의 빌드를 조율합니다.
자동화가 테스트를 없애지는 않습니다. 지원되지 않는 CPU 기능, 누락된 시스템 라이브러리, 호환되지 않는 런타임 기대치 때문에 wheel이 성공적으로 설치된 뒤에도 실패할 수 있습니다.
Linux는 배포판마다 서로 다른 버전의 시스템 구성 요소를 제공하기 때문에 특히 복잡합니다. Manylinux 사양은 광범위하게 호환되는 wheel을 위한 표준화된 빌드 환경을 프로젝트에 제공합니다.
Windows와 macOS에는 각각의 아티팩트가 필요합니다. Apple의 Intel 및 Arm 하드웨어 분화는 또 하나의 차원을 더했지만, 범용 바이너리가 때때로 두 아키텍처를 결합할 수 있습니다.
안정 ABI는 인터프리터 버전 차원을 줄일 수 있습니다. ABI는 컴파일된 코드가 바이너리 런타임을 호출하는 방식을 규정하는 저수준 계약입니다.
CPython의 기존 abi3 안정 ABI는 조건을 충족하는 확장이 플랫폼과 아키텍처당 하나의 wheel로 여러 Python 3 버전을 대상으로 하도록 합니다. 확장은 Limited API 사용으로 제한해야 합니다.
이 제한은 더 폭넓은 호환성을 위해 일부 API 접근성과 잠재적 최적화를 교환합니다. PyO3는 abi3 확장을 빌드할 때 적절한 최소 Python 버전을 선택하도록 지원합니다.
패키징 문제는 free-threaded CPython과 함께 다시 진화하고 있습니다. free-threaded 빌드는 전통적인 GIL 구성을 제거하여 네이티브 확장이 전제했던 가정을 바꿉니다.
현재 PyO3 문서는 전통적인 abi3 wheel과 free-threaded 빌드를 위한 새로운 안정 ABI 경로를 구분합니다. 유지 관리자는 자신의 아티팩트가 실제로 어떤 인터프리터 구성을 지원하는지 검증해야 합니다.
플랫폼 문제는 파서 기사에 대한 공개 논의에서 빠르게 등장했습니다. 개발자들은 Rust 기반 의존성이 Python이 실행될 수 있는 모든 환경에서도 여전히 작동하는지 물었습니다.
짧은 대답은 아니오입니다. 컴파일된 의존성은 유지 관리자가 호환 가능한 wheel을 게시한 환경 또는 사용자가 지원되는 툴체인으로 빌드할 수 있는 환경에서 작동합니다.
이 제한은 다른 언어로 작성된 네이티브 확장에도 적용됩니다. Rust는 사용 가능한 컴파일러 대상과 빌드 의존성을 바꾸지만, 근본적인 이식성 문제를 없애지는 않습니다.
cryptography 패키지는 사용자 경험을 잘 보여줍니다. 해당 문서는 대부분의 사용자가 사전 빌드된 wheel을 받고 Rust를 설치할 필요가 없다고 설명합니다.
게시된 wheel 범위 밖의 사용자는 Rust 컴파일러와 기타 네이티브 의존성이 필요할 수 있습니다. pip install이 언어 중립적으로 유지될 것이라 기대했던 사람에게는 이러한 대체 경로가 놀라울 수 있습니다.
브라우저에서 호스팅되는 Python은 또 다른 엣지 케이스입니다. Pyodide는 WebAssembly를 통해 Python을 실행하므로, 일반적인 데스크톱 및 서버용 wheel은 այնտեղ에서 자동으로 작동하지 않습니다.
생태계는 Rust를 포함하는 패키지의 WebAssembly 빌드에서 진전을 이루었습니다. Hacker News 토론에서 제기된 링크에 따르면 Pydantic-core는 이제 관련 아티팩트를 제공합니다.
그럼에도 WebAssembly 지원에는 의도적인 패키징과 테스트가 필요합니다. 입출력 동작은 스레드, 파일, 소켓, 비동기 런타임과 관련한 브라우저 제약에 부딪힐 수도 있습니다.
모바일 시스템, 임베디드 환경, 특이한 프로세서, 오래된 엔터프라이즈 배포판도 비슷한 부담을 만듭니다. 순수 Python 폴백은 지원 범위를 보호할 수 있지만, 두 구현을 유지하는 일은 작업을 추가합니다.
따라서 라이브러리 팀은 모든 네이티브 재작성에 이식성 비용을 반영해야 합니다. 코드 경로는 더 빨라질 수 있지만, 프로젝트 전체 사용자를 대상으로 배포하기는 더 어려워질 수 있습니다.
인기 패키지라면 이러한 절충을 받아들일 가치가 있을 수 있습니다. 큰 기여자 기반과 성숙한 릴리스 파이프라인은 광범위한 wheel 매트릭스를 지원할 수 있습니다.
소규모 프로젝트는 다른 계산식을 마주합니다. 네이티브 빌드는 간결한 라이브러리를 여러 환경에 걸친 릴리스 엔지니어링의 약속으로 바꿀 수 있습니다.
PyO3와 maturin은 이러한 부담을 상당히 줄입니다. 하지만 없애지는 않으며, 사용자는 빌드되지 않은 모든 대상을 설치 실패로 경험합니다.
회의적인 관점은 엔드투엔드 측정에서 시작한다
“Rust로 작성됨”은 구현 사실일 뿐 성능 결과가 아니며, 유지 관리자는 변환과 소비를 포함하는 벤치마크가 필요합니다.
Rust는 동등한 Python 루프에 비해 CPU 집약적 코드의 속도를 높이는 경우가 많습니다. 하지만 이 비교만으로는 애플리케이션의 완료된 작업량에 대해 거의 알 수 없습니다.
확장 호출에는 인수 변환, 검증, 네이티브 디스패치, 계산, 출력 변환, 할당, 오류 처리가 포함됩니다. 호출자는 이후 결과를 다시 변환할 수도 있습니다.
유용한 벤치마크는 이 전체 경로를 측정합니다. 애플리케이션이 사용하는 표현에서 시작해 다음 단계가 소비할 수 있는 데이터로 끝납니다.
마이크로벤치마크도 여전히 역할이 있습니다. 어느 단계가 시간을 소비하는지 식별하고 코드 변경 후 알고리즘이 개선됐는지를 드러냅니다.
하지만 팀이 가장 빠른 내부 단계만 제시하면 오해를 부를 수 있습니다. 파서 처리량 수치는 이후 대규모 Python 객체 그래프를 구성하는 비용을 숨길 수 있습니다.
벤치마크에는 대표적인 입력도 필요합니다. 작은 문서는 고정 호출 오버헤드를 과장할 수 있고, 균질한 합성 데이터는 프로덕션에서 나타나는 할당 패턴을 놓칠 수 있습니다.
워밍된 캐시, 릴리스 빌드, 컴파일러 플래그, CPU 기능은 결과를 바꿀 수 있습니다. 비교에서는 이러한 조건을 명확히 하고 동일한 공개 동작을 테스트해야 합니다.
정확성도 같은 비중을 가져야 합니다. 파서와 검증기는 잘못된 인코딩, 극단적인 중첩, 중복 키, 숫자 엣지 케이스, 예기치 않은 리소스 사용을 마주합니다.
예외를 바꾸거나 다른 입력을 허용하는 포트는 서로 다른 작업을 수행하기 때문에 더 빠를 수 있습니다. 성능 테스트에는 호환성 테스트가 동반되어야 합니다.
메모리 사용량은 겉보기의 이점을 뒤집을 수 있습니다. 완전한 Rust 트리와 새로 구체화된 Python 트리를 함께 유지하면 일시적으로 두 표현이 필요할 수 있습니다.
스트리밍 또는 지연 설계는 이러한 중복을 줄일 수 있습니다. 하지만 오류 발생 시점과 네이티브 버퍼가 할당된 상태로 유지되는 기간도 바꿀 수 있습니다.
동시성 주장에도 비슷한 주의가 필요합니다. Rust 코드는 여러 스레드를 사용할 수 있고, PyO3는 적절한 네이티브 작업 동안 인터프리터 접근을 해제할 수 있습니다.
확장은 임의의 네이티브 스레드에서 일반 Python 객체를 안전하게 조작할 수 없습니다. 해당 객체와 상호작용할 때마다 PyO3의 인터프리터 규칙으로 돌아가야 합니다.
free-threaded Python은 이 상황의 일부를 바꾸지만, 애플리케이션 데이터에서 동기화의 필요성을 없애지는 않습니다. 스레드 안전성은 여전히 라이브러리의 명시적인 책임입니다.
보안 역시 단순한 구호를 따르지 않습니다. Rust의 안전한 부분집합은 여러 메모리 오용 범주를 방지하지만, 네이티브 확장에는 unsafe 블록과 취약한 의존성이 포함될 수 있습니다.
컴파일된 코드의 결함은 일반적인 Python 예외를 발생시키는 대신 인터프리터 프로세스를 충돌시킬 수 있습니다. 이러한 결과는 모든 네이티브 확장 언어에 적용됩니다.
따라서 PyO3에 대한 가장 강력한 근거는 구체적입니다. 계산량과 데이터 레이아웃이 경계를 정당화하는 경우, 유지 관리자가 Python 인터페이스와 Rust 구현을 결합할 수 있게 합니다.
가장 약한 근거는 외형적인 재작성입니다. 함수가 제한적인 작업만 수행하거나 대부분의 시간을 I/O에 쓴다면, 간결한 Python 코드를 네이티브 장치로 대체해도 얻는 것이 거의 없습니다.
프로젝트는 먼저 기존 애플리케이션을 프로파일링해야 합니다. 안정적인 핫 패스를 식별하고, 호환성 요구 사항을 정의하며, 교환되는 값의 크기와 형태를 측정해야 합니다.
그런 다음 팀은 하나의 경계를 프로토타이핑할 수 있습니다. 변환이 지배적이라면, 다음 결정은 파서 명령어나 컴파일러 최적화가 아니라 데이터 표현에 관한 것입니다.
이러한 회의적 검증은 Rust 기반 Python에 반대하는 주장이 아닙니다. 모든 성능 불만에 대한 기본 답이 되는 것을 막아주는 장치입니다.
성공적인 네이티브 확장은 일반 사용자에게 구현을 숨깁니다. 설치가 작동하고, 예외는 익숙하게 유지되며, 타입 힌트는 유용하고, 실제 워크로드에서 성능이 개선됩니다.
사용자가 언어 선택을 축하할 필요는 없습니다. 기존 Python 작업이 더 빨리 끝난다는 사실만 알아차리면 됩니다.
세 가지 신호가 PyO3의 다음 방향을 보여줄 것이다
다음 단계는 경계를 고려한 API, 더 폭넓은 wheel 지원 범위, 전통적 Python과 free-threaded Python 모두에서 작동하는 확장에 달려 있습니다.
첫 번째 신호는 공개 벤치마크의 변화입니다. 더 많은 프로젝트가 네이티브 커널 처리량뿐 아니라 객체 생성을 포함한 엔드투엔드 시간을 보고해야 합니다.
이러한 변화는 언어 선택을 관측 가능한 애플리케이션 결과와 연결함으로써 PyO3의 근거를 강화할 것입니다. 또한 변환 과정에서 이득이 사라지는 포트도 드러낼 것입니다.
여러 반환 설계를 비교하는 벤치마크를 살펴보십시오. 네이티브 기반 뷰, 이터레이터, 배치 결과, 완전히 구체화된 딕셔너리는 매우 다른 결과를 낼 수 있습니다.
메모리 프로파일은 시간 측정 결과와 함께 제시되어야 합니다. 확장이 Rust와 Python 구조를 일시적으로 중복 보유하는지 보여줄 수 있습니다.
두 번째 신호는 더 폭넓은 wheel 지원 범위입니다. 성공적인 프로젝트는 주요 운영체제, 프로세서 계열, 지원되는 Python 버전을 위한 신뢰할 수 있는 아티팩트를 게시할 것입니다.
WebAssembly는 특별한 주의를 기울일 필요가 있습니다. 더 나은 도구와 PyPI에서 제공되는 WebAssembly wheel이 늘어나면 브라우저 기반 Python 사용자가 제기하는 이식성 우려를 줄일 수 있습니다.
모바일과 덜 일반적인 Linux 아키텍처도 여전히 유용한 시험 대상입니다. 지원되는 각 타깃은 일반적인 Python 의존성이 실제로 의미하는 범위를 넓힙니다.
프로젝트는 소스 빌드 경로도 문서화해야 합니다. 오류 메시지, 컴파일러 요구 사항, 재현 가능한 빌드 지침이 명확하다면 누락된 wheel의 영향은 줄어듭니다.
네이티브 패키지가 순수 Python 버전에서 지원하던 타깃을 반복적으로 중단한다면 이식성 우려는 더 커집니다. 빌드 자동화가 이런 공백을 메운다면 우려는 약해집니다.
세 번째 신호는 free-threaded CPython에 대한 안정적인 지원입니다. 확장 프로그램은 인터프리터 잠금, 공유 상태, 객체 접근에 관한 기존 가정을 조정해야 합니다.
PyO3의 발전하는 API와 stable ABI 지원은 이러한 전환의 방향을 결정할 것입니다. 유지관리자에게는 단순한 컴파일 성공만으로는 충분하지 않으며, 현실적인 워크로드에서의 동시성 테스트가 필요합니다.
성숙한 결과라면 하나의 프로젝트가 릴리스 복잡성을 통제 불가능하게 늘리지 않고도 기존 인터프리터와 free-threaded 인터프리터를 모두 지원할 수 있어야 합니다.
실패의 모습은 다를 것입니다. 파편화된 wheel 세트, 불명확한 ABI 태그, 숨겨진 직렬화 병목은 Rust 기반 패키지를 기본 의존성으로 신뢰하기 어렵게 만들 것입니다.
더 큰 방향성은 여전히 설득력이 있습니다. Python은 큰 사용자 기반, 읽기 쉬운 오케스트레이션, 생산적인 애플리케이션 계층을 제공합니다. Rust는 컴파일된 커널, 명시적인 데이터 구조, 더 안전한 네이티브 도구를 제공합니다.
그러나 이 협업은 경계가 설계의 일부가 될 때만 성공합니다. 라이브러리는 PyO3로 Python 내부에서 Rust를 실행하지만, 사용자는 Python 객체와 Python 패키지 아티팩트를 사용합니다.
포팅을 검토하는 개발자는 하나의 구체적인 질문에서 시작해야 합니다. 빠른 Rust 함수가 반환된 뒤 무엇이 그 경계를 넘어야 하는가?
재작성에 착수하기 전에 그 반환 경로를 프로파일링하세요. 약속한 모든 타깃에서 wheel을 테스트하세요. 그런 다음 전체 작업을 사용자가 이미 의존하고 있는 Python 구현과 비교하세요.
네이티브 버전이 여전히 더 우수하다면 PyO3는 그 자리를 입증한 것입니다. 변환 과정에서 이득이 사라진다면, 더 많은 코드를 바꾸기 전에 인터페이스부터 바꾸세요.



