Python으로 돌리는 이미지 해시/유사도 매칭 로직 중 일부가 CPU bound라서 Rust로 포팅 실험. pyo3 0.14 버전, Python 3.9, Rust 1.55. maturin으로 빌드.
프로젝트 구조.
my_hash/
├── Cargo.toml
├── pyproject.toml
└── src/
└── lib.rs
Cargo.toml
[lib]
name = "my_hash"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.14", features = ["extension-module"] }
image = "0.23"
lib.rs — phash 같은 걸 Rust로 구현.
use pyo3::prelude::*;
use image::imageops::FilterType;
use image::DynamicImage;
#[pyfunction]
fn phash(path: &str) -> PyResult<u64> {
let img = image::open(path).map_err(|e| PyErr::new::<pyo3::exceptions::PyIOError, _>(e.to_string()))?;
let small = img.resize_exact(8, 8, FilterType::Lanczos3).to_luma8();
let mean: u32 = small.pixels().map(|p| p[0] as u32).sum::<u32>() / 64;
let mut hash: u64 = 0;
for (i, p) in small.pixels().enumerate() {
if p[0] as u32 > mean {
hash |= 1 << i;
}
}
Ok(hash)
}
#[pymodule]
fn my_hash(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(phash, m)?)?;
Ok(())
}
빌드/설치.
pip install maturin
maturin develop --release
Python에서 그냥 import 해서 씀.
from my_hash import phash
print(phash("sample.jpg"))
벤치
1000장 phash 계산.
- Pillow + numpy 구현: 11.2초
- Rust(pyo3) 구현: 2.8초
4배 개선. 이미지 디코드가 병목의 큰 부분이라 이 이상 당기려면 멀티스레드(rayon) 도입해야 함. GIL 풀리는 구간(py.allow_threads)에서 Python쪽 multiprocessing과도 조합 가능.
걸린 점
- pyo3 메이저 버전별로 API 꽤 바뀐다. 0.13 → 0.14 사이에도
wrap_pyfunction!사용법 달라짐. - 예외 매핑을 Rust Result → PyErr로 직접 해줘야 하는데, 에러 문자열 잘 보존 안 하면 파이썬 쪽 디버깅이 힘들다.
- 리눅스/맥/윈도 다 서포트하려면 maturin의 manylinux 빌드 세팅 해야 함. 사내 배포만이면 linux wheel 하나로 충분.
결론 — 코어 루프 몇 개만 Rust로 빼는 하이브리드 구조, 충분히 실용적. 단 팀에 Rust 알아야 유지됨.