
큰 데이터를 다루게 될 경우
Python에서 Rust로 데이터를 옮겨 오는 게 병목 현상을 야기할 수 있다.
따라서 큰 데이터를 다룰 때는 데이터 자체를 복사하는 과정을 생략하고
메모리 주소만 넘겨 받아 사용하는 편이 효율적이다.
Rust의 rust-numpy 를 사용하면 Python의 Numpy 배열을
복사 없이 직접 읽고 쓸 수 있다.
~/workspace$ mkdir zero-copy && cd zero-copy ~/workspace/zero-copy$ python3 -m venv venv ~/workspace/zero-copy$ source venv/bin/activate ~/workspace/zero-copy$ # 평소에 설치하던 것 외에 추가된 라이브러리를 놓치지 말자 ~/workspace/zero-copy$ pip install maturin fastapi uvicorn orjson numpy ~/workspace/zero-copy$ maturin init ~/workspace/zero-copy$ # 선택지 중 기본값인 PyO3 선택 ~/workspace/zero-copy$ # Cargo.toml과 src/lib.rs가 자동 생성된다 ~/workspace/zero-copy$ # Python 코드는 직접 생성해 주어야 한다 ~/workspace/zero-copy$ mkdir app && touch app/main.py ~/workspace/zero-copy$ tree -I venv . ├── app │ └── main.py ├── Cargo.toml ├── pyproject.toml └── src └── lib.rs
Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.
Python의 Numpy 배열을 가져다 쓰기 위한 numpy 크레이트를 추가하고,
큰 데이터를 빠르게 처리할 수 있도록 병렬 처리를 위한 Rayon 크레이트,
그리고 데이터를 처리하는 시간동안 다른 작업이 멈추어 있지 않도록
비동기 처리를 위한 takio 크레이트와 pyo3_async_runtimes 크레이트도 추가해 준다.
여담이지만 numpy, pyo3_async_runtimes 와 같이
Python과 연관된 크레이트의 경우 대체로 PyO3와 버전이 함께 올라간다.
PyO3의 버전이 올라감에 따라 변경점에 최적화하여 같이 변경되기 때문에
되도록이면 이들끼리 버전을 맞춰주는 게 좋다.
Cargo.toml[package] name = "zero-copy" version = "0.1.0" edition = "2024" [lib] name = "rust_engine" crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.0" numpy = "0.28" rayon = "1.11" tokio = { version = "1.50", features = ["full"] } pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
Python의 1차원 Numpy 배열을 사용하기 위한 자료형을 가져온다.
실무에서는 PyArray1 과 PyReadonlyArray1 을 세트로 가져오는 경우가 많지만
우리는 배열을 생성하지는 않고 사용만 할 것이므로 후자만 가져와도 충분하다.
Numpy 배열을 복사 없이 그대로 사용하여 병렬 연산을 수행할 것이다.
src/lib.rsuse pyo3::prelude::*; use numpy::PyReadonlyArray1; use rayon::prelude::*; #[pyfunction] fn zero_copy_sum(array: PyReadonlyArray1<f64>) -> f64 { let slice = array.as_slice().expect("Numpy 슬라이스를 가져오는 데 실패했습니다."); slice.par_iter().sum() } #[pymodule] fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(zero_copy_sum, m)?)?; Ok(()) }
사용자로부터 입력받은 크기의 Numpy 배열을 임의로 생성하여
Rust에게 모든 배열 원소의 합을 구하라고 던져 준다.
데이터 생성 등으로 인한 오버헤드를 제외하고
순수 Rust 연산 시간을 알아보기 위해 time 라이브러리를 사용한다.
app/main.pyimport numpy as np from fastapi import FastAPI from fastapi.responses import ORJSONResponse import rust_engine import time class UTF8ORJSONResponse(ORJSONResponse): media_type = "application/json; charset=utf-8" app = FastAPI(default_response_class=UTF8ORJSONResponse) @app.get("/") def read_root(): return { "status": "200", "info": "서버 가동 중입니다." } @app.get("/compute/{size}") def compute(size: int): data = np.random.rand(size) start = time.perf_counter() result = rust_engine.zero_copy_sum(data) end = time.perf_counter() rust_duration = end - start return { "size": len(data), "result": result, "rust_pure_time": f"Rust 연산에 걸린 시간: {rust_duration:.4f} sec" }
Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
병렬 처리가 포함된 코드는 성능 최적화를 위해 --release 를 붙여 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.
uvicorn 라이브러리를 통해 FastAPI를 실행한다.
~/workspace/zero-copy$ maturin develop --release ~/workspace/zero-copy$ uvicorn app.main:app --reload
curl 명령어 또는 브라우저를 통해 다음과 같은 테스트를 해볼 수 있다.
http://127.0.0.1:8000/compute/100000000~$ curl -i http://127.0.0.1:8000/compute/100000000 HTTP/1.1 200 OK date: Thu, 26 Mar 2026 00:49:03 GMT server: uvicorn content-length: 105 content-type: application/json; charset=utf-8 {"size":100000000,"result":50003983.53816322,"rust_pure_time":"Rust 연산에 걸린 시간: 0.0074 sec"}%
~$ curl -i http://127.0.0.1:8000/compute/1000000000 HTTP/1.1 200 OK date: Thu, 26 Mar 2026 00:49:08 GMT server: uvicorn content-length: 107 content-type: application/json; charset=utf-8 {"size":1000000000,"result":499998781.80647945,"rust_pure_time":"Rust 연산에 걸린 시간: 0.0681 sec"}%
~$ curl -i http://127.0.0.1:8000/compute/10000000000 HTTP/1.1 200 OK date: Thu, 26 Mar 2026 00:49:22 GMT server: uvicorn content-length: 108 content-type: application/json; charset=utf-8 {"size":10000000000,"result":5000036618.422572,"rust_pure_time":"Rust 연산에 걸린 시간: 55.5093 sec"}%데이터 양이 너무 많아지면 SWAP 공간에 대한 오버헤드가 발생한다.
그런 경우에는 MMAP을 사용할 수 있는데 그것에 대해서는 이후에 알아보도록 하자.