C, C++로 작성된 코드를 파이썬에서 사용하기
파이썬으로 개발된 코드의 성능 향상을 위해 코드의 일부분을 C로 작성해야 하는 경우가 있다.
여러가지 방법들의 장단점을 비교해보려고 한다.
C extension interface
Python 런타임에 액세스 할 수 있는 API를 이용하여 c 함수를 정의할 수 있음.
허나, 간단한 함수 하나 호출을 위해서도 아래와 같이 어마어마한 boilerplate 코드를 요구함.
#include <Python.h>
static PyObject* SpamError = nullptr;
static PyObject*
spam_system(PyObject* , PyObject* args) {
const char* command = nullptr;
if(!PyArg_ParseTuple(args, "s", &command)) {
return nullptr;
}
const int sts = system(command);
if(sts != 0) {
PyErr_SetString(SpamError, "System command failed");
return nullptr;
}
Py_INCREF(Py_None);
return Py_None;
}
static PyMethodDef SpamMethods[] = {
{"system", spam_system, METH_VARARGS, "Execute a shell command"},
{nullptr, nullptr, 0, nullptr}
};
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT,
"spam",
"SPAM SPAM SPAM",
-1,
SpamMethods
};
PyMODINIT_FUNC
PyInit_spam() {
PyObject* module = PyModule_Create(&spammodule);
if(nullptr == module) return nullptr;
SpamError = PyErr_NewExceptionWithDoc(
"spam.error", "Error in spamming", nullptr, nullptr);
Py_INCREF(SpamError);
PyModule_AddObject(module, "error", SpamError);
return module;
}
cmake로 빌드할 수도 있지만, 빌드시 버전 의존성이 생기므로 setup.py를 이용하여 패키지 설치시 빌드하는게 일반적. (ex. numpy) 함수 정의가 들어 있는 import를 위한 py 파일도 만들어준다.
from distutils.core import setup, Extension
module = Extension('spam',
sources = ['algo.cpp'])
setup(name='spam',
version='1.0',
ext_modules=[module])
cmake로 만든 파일을 import할 때는 .so파일을 import 하면 된다.
(주: PyInit_spam()에서 spam과 패키지 (so 파일명)은 일치해야 한다.)
# build
$ python setup.py install
# install
$ python setup.py install
# usage
import spam
print(spam.system('ls'))
Q. pip remove로 삭제가 안되네.. 왜일까?
ctypes
from ctypes import cdll, Structure, c_int, c_double, c_uint
lib = cdll.LoadLibrary('./libsomelib.so')
print('Loaded lib {0}'.format(lib))
# Describe the DataPoint structure to ctypes.
class DataPoint(Structure):
_fields_ = [('num', c_int),
('dnum', c_double)]
# Initialize the DataPoint[4] argument. Since we just use the DataPoint[4]
# type once, an anonymous instance will do.
dps = (DataPoint * 4)((2, 2.2), (3, 3.3), (4, 4.4), (5, 5.5))
# Grab add_data from the library and specify its return type.
# Note: .argtypes can also be specifiedPyInit_spam() {
add_data_fn = lib.add_data
add_data_fn.restype = DataPoint
print('Calling add_data via ctypes')
dout = add_data_fn(dps, 4)
print('dout = {0}, {1}'.format(dout.num, dout.dnum))
장점: C에서는 C코드만 작성. ffi boilerplate는 python에서 작성. (함수 정보, 타입 캐스팅에 대해 정의)
문제점: c코드의 정의가 변할 때마다 저걸 매번 갱신하는것도 수고스럽다.. 자동으로 작성하도록 도와주는 툴이 있다. (ctypesgen)
cffi
매번 정의가 바뀔 때마다 파이썬 코드를 갱신해야되는 ctypes의 문제점 개선 버전. c코드로부터 정의와 데이터 타입을 유추하여 호출
할 수 있음.
from cffi import FFI
ffi = FFI()
lib = ffi.dlopen('./libsomelib.so')
print('Loaded lib {0}'.format(lib))
# Describe the data type and function prototype to cffi.
ffi.cdef('''
typedef struct {
int num;
double dnum;
} DataPoint;
DataPoint add_data(const DataPoint* dps, unsigned n);
''')
# Create an array of DataPoint structs and initialize it.
dps = ffi.new('DataPoint[]', [(2, 2.2), (3, 3.3), (4, 4.4), (5, 5.5)])
print('Calling add_data via cffi')
# Interesting variation: passing invalid arguments to add_data will trigger
# a cffi type-checking exception.
dout = lib.add_data(dps, 4)
print('dout = {0}, {1}'.format(dout.num, dout.dnum))
cython
아직 안써봄..
pybind11
c++11을 지원. 데이터 자동 캐스팅. (ex: boilerplate 없이 python list -> c++ vector로 자동으로 캐스팅.)
하지만 이걸 하기 위한 성능 희생.
아직 안써봄..
결론
성능? c extension > cython > pybind11 > cffi (생산성은 아마 그 반대로??)
실험 해보기. (성능이란 호출과 리턴을 위한 오버헤드 영향을 의미.)
- C 코드 동작 속도는 차이가 없을 듯.
- 그렇다면 경우에 따라 호출과 리턴에 의한 성능차이가 미미할 수 있다. 이땐 생산성 좋은걸 쓰자.
- C는 C코드만 작성하는게 제일 좋은 것 같다.(개인적 생각..)
- 꼭 파이썬에서만 쓸 라이브러리를 만드는게 아니므로.
참고
- https://github.com/krikit/ctypes_tutorial
- https://eli.thegreenplace.net/2013/03/09/python-ffi-with-ctypes-and-cffi
'software' 카테고리의 다른 글
Ubuntu에서 Rust 설치하기 (0) | 2020.09.07 |
---|---|
파이썬 int 타입의 특이한 내부구현 (0) | 2019.12.26 |
Google Cloud(GCP) - Pub/Sub (0) | 2019.11.03 |
우분투 개발용 환경 설정 (0) | 2019.09.01 |