C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기
들어가며: Python은 편한데, 이 루프만 C++로 돌리고 싶습니다
“데이터 전처리 루프가 30분 걸려요”
AI·데이터 사이언스 실무에서 자주 겪는 상황입니다. Pandas로 1억 행을 처리하는 루프가 30분 넘게 걸리며, NumPy 벡터화로도 한계가 있는 복잡한 조건문·중첩 루프가 있습니다. 문제: Python의 인터프리터 특성상 순수 루프는 C에 비해 수십 배 느립니다. 해결: 병목 구간만 C++로 옮기며, Python에서 import 해서 쓰는 패턴입니다. pybind11은 이 과정을 가장 쉽게 만들어 주는 도구다.
추가 문제 시나리오
실시간 추론 지연
딥러닝 모델의 전처리(이미지 리사이즈, 정규화, 배치 구성)가 픽셀 단위 Python 루프로 구현되어 있어 추론보다 전처리가 더 오래 걸립니다. 원소마다 인터프리터를 거치는 루프를 C++로 옮기면 그 오버헤드가 통째로 사라집니다.
몬테카를로 시뮬레이션
금융 옵션 가격 시뮬레이션에서 1000만 회 반복이 필요합니다. Pure Python으로는 수 시간, C++ 확장으로는 수 분 내에 완료됩니다.
대용량 이미지 배치 처리
OpenCV Python 바인딩의 함수 하나하나는 이미 C++로 실행되므로 빠릅니다. 느려지는 것은 그 사이를 Python이 잇는 경우, 즉 커스텀 필터를 픽셀 루프로 짜거나 작은 호출을 수없이 반복하는 파이프라인입니다. 이런 부분을 C++ 한 덩어리로 묶으면 호출 경계를 넘는 비용과 중간 배열 생성이 줄어듭니다.
레거시 C++ 라이브러리 활용
기존 C++ 엔진(검색, 랭킹, 암호화 등)을 Python 서비스에서 호출해야 할 때, pybind11으로 래핑하면 최소 코드로 연동할 수 있습니다.
pybind11으로 해결
flowchart LR
subgraph before[문제 상황]
P1[Python 루프] --> P2[30분 대기]
P2 --> P3[병목]
end
subgraph after[pybind11 적용]
A1[Python] --> A2[import engine]
A2 --> A3[C++ 확장]
A3 --> A4[2분 완료]
end
pybind11은 C++ 코드를 Python에서 import 가능한 확장 모듈로 만들어 주는 헤더 전용 라이브러리입니다. Boost.Python보다 가볍으며, C++11/14 문법을 잘 활용하며, NumPy 배열과의 연동도 지원해서 AI·데이터 사이언스 쪽에서 수요가 높습니다.
pybind11로 C++ 함수와 클래스를 Python 모듈로 노출하는 최소 예제에서 출발해 setuptools·CMake 빌드, py::array_t를 이용한 NumPy 연동, 순수 Python·NumPy와의 성능 비교, GIL·예외·wheel 배포 같은 운영 문제까지 다룹니다.
요구 환경: Python 3.6+(개발 헤더 필요: python3-dev 등), pybind11(pip pybind11 또는 vcpkg/FetchContent), CMake 3.4+ 또는 setuptools. C++11 이상. NumPy 연동 시 numpy 패키지 설치.
pybind11이란
역할
- C++ 타입(함수, 클래스, enum 등) 을 Python 쪽에서 쓸 수 있게 바인딩해 줍니다.
- 헤더 전용: 헤더만 포함하면 되고, Python이 제공하는 C API와 결합해 네이티브 확장 모듈(.pyd / .so) 을 빌드합니다.
- C++11 이상을 전제로 하며, STL 컨테이너(
vector,map등)를 자동으로 Pythonlist,dict등으로 변환해 줍니다.
아키텍처 개요
flowchart TB
subgraph python[Python]
P[Python 스크립트]
P --> |import| M[확장 모듈 .pyd/.so]
end
subgraph cpp[C++]
M --> |pybind11 바인딩| C[C++ 함수/클래스]
C --> |NumPy 버퍼| N["py array_t"]
end
N --> |복사 없이| P
왜 pybind11인가
- Boost.Python 대비 가볍으며, 컴파일이 빠르며, 최신 C++ 스타일과 잘 맞습니다.
- NumPy의 버퍼 프로토콜을 지원해, 배열을 복사 없이 C++에 넘기거나 받을 수 있어, 행렬 연산·이미지 처리 같은 무거운 연산에 적합합니다.
- AI·데이터 파이프라인에서 “학습/추론의 핵심 루프만 C++”, 나머지는 Python” 구조를 만들 때 표준적으로 사용됩니다.
최소 예제: 함수와 클래스 노출
함수 하나 노출
PYBIND11_MODULE(example, m)에서 example은 Python에서 import example로 불러올 때의 모듈 이름이며, m은 그 모듈 객체입니다. m.def(“add”, &add, …)로 C++ 함수 add를 Python 이름 “add”에 연결하면, 빌드된 확장 모듈(.pyd 또는 .so)을 로드한 뒤 example.add(1, 2)처럼 호출할 수 있습니다.
// example.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int a, int b) {
return a + b;
}
PYBIND11_MODULE(example, m) {
m.doc() = "minimal pybind11 example";
m.def("add", &add, "Add two integers");
}
- PYBIND11_MODULE(example, m): Python에서
import example했을 때의 모듈 이름이example이고,m이 그 모듈 객체입니다. - m.def(“add”, &add, …): C++ 함수
add를 Python 이름"add"로 등록합니다. 빌드 후 Python에서:
import example
print(example.add(1, 2)) # 3
클래스 노출
py::class_
// 타입 정의
class Calculator {
public:
int mul(int a, int b) const { return a * b; }
};
PYBIND11_MODULE(example, m) {
py::class_<Calculator>(m, "Calculator")
.def(py::init<>())
.def("mul", &Calculator::mul);
}
- py::class_
(m, “Calculator”) : C++ 클래스Calculator를 Python 클래스Calculator로 등록. - .def(py::init<>()): 기본 생성자.
- .def(“mul”, &Calculator::mul): 메서드
mul을 노출. Python에서:
import example
calc = example.Calculator()
print(calc.mul(3, 4)) # 12
빌드: CMake로 확장 모듈 만들기
CMakeLists.txt 예시
cmake_minimum_required(VERSION 3.15)
project(example LANGUAGES CXX)
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(example example.cpp)
target_link_libraries(example PRIVATE pybind11::embed)
- pybind11_add_module(example example.cpp):
example.cpp를 빌드해example확장 모듈을 만듭니다. (Windows:example.pyd, Linux/macOS:example.cpython-3xx.so) - 빌드 결과물을 Python의 sys.path에 넣거나,
import하는 디렉터리에 두면import example로 사용할 수 있습니다.
setuptools로 빌드 (pyproject.toml)
[build-system]
requires = ["setuptools", "pybind11"]
build-backend = "setuptools.build_meta"
[project]
name = "my_engine"
version = "0.1.0"
[tool.setuptools.packages.find]
where = [src]
[tool.setuptools.package-dir]
"" = "src"
# setup.py (또는 pyproject.toml의 [tool.setuptools] 섹션)
# 필요한 모듈 import
from setuptools import setup, Extension
ext = Extension(
"my_engine",
sources=[src/my_engine.cpp],
include_dirs=[],
language="c++",
)
setup(ext_modules=[ext])
pyproject.toml또는setup.py에서 pybind11을 빌드 의존성으로 두며, Extension으로 소스와 include 경로를 지정해pip install .로 빌드·설치하는 방식입니다. 팀 공유·패키지 배포 시 유용합니다.
NumPy와 연동하기
배열을 C++에 넘기기
py::array_t
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
// 패키지 선언
namespace py = pybind11;
// NumPy 배열을 받아서 합을 구하는 예
double sum_array(py::array_t<double> arr) {
auto buf = arr.request();
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s;
}
PYBIND11_MODULE(example, m) {
m.def("sum_array", &sum_array);
}
- request() 로 버퍼 정보(ptr, size, shape, stride)를 얻으며, 연속(contiguous) 가정이 맞는지 확인한 뒤 사용하는 것이 안전합니다.
- 반환할 때는
py::array_t<T>를 만들어 반환하면 Python 쪽에서 NumPy 배열로 받을 수 있습니다.
배열 반환하기
py::array_t<double> create_array(size_t n) {
auto result = py::array_t<double>(n);
auto buf = result.request();
double* ptr = static_cast<double*>(buf.ptr);
for (size_t i = 0; i < n; ++i) ptr[i] = static_cast<double>(i);
return result;
}
PYBIND11_MODULE(example, m) {
m.def("create_array", &create_array);
}
import example
import numpy as np
arr = example.create_array(10)
print(arr) # [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
GIL 해제 (긴 연산 시)
GIL(Global Interpreter Lock)을 유지한 채로 오래 걸리는 C++ 연산을 하면, 다른 Python 스레드가 블로킹됩니다. py::gil_scoped_release로 GIL을 해제하세요.
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
double heavy_compute(py::array_t<double> arr) {
py::gil_scoped_release release; // GIL 해제
auto buf = arr.request();
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s; // 반환 시 자동으로 GIL 재획득
}
- GIL을 풀었을 때는 Python 객체를 건드리지 않도록 주의합니다. 순수 C++ 데이터만 다루세요.
실무 활용 사례
AI/ML 파이프라인: PyTorch나 TensorFlow로 학습한 모델의 전처리·후처리가 Python 루프로 되어 있다면 그 부분만 C++로 옮기는 것이 흔한 패턴입니다. 모델 추론 자체는 이미 네이티브 코드라서, 옮길 대상은 프로파일러로 Python 쪽 시간이 큰 구간을 확인해 고릅니다. 이미지/비디오 처리: OpenCV C++ 코드를 pybind11로 감싸 Python 스크립트에서 호출하면, Jupyter 노트북에서 빠르게 실험하면서도 핵심 연산은 C++의 SIMD·멀티스레드로 가속할 수 있습니다. 금융/시뮬레이션: 몬테카를로 시뮬레이션처럼 반복 연산이 많은 로직을 C++로 짜고, Python에서 파라미터를 넘겨 결과를 받으면 분석·시각화는 Python(pandas, matplotlib)으로, 연산은 C++로 분리할 수 있습니다.
NumPy 내적과 GIL 해제, 예외 등록, STL 자동 변환
예제 1: 벡터 내적 (NumPy 연동 + GIL 해제)
// dot_product.cpp
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
double dot_product(py::array_t<double> a, py::array_t<double> b) {
auto buf_a = a.request();
auto buf_b = b.request();
if (buf_a.size != buf_b.size) {
throw std::runtime_error("배열 크기가 일치하지 않습니다");
}
double* pa = static_cast<double*>(buf_a.ptr);
double* pb = static_cast<double*>(buf_b.ptr);
size_t n = buf_a.size;
py::gil_scoped_release release;
double result = 0;
for (size_t i = 0; i < n; ++i) {
result += pa[i] * pb[i];
}
return result;
}
PYBIND11_MODULE(dot_product, m) {
m.doc() = "NumPy 배열 내적 계산";
m.def("dot_product", &dot_product, "Compute dot product of two arrays");
}
# test_dot.py
import numpy as np
import dot_product
a = np.random.rand(10_000_000)
b = np.random.rand(10_000_000)
result = dot_product.dot_product(a, b)
expected = np.dot(a, b)
print(f"pybind11: {result}, NumPy: {expected}, diff: {abs(result - expected)}")
예제 2: 사용자 정의 예외 등록
#include <pybind11/pybind11.h>
namespace py = pybind11;
class ValidationError : public std::exception {
public:
const char* what() const noexcept override {
return "Validation failed";
}
};
void validate_positive(int x) {
if (x <= 0) {
throw ValidationError();
}
}
PYBIND11_MODULE(validation, m) {
py::register_exception<ValidationError>(m, "ValidationError");
m.def("validate_positive", &validate_positive);
}
import validation
try:
validation.validate_positive(-1)
except validation.ValidationError as e:
print("Caught:", e)
예제 3: STL 컨테이너 자동 변환
#include <pybind11/pybind11.h>
#include <vector>
#include <map>
#include <string>
namespace py = pybind11;
std::vector<int> get_primes(int n) {
std::vector<int> primes;
for (int i = 2; i <= n; ++i) {
bool is_prime = true;
for (int p : primes) {
if (p * p > i) break;
if (i % p == 0) { is_prime = false; break; }
}
if (is_prime) primes.push_back(i);
}
return primes;
}
std::map<std::string, int> count_chars(const std::string& s) {
std::map<std::string, int> m;
for (char c : s) {
m[std::string(1, c)]++;
}
return m;
}
PYBIND11_MODULE(containers, m) {
m.def("get_primes", &get_primes);
m.def("count_chars", &count_chars);
}
import containers
primes = containers.get_primes(20)
print(primes) # [2, 3, 5, 7, 11, 13, 17, 19]
counts = containers.count_chars("hello")
print(counts) # {'h': 1, 'e': 1, 'l': 2, 'o': 1}
ModuleNotFoundError, undefined symbol, 비연속 NumPy 배열 같은 에러
문제 1: “ModuleNotFoundError: No module named ‘example’”
원인: 빌드된 .pyd 또는 .so 파일이 Python이 찾는 경로에 없음.
해결법:
# 빌드 결과물 위치 확인
ls build/*.so # Linux/macOS
ls build/*.pyd # Windows
# 현재 디렉터리에서 실행하거나 PYTHONPATH 설정
export PYTHONPATH="${PYTHONPATH}:$(pwd)/build"
python -c "import example"
또는 sys.path에 추가:
import sys
sys.path.insert(0, "/path/to/build")
import example
문제 2: “ImportError: … undefined symbol: PyInit_example”
원인: 모듈 이름 불일치. PYBIND11_MODULE(example, m)의 첫 인자와 import example이 일치해야 함. 또는 Python 버전/ABI 불일치.
해결법:
- 모듈 이름 확인:
PYBIND11_MODULE의 첫 인자 =import이름 - Python 버전 확인:
python3 --version과 빌드 시 사용한 Python이 동일한지 확인
문제 3: “find_package(pybind11) failed”
원인: pybind11이 CMake에서 찾을 수 없음. 해결법:
# pip로 설치 후 CMAKE_PREFIX_PATH 설정
pip install pybind11
export CMAKE_PREFIX_PATH=$(python3 -c "import pybind11; print(pybind11.get_cmake_dir())")
cmake ..
또는 FetchContent로 pybind11 포함:
include(FetchContent)
FetchContent_Declare(
pybind11
GIT_REPOSITORY https://github.com/pybind/pybind11.git
GIT_TAG v2.11.1
)
FetchContent_MakeAvailable(pybind11)
문제 4: NumPy 배열 “buffer is not contiguous”
원인: arr.request()로 받은 버퍼가 C-contiguous가 아님 (예: arr.T, arr[:, ::2]).
해결법 1 — Python에서 연속 배열로 변환:
# 필요한 모듈 import
import numpy as np
arr = np.arange(12).reshape(3, 4).T # Fortran order
arr = np.ascontiguousarray(arr) # C order로 변환
result = example.sum_array(arr)
해결법 2 — C++에서 stride 처리:
double sum_array_strided(py::array_t<double> arr) {
auto buf = arr.request();
if (buf.ndim != 1) {
throw std::runtime_error("1D array only");
}
double* ptr = static_cast<double*>(buf.ptr);
size_t stride = buf.strides[0] / sizeof(double);
size_t n = buf.shape[0];
double s = 0;
for (size_t i = 0; i < n; ++i) {
s += ptr[i * stride];
}
return s;
}
문제 5: “Fatal Python error: PyThreadState_Get: no current thread”
원인: GIL을 해제한 상태에서 Python API를 호출함.
해결법: GIL 해제 구간에서는 Python 객체에 접근하지 말 것. 순수 C++ 포인터(buf.ptr 등)만 사용.
// ❌ 잘못된 예
double bad(py::array_t<double> arr) {
py::gil_scoped_release release;
py::print("hello"); // ❌ Python API 호출 - 크래시!
// ...
}
// ✅ 올바른 예
double good(py::array_t<double> arr) {
auto buf = arr.request(); // GIL 유지 상태에서 버퍼 정보 획득
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
py::gil_scoped_release release;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s; // 반환 시 GIL 자동 재획득
}
문제 6: “TypeError: … incompatible function arguments”
원인: Python에서 넘긴 타입이 C++ 함수 시그니처와 맞지 않음 (예: float 넘기고 int 기대).
해결법: py::arg()로 인자 힌트 추가:
m.def("add", &add, py::arg("a"), py::arg("b") = 0);
또는 Python에서 올바른 타입으로 변환:
example.add(int(x), int(y))
문제 7: Windows에서 “LINK : fatal error LNK1104: cannot open file ‘python311.lib’”
원인: Python 개발 라이브러리 경로를 찾지 못함. 해결법:
- python.org에서 “Windows embeddable package”가 아닌 전체 설치판 사용
- 또는
py -3.11 -m pip install pybind11후 해당 환경의 Python 사용
1억 개 합산: Pure Python, NumPy, pybind11 비교
벤치마크 설정
동일 연산(1억 개 double 합산)을 Pure Python, NumPy, pybind11 C++로 비교합니다.
# benchmark.py
import time
import numpy as np
def pure_python_sum(n):
total = 0.0
for i in range(n):
total += 1.0 / (i + 1)
return total
def numpy_sum(n):
arr = np.arange(1, n + 1, dtype=np.float64)
return np.sum(1.0 / arr)
# pybind11 모듈 (sum_array 또는 유사 함수)
# import engine
# def pybind11_sum(n):
# arr = np.arange(1, n + 1, dtype=np.float64)
# return engine.sum_array(1.0 / arr)
n = 100_000_000
t0 = time.perf_counter()
r1 = pure_python_sum(n)
t1 = time.perf_counter()
print(f"Pure Python: {t1-t0:.3f}s, result={r1:.6f}")
t0 = time.perf_counter()
r2 = numpy_sum(n)
t1 = time.perf_counter()
print(f"NumPy: {t1-t0:.3f}s, result={r2:.6f}")
결과를 읽는 법
직접 실행해 보면 Pure Python이 NumPy보다 수십 배 이상 느리게 나오는 것이 보통입니다. 원소마다 바이트코드를 해석하고 float 객체를 만드는 비용이 있기 때문입니다.
- NumPy는 C로 구현된 루프를 사용하므로 이미 매우 빠릅니다.
- 위 pybind11 버전은
1.0 / arr를 NumPy로 계산한 뒤 합산만 C++로 하므로, NumPy 버전과 거의 같은 속도가 나옵니다. 둘 다 800MB 배열을 메모리에서 읽는 속도가 병목입니다. 이렇게 NumPy로 깔끔하게 표현되는 연산은 C++로 옮겨도 얻을 것이 거의 없습니다. - pybind11이 크게 앞서는 것은 NumPy로 표현하기 어려운 로직입니다. 원소마다 조건이 갈리는 루프, 이전 결과에 의존하는 반복, 중간 배열을 여러 개 만들어야 하는 연산은 NumPy로 하면 여러 번 배열을 훑거나 Python 루프로 돌아가야 하지만, C++에서는 한 번의 루프로 끝납니다.
메모리 사용량
| 방식 | 1억 double (800MB) 처리 시 |
|---|---|
| Pure Python | 루프 변수만 사용, 배열 없음 |
| NumPy | 800MB (arange) + 800MB (1.0 / arr 결과) |
| pybind11 | 넘겨받은 배열은 복사 없이 공유 (단 위 예제는 1.0 / arr를 NumPy로 먼저 만들므로 NumPy와 같음) |
pybind11은 복사 없이 NumPy 버퍼를 그대로 사용하므로 메모리 오버헤드가 거의 없습니다.
에러 로깅, ABI 호환, pyproject.toml 패키징
패턴 1: 에러 처리와 로깅
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <spdlog/spdlog.h> // 또는 다른 로깅 라이브러리
namespace py = pybind11;
double safe_sum(py::array_t<double> arr) {
if (!arr) {
throw std::invalid_argument("배열이 None입니다");
}
auto buf = arr.request();
if (buf.ndim != 1) {
throw std::invalid_argument("1차원 배열만 지원합니다");
}
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s;
}
패턴 2: 버전 및 ABI 호환성
PYBIND11_MODULE(example, m) {
m.attr("__version__") = "1.0.0";
m.def("add", &add);
}
배포 시 Python 버전별 wheel 제공:
# manylinux로 Linux wheel 빌드
docker run --rm -v $(pwd):/io quay.io/pypa/manylinux_2_28_x86_64 \
/io/scripts/build_wheels.sh
패턴 3: 초기화/정리 훅
PYBIND11_MODULE(example, m) {
m.def("init", []() {
// 전역 리소스 초기화
return true;
});
m.def("shutdown", []() {
// 정리
});
}
패턴 4: 설정 주입
struct Config {
int num_threads = 4;
bool use_gpu = false;
};
Config g_config;
void set_config(int num_threads, bool use_gpu) {
g_config.num_threads = num_threads;
g_config.use_gpu = use_gpu;
}
PYBIND11_MODULE(example, m) {
m.def("set_config", &set_config);
}
패턴 5: pyproject.toml 기반 패키지 구조
my_engine/
├── pyproject.toml
├── src/
│ └── my_engine/
│ ├── __init__.py
│ └── _native.cpp
└── tests/
└── test_engine.py
# src/my_engine/__init__.py
from ._native import add, sum_array # noqa: F401
__all__ = ["add", "sum_array"]
패턴 6: 다차원 배열 처리 (2D 행렬)
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
// 2D 행렬의 각 행 합계 반환
py::array_t<double> row_sums(py::array_t<double> arr) {
auto buf = arr.request();
if (buf.ndim != 2) {
throw std::runtime_error("2D 배열만 지원합니다");
}
size_t rows = buf.shape[0];
size_t cols = buf.shape[1];
auto result = py::array_t<double>(rows);
auto rbuf = result.request();
double* ptr = static_cast<double*>(buf.ptr);
double* out = static_cast<double*>(rbuf.ptr);
size_t stride = buf.strides[0] / sizeof(double);
for (size_t i = 0; i < rows; ++i) {
double s = 0;
for (size_t j = 0; j < cols; ++j) {
s += ptr[i * stride + j];
}
out[i] = s;
}
return result;
}
PYBIND11_MODULE(matrix_ops, m) {
m.def("row_sums", &row_sums);
}
import numpy as np
import matrix_ops
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float64)
result = matrix_ops.row_sums(arr)
print(result) # [6. 15.]
배포 전 확인할 것
- Python 버전별 wheel 빌드 (3.8, 3.9, 3.10, 3.11 등)
- manylinux/macOS/Windows 플랫폼 지원
- 사용자 정의 예외 등록 (
py::register_exception) - GIL 해제 구간에서 Python API 미호출 확인
- NumPy 배열 연속성 검사 또는
np.ascontiguousarray문서화 -
__version__속성 노출 - 에러 메시지 한글/영문 일관성
- CI에서 빌드·테스트 자동화
GIL·예외·ABI 주의점과 첫 모듈 실습
GIL (Global Interpreter Lock)
- Python C API를 쓰는 확장은 GIL의 영향을 받습니다. 순수하게 숫자만 돌리는 C++ 구간에서는 py::gil_scoped_release 로 GIL을 풀어 두면, 다른 스레드가 Python을 실행할 수 있어 병렬화 시 유리할 수 있습니다. GIL을 풀었을 때는 Python 객체를 건드리지 않도록 주의합니다.
예외
- C++ 예외는 pybind11이 Python 예외로 변환해 전파합니다.
std::exception→RuntimeError등. 사용자 정의 예외도 등록해 두면 Python에서except로 잡을 수 있습니다.
ABI·버전
- 확장 모듈은 특정 Python 버전·컴파일러와 붙어 빌드되므로, 배포 시 같은 환경에서 쓰거나, wheel을 버전별로 여러 개 두는 방식이 필요합니다. 가상환경·conda와 함께 쓰면 관리가 수월합니다.
빌드 실패 시 체크리스트
- Python 버전:
find_package(Python3)또는 pip/conda 환경의 Python과 같은 버전·경로를 쓰는지 확인. 다른 Python을 쓰면 모듈이 import 시 로드되지 않을 수 있음. - pybind11 경로:
find_package(pybind11 CONFIG)가 실패하면, pybind11을 설치했는지, CMAKE_PREFIX_PATH나 pybind11_DIR에 올바른 경로가 잡혀 있는지 확인. - 컴파일러: MSVC는 /std:c++14 이상 권장. GCC/Clang은 -std=c++14 이상. pybind11은 C++11 이상을 요구.
- 확장자·이름: Windows에서는
.pyd, Linux/macOS에서는.so. 모듈 이름(PYBIND11_MODULE(이름, m)의 첫 인자)과import 이름이 일치해야 함. - 경로: 빌드 결과물(.pyd 또는 .so)이 Python이 찾는 경로(현재 디렉터리, PYTHONPATH, site-packages 등)에 있어야
import가능.
자주 하는 실수 요약
| 실수 | 해결법 |
|---|---|
| GIL 유지한 채 긴 연산 | py::gil_scoped_release 사용 |
| NumPy 배열 연속성 미확인 | np.ascontiguousarray() 또는 stride 처리 |
| 사용자 정의 예외 미등록 | py::register_exception<> 호출 |
| 모듈 이름 불일치 | PYBIND11_MODULE 첫 인자 = import 이름 |
| GIL 해제 후 Python API 호출 | 해제 구간에서는 C++ 데이터만 사용 |
단계별 실습: 첫 pybind11 모듈 만들기
1단계: 환경 준비 (5분)
# Python 개발 헤더 설치
sudo apt install python3-dev # Ubuntu/Debian
# 또는 brew install [email protected] # macOS
# pybind11 설치
pip install pybind11
2단계: 간단한 C++ 함수 작성 (3분)
// mymodule.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
int multiply(int a, int b) { return a * b; }
PYBIND11_MODULE(mymodule, m) {
m.def("multiply", &multiply);
}
3단계: 빌드 (2분)
c++ -O3 -Wall -shared -std=c++11 -fPIC \
$(python3 -m pybind11 --includes) \
mymodule.cpp -o mymodule$(python3-config --extension-suffix)
4단계: 테스트 (1분)
import mymodule
print(mymodule.multiply(6, 7)) # 42
소요 시간: 총 11분이면 첫 pybind11 모듈을 만들고 실행할 수 있습니다.
다음 단계로 나아가기
이 글을 마스터했다면:
- NumPy 고급 연동: stride, 다차원 배열, 메모리 레이아웃 최적화
- 멀티스레딩: GIL 관리, 병렬 처리 패턴
- 패키지 배포: PyPI에 wheel 배포, manylinux 빌드 관련 글: 멀티스레드 기초(#7-1), 성능 최적화(#15-1)
같이 보면 좋은 글
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ 제약된 환경에서의 C++: Exception과 RTTI 없이 안전한 코드 짜기 [#42-1]
- C++와 Python 중 무엇을 먼저 배울까
- C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
- pybind11로 C++와 Python 연결하기
- C++ Data Race
- C++ 캐시 히트(Cache Hit)를 높이는 메모리 정렬과 패딩 | False Sharing 해결
- C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#34-3]
자주 묻는 질문 (FAQ)
Q. “Fatal Python error: PyThreadState_Get: no current thread”는 왜 발생하나요?
A. py::gil_scoped_release로 GIL을 해제한 구간 안에서 py::print나 Python 객체 접근 같은 Python API를 호출하면 발생합니다. GIL을 해제한 동안에는 buf.ptr처럼 미리 꺼내 둔 순수 C++ 데이터만 다뤄야 합니다. 긴 연산 중간에 Python 객체가 꼭 필요하다면 그 부분만 py::gil_scoped_acquire로 GIL을 다시 잡고 접근합니다.
Q. NumPy만으로 부족한가요?
A. NumPy 벡터화로 충분한 경우가 많습니다. 하지만 조건문이 복잡하거나, 중첩 루프가 많거나, 이전 계산 결과에 의존하는 커스텀 알고리즘은 NumPy로 표현하기 어렵고 결국 Python 루프로 돌아가게 되는데, 이런 구간은 pybind11로 옮겼을 때 차이가 큽니다. pybind11으로 C++을 Python에서 불러 쓸 수 있어 고성능 엔진 연동에 적합합니다. 다음으로 Wasm·Emscripten(#35-2)를 읽어보면 좋습니다. 다음 글: [C++ 실무 융합 #35-2] WebAssembly(Wasm)와 Emscripten: C++을 브라우저에서 돌리기 이전 글: [C++ 면접 심화 #34-2] 캐시 히트(Cache Hit)를 높이는 C++ 메모리 정렬과 패딩