brunch

You can make anything
by writing

C.S.Lewis

by 유윤식 Jun 13. 2024

Python: Maturin#02

#encode #decode #파이썬vs러스트 #orjson짱

Python 내장 함수가 가끔은 Rust 로 만든 라이브러리보다 빠를 때가 있다는 사실...


큰 String 데이터를 encode, decode 할때

encode, decode 함수를 Rust 로 만들어서

Python 에서 import 하고 성능을 비교(cProfiler)해보자.


추가로 orjson 과도 비교해보자.

사실 결과적으로는 orjson 을 사용하는게 가장 좋다.


먼저 간단하게 Rust 를 작성하기 위한 directory 구조를 만들면,


target directory 와 Cargo.lock 은 build 할 때 자동 생성된다.

lib.rs 와 Cargo.toml, pyproject.toml 파일을 수동으로 생성한다.


먼저 lib.rs


use pyo3::prelude::*;


#[pyfunction]

fn encode_string(input: &str) -> PyResult<Vec<u8>> {

    Ok(input.as_bytes().to_vec())

}


#[pyfunction]

fn decode_string(input: Vec<u8>) -> PyResult<String> {

    match String::from_utf8(input) {

        Ok(s) => Ok(s),

        Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Invalid UTF-8 sequence: {}", e))),

    }

}


#[pymodule]

fn rust_encoding(py: Python, m: &PyModule) -> PyResult<()> {

    m.add_function(wrap_pyfunction!(encode_string, m)?)?;

    m.add_function(wrap_pyfunction!(decode_string, m)?)?;

    Ok(())

}


그 다음 Cargo.toml 파일을 작성한다.


[package]

name = "rust_encoding"

version = "0.1.0"

edition = "2018"


[dependencies]

pyo3 = { version = "0.15", features = ["extension-module"] }


[lib]

crate-type = ["cdylib"]


마지막으로 pyproject.toml 파일을 만들어준다.


[build-system]

requires = ["maturin>=0.14.0"]

build-backend = "maturin"


[tool.maturin.metadata]

name = "rust_encoding"

requires-python = ">=3.6"



이렇게 하면 rust_encoding 이라는 directory 에서 작업을 진행할 수 있는데,

>> pip install maturin

>> maturin build


build 가 끝나면 target 밑에 wheels 가 생성되는데 해당 파일을 pypi 레파지토리에 업로드 해보자.

>> pip install twine

>> twine upload target/wheels/*


이때 pypi 계정 정보(API TOKEN, PASSWORD)가 필요하다.

https://pypi.org/account/register/

계정이 없다면 하나 만들어서 인생 첫(?) pypi 모듈을 생성해보자!


여차여차 업로드를 완료하면

rust_encoding 라이브러리를 확인해볼 수 있다.


https://pypi.org/project/rust-encoding/


이제 실제로 로컬에 설치해서 성능을 비교해보면,

>> pip install rust-encoding

>> pip install orjson


이제 쥬피터 노트북에서 함수를 작성해보자.


# 먼저 데이터를 대략 크게(?) 만들고,

data = [str(i) + " 번째 데이터 입니다." for i in range(1_000_000)]


# 모듈을 import 하고,

import rust_encoding

import json

import orjson

import cProfile

import contextlib


# Rust 모듈에서 encode_string, decode_string 함수를 가져옴

rust_encode_string = rust_encoding.encode_string

rust_decode_string = rust_encoding.decode_string


# 파이썬 내장 모듈의 인코딩 및 디코딩 함수

py_encode_string = json.dumps

py_decode_string = json.loads


# Rust 로 작성된 공식 인코딩 및 디코딩 함수

rust_encode_string = orjson.dumps

rust_decode_string = orjson.loads


# 큰 데이터를 문자열로 변환 > 안전장치(data 가 list, dict 일 때도 작동하도록)

data2 = json.dumps(data)


@contextlib.contextmanager

def cprofile_context(func_name):

    pr = cProfile.Profile()

    pr.enable()

    yield

    pr.disable()

    print(func_name)

    ps = pstats.Stats(pr)

    ps.print_stats()


def profile_encode():

    # Rust encode_string 함수를 사용하여 인코딩

    with cprofile_context("myrust_encoding.prof"):

        rust_encode_string(data2)


def profile_decode():

    # Rust decode_string 함수를 사용하여 디코딩

    with cprofile_context("myrust_decoding.prof"):

        rust_decode_string(rust_encode_string(data2))


def profile_py_encode():

    # Python 내장 모듈을 사용하여 인코딩

    with cprofile_context("py_encoding.prof"):

        py_encode_string(data)


def profile_py_decode():

    # Python 내장 모듈을 사용하여 디코딩

    with cprofile_context("py_decoding.prof"):

        py_decode_string(py_encode_string(data))


def profile_rust_encode():

    # Python 내장 모듈을 사용하여 인코딩

    with cprofile_context("rust_encoding.prof"):

        rust_encode_string(data)


def profile_rust_decode():

    # Python 내장 모듈을 사용하여 디코딩

    with cprofile_context("rust_decoding.prof"):

        rust_decode_string(rust_encode_string(data))


# 함수 호출

profile_encode()

profile_decode()

profile_py_encode()

profile_py_decode()

profile_rust_encode()

profile_rust_decode()


이대로 실행하면

그냥 orjson 쓰자!

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari