
가변카피 첫 예제에서 데이터 양이 너무 많아지면
SWAP 공간에 대한 오버헤드가 발생한다는 사실을 확인해 보았다.
파일을 마치 메모리인 것처럼 다루는 MMAP을 사용하면
그런 오버헤드를 완화할 수 있다.
일반적인 파일 읽기는 File -> OS Buffer -> User Buffer 순으로
데이터를 복사하여 사용하는 반면,
MMAP은 파일을 프로세스의 가상 주소 공간에 직접 매핑한다.
데이터를 명시적으로 read() 하지 않아도 되며
RAM 용량보다 훨씬 큰 파일을 읽는 경우에도
파일 크기만큼의 메모리 공간이 있는 것처럼 사용할 수 있다.
~/workspace$ mkdir memory-mapping && cd memory-mapping ~/workspace/memory-mapping$ python3 -m venv venv ~/workspace/memory-mapping$ source venv/bin/activate ~/workspace/memory-mapping$ # 평소에 설치하던 것 외에 추가된 라이브러리를 놓치지 말자 ~/workspace/memory-mapping$ pip install maturin fastapi uvicorn orjson numpy ~/workspace/memory-mapping$ maturin init ~/workspace/memory-mapping$ # 선택지 중 기본값인 PyO3 선택 ~/workspace/memory-mapping$ # Cargo.toml과 src/lib.rs가 자동 생성된다 ~/workspace/memory-mapping$ # Python 코드는 직접 생성해 주어야 한다 ~/workspace/memory-mapping$ mkdir app && touch app/main.py ~/workspace/memory-mapping$ tree -I venv . ├── app │ └── main.py ├── Cargo.toml ├── pyproject.toml └── src └── lib.rs
Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.
MMAP을 위해서는 memmap2 크레이트를 불러와야 한다.
Cargo.toml[package] name = "memory-mapping" version = "0.1.0" edition = "2024" [lib] name = "rust_engine" crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.0" numpy = "0.28" rayon = "1.11" memmap2 = "0.9"
파일 경로를 받아 그 파일을 MMAP으로 열고,
안에 들어있는 f64 데이터를 제로카피로 읽어 합계를 계산한다.
바이트 데이터를 f64 슬라이스로 변환하여 사용할 것이다.
f64는 8바이트이므로, 전체 바이트를 8로 나눈 만큼의 슬라이스를 만들고
각 슬라이스의 합을 Rayon을 통해 병렬 연산한다.
src/lib.rsuse pyo3::prelude::*; use memmap2::Mmap; use std::fs::File; use rayon::prelude::*; #[pyfunction] fn sum_mmap_file(file_path: String) -> PyResult<f64> { let file = File::open(file_path)?; let mmap = unsafe { Mmap::map(&file)? }; let raw_ptr = mmap.as_ptr() as *const f64; let len = mmap.len() / 8; let slice = unsafe { std::slice::from_raw_parts(raw_ptr, len) }; let total_sum = slice.par_iter().sum(); Ok(total_sum) } #[pymodule] fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_mmap_file, m)?)?; Ok(()) }
MMAP 테스트를 위해 거대한 바이너리 파일을 만드는 함수와
실제 MMAP 연산을 Rust에게 요청하는 함수를 각각 작성한다.
여기서는 전부 1.0 으로 채워진 GB 단위의 바이너리 파일을 사용하겠다.
app/main.pyimport numpy as np from fastapi import FastAPI from fastapi.responses import ORJSONResponse import rust_engine import os 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("/setup-mmap-file/{gb}") def setup_file(gb: int): file_path = "huge_data.bin" size = gb * 1024 * 1024 * 1024 elements = size // 8 data = np.ones(elements, dtype=np.float64) data.tofile(file_path) return { "message": f"{gb}GB 파일 생성 완료", "path": file_path } @app.get("/sum-mmap") def sum_mmap(): file_path = "huge_data.bin" if not os.path.exists(file_path): return { "error": "파일이 없습니다. /setup-mmap-file 라우트를 먼저 호출하세요." } start = time.perf_counter() result = rust_engine.sum_mmap_file(file_path) end = time.perf_counter() rust_duration = end - start return { "file_size": f"{os.path.getsize(file_path) / (1024**3)} GB", "result": result, "rust_pure_time": f"Rust 연산에 걸린 시간: {rust_duration:.4f} sec" }
Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
병렬 처리가 포함된 코드는 성능 최적화를 위해 --release 를 붙여 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.
uvicorn 라이브러리를 통해 FastAPI를 실행한다.
~/workspace/memory-mapping$ maturin develop --release ~/workspace/memory-mapping$ uvicorn app.main:app --reload
curl 명령어 또는 브라우저를 통해 다음과 같은 테스트를 해볼 수 있다.
http://127.0.0.1:8000/setup-mmap-file/8http://127.0.0.1:8000/sum-mmap~$ curl -i http://127.0.0.1:8000/setup-mmap-file/8 HTTP/1.1 200 OK date: Mon, 30 Mar 2026 01:19:13 GMT server: uvicorn content-length: 61 content-type: application/json; charset=utf-8 {"message":"8GB 파일 생성 완료","path":"huge_data.bin"}% ~$ curl -i http://127.0.0.1:8000/sum-mmap HTTP/1.1 200 OK date: Mon, 30 Mar 2026 01:19:18 GMT server: uvicorn content-length: 104 content-type: application/json; charset=utf-8 {"file_size":"8.0 GB","result":1073741824.0,"rust_pure_time":"Rust 연산에 걸린 시간: 0.5097 sec"}%데이터 크기가 작으면 OS 페이지 캐시의 도움으로 매우 빠르게 완료된다. (약 15.6 GB/s)
~$ curl -i http://127.0.0.1:8000/setup-mmap-file/16 HTTP/1.1 200 OK date: Mon, 30 Mar 2026 01:19:29 GMT server: uvicorn content-length: 62 content-type: application/json; charset=utf-8 {"message":"16GB 파일 생성 완료","path":"huge_data.bin"}% ~$ curl -i http://127.0.0.1:8000/sum-mmap HTTP/1.1 200 OK date: Mon, 30 Mar 2026 01:19:34 GMT server: uvicorn content-length: 105 content-type: application/json; charset=utf-8 {"file_size":"16.0 GB","result":2147483648.0,"rust_pure_time":"Rust 연산에 걸린 시간: 2.8441 sec"}%캐시의 도움을 받지 못하는 순수 SSD 읽기 속도로 수행된다. (약 5.6 GB/s)
~$ curl -i http://127.0.0.1:8000/setup-mmap-file/80 HTTP/1.1 200 OK date: Mon, 30 Mar 2026 01:20:02 GMT server: uvicorn content-length: 62 content-type: application/json; charset=utf-8 {"message":"80GB 파일 생성 완료","path":"huge_data.bin"}% ~$ curl -i http://127.0.0.1:8000/sum-mmap HTTP/1.1 200 OK date: Mon, 30 Mar 2026 01:20:51 GMT server: uvicorn content-length: 107 content-type: application/json; charset=utf-8 {"file_size":"80.0 GB","result":10737418240.0,"rust_pure_time":"Rust 연산에 걸린 시간: 34.3357 sec"}%RAM(64GB) 크기를 초과하여 시간이 많이 걸린다. (약 2.3 GB/s)
100억 개의 데이터를 약 55초만에 처리했던 것(약 1.44 GB/s)에 비하면 많이 빨라졌지만.크기가 작은 데이터는 메모리에 먼저 올려 놓고 사용하는
일반적인 제로카피가 더 빠른 연산이 가능하지만
데이터 크기가 커질수록 MMAP을 사용하는 게 이득이다.