좋아요, 해결책은 Numba에서 llvmlite 라이브러리를 많이 사용합니다.
직렬화 기능을 우리가 Numba 일부 함수를 정의
첫째을 얻기.
import numba
@numba.jit("f8(f8)", nopython=True)
def example(x):
return x + 1.1
, 당신은 그것이 ELF 인코딩 된 바이트 배열 (bytes
객체가 아닌 str
당신이 만약이 있다고 볼 수는 elfbytes
을 인쇄 할 경우 우리는
cres = example.overloads.values()[0] # 0: first and only type signature
elfbytes = cres.library._compiled_object
와 오브젝트 코드에 액세스 할 수 있습니다 파이썬 3에서). 이것은 공유 라이브러리 나 실행 파일을 컴파일 할 때 파일에 들어갈 수 있기 때문에 동일한 아키텍처, 동일한 라이브러리 등이있는 모든 컴퓨터에 이식 할 수 있습니다.
이 번들에는 몇 가지 기능이 있습니다.
print(cres.library.get_llvm_str())
우리가 __main__.example$1.float64
라는 원하는 일을 우리는 그것 LLVM IR에 입력 한 서명을 볼 수 있습니다 다음 LLVM IR를 덤핑으로 볼 나중에 참조 할 수 있도록
define i32 @"__main__.example$1.float64"(double* noalias nocapture %retptr, { i8*, i32 }** noalias nocapture readnone %excinfo, i8* noalias nocapture readnone %env, double %arg.x) #0 {
entry:
%.14 = fadd double %arg.x, 1.100000e+00
store double %.14, double* %retptr, align 8
ret i32 0
}
를 적어 둡니다을 : 첫 번째 인수가에 대한 포인터이 결과로 덮어 쓰게되면 두 번째와 세 번째 인수는 사용되지 않는 포인터가되고 마지막 인수는 double
입력이됩니다.
(프로그래밍 방식으로 [x.name for x in cres.library._final_module.functions]
을 사용하여 함수 이름을 얻을 수 있으며 Numba가 실제로 사용하는 진입 점은 cres.fndesc.mangled_name
입니다.)
모든 ELF 및 함수 서명을 모든 컴퓨팅을 수행하는 컴퓨터로 컴파일하는 모든 컴퓨터에서 전송합니다.
우리가 전혀 Numba과 llvmlite를 사용하는거야, 컴퓨팅 시스템에서 이제 다시
를 읽기 (this page 다음).
def object_compiled_hook(ll_module, buf):
pass
def object_getbuffer_hook(ll_module):
return elfbytes
engine.set_object_cache(object_compiled_hook, object_getbuffer_hook)
는 같은 엔진을 마무리 : LLVM 실행 엔진 만들기
import llvmlite.binding as llvm
llvm.initialize()
llvm.initialize_native_target()
llvm.initialize_native_asmprinter() # yes, even this one
을 : 초기화
target = llvm.Target.from_default_triple()
target_machine = target.create_target_machine()
backing_mod = llvm.parse_assembly("")
engine = llvm.create_mcjit_compiler(backing_mod, target_machine)
을 그리고 지금은 elfbytes
라는 우리의 ELF로드가 자사의 캐싱 메커니즘을 납치 우리는 IR을 컴파일했지만 실제로는 그 단계를 건너 뛰었습니다. 엔진은 디스크 기반 캐시에서 발생한다고 생각하여 ELF를로드합니다.
engine.finalize_object()
이제이 엔진의 공간에서 우리의 기능을 발견 할 것입니다. 다음이 0L
을 반환하면 잘못된 것이 있습니다. 함수 포인터 여야합니다.
func_ptr = engine.get_function_address("__main__.example$1.float64")
이제는 func_ptr
을 우리가 호출 할 수있는 ctypes 함수로 해석해야합니다. 수동으로 서명을 설정해야합니다.
import ctypes
pdouble = ctypes.c_double * 1
out = pdouble()
pointerType = ctypes.POINTER(None)
dummy1 = pointerType()
dummy2 = pointerType()
# restype first then argtypes...
cfunc = ctypes.CFUNCTYPE(ctypes.c_int32, pdouble, pointerType, pointerType, ctypes.c_double)(func_ptr)
그리고 지금 우리는 그것을 호출 할 수
cfunc(out, dummy1, dummy2, ctypes.c_double(3.14))
print(out[0])
# 4.24, which is 3.14 + 1.1. Yay!
더 많은 합병증
JITed 기능은 결국, 당신은 많은 값을 통해 꽉 루프를 수행 할 (배열 입력이있는 경우 파이썬이 아닌 컴파일 된 코드에서) Numba는 Numpy 배열을 인식하는 코드를 생성합니다. 예외 객체에 대한 포인터와 Numpy 배열에 별도의 매개 변수로 제공되는 모든 메타 데이터를 포함하여이 호출 규칙은 상당히 복잡합니다. 그것은 이 아니고은 Numpy의 ctypes 인터페이스에서 사용할 수있는 진입 점을 생성합니다.
그러나 매우 높은 수준의 진입 점은 Python *args, **kwds
을 인수로 사용하여 내부적으로 파싱합니다. 사용 방법은 다음과 같습니다.
첫째, 누구의 이름으로 시작하는 기능을 찾아 "으로 CPython을."
name = [x.name for x in cres.library._final_module.functions if x.name.startswith("cpython.")][0]
그들 중 정확히 하나가 있어야합니다.
func_ptr = engine.get_function_address(name)
을 세 PyObject*
인수 한 PyObject*
반환 값으로 캐스팅 그런 다음, 직렬화 및 역 직렬화 후, 상기 한 방법을 사용하여 함수 포인터를 얻는다. (LLVM은 이것들이 i8*
라고 생각합니다.)
class PyTypeObject(ctypes.Structure):
_fields_ = ("ob_refcnt", ctypes.c_int), ("ob_type", ctypes.c_void_p), ("ob_size", ctypes.c_int), ("tp_name", ctypes.c_char_p)
class PyObject(ctypes.Structure):
_fields_ = ("ob_refcnt", ctypes.c_int), ("ob_type", ctypes.POINTER(PyTypeObject))
PyObjectPtr = ctypes.POINTER(PyObject)
cpythonfcn = ctypes.CFUNCTYPE(PyObjectPtr, PyObjectPtr, PyObjectPtr, PyObjectPtr)(fcnptr)
이 세 가지 인수의 첫 번째는 폐쇄 (함수가 액세스하는 전역 변수)이며, 나는 우리가 필요하지 않은 가정거야. 클로저 대신 명시 적 인수를 사용하십시오. 우리는 CPython의 id()
구현이 PyObject
포인터를 만들기 위해 포인터 값을 반환한다는 사실을 사용할 수 있습니다.
def wrapped(*args, **kwds):
closure =()
return cpythonfcn(ctypes.cast(id(closure), PyObjectPtr), ctypes.cast(id(args), PyObjectPtr), ctypes.cast(id(kwds), PyObjectPtr))
는 이제 기능은 원래 Numba 디스패처 기능과 같은
wrapped(whatever_numpy_arguments, ...)
로 호출 할 수 있습니다.
모든 그 후 결론
, 그것은 그것의 가치가 있었다? Numba로 엔드 투 엔드 컴파일을하는 것은 쉬운 방법입니다.이 간단한 기능을 사용하려면 50ms가 걸립니다. 기본 -O2
대신 -O3
을 요청하면이 속도를 40 % 느리게 할 수 있습니다.
미리 컴파일 된 ELF 파일에서 스 플라이 싱하는 데는 0.5ms가 걸리지 만 100 배 더 빠릅니다. 더욱 복잡한 함수에서는 컴파일 시간이 길어 지지만 splicing-in 절차는 모든 함수에 대해 항상 0.5ms가 걸립니다.
제 신청서의 경우 이는 절대적으로 가치가 있습니다. 즉, 한 번에 10MB의 계산을 수행 할 수 있고 컴파일 (작업 준비)보다는 컴퓨팅 (실제 작업)을하는 데 대부분의 시간을 소비 할 수 있습니다. 이것을 100 배로 늘리면 한 번에 1GB로 계산을 수행해야합니다. 한 머신이 100GB의 주문으로 제한되어 있으며 100 개의 주문 프로세스에서 공유되어야하므로 문제가 너무 세분화 될 수 있기 때문에 리소스 제한,로드 밸런싱 문제 등이 발생할 위험이 더 큽니다. .
그러나 다른 응용 프로그램의 경우 50ms는 아무것도 아닙니다. 모두 응용 프로그램에 따라 다릅니다.
numba 코드가 동일한 입력 유형으로 매번 동일한 프로세스에서 실행되는 경우 처음에는 jit 페널티 만 지불하면 시작할 때 수행하는 "워밍업"절차가있을 수 있으므로 첫 번째 쿼리에서 지불하십시오. 또는 Numba의 사전 컴파일 기능을 볼 수도 있습니다. 하지만 유스 케이스 제약을 완전히 이해하지 못하고있다. – JoshAdel
또한 이것은 핵심 개발자로부터 응답을 얻을 수있는 Numba 전자 메일 목록에 더 적합한 질문 유형입니다. – JoshAdel
코드는 모든 쿼리마다 다릅니다. Numba 목록에 대해서도 물어볼 것이지만 Numba 질문 만이 아니기 때문에 여기에서 묻습니다. 해결책은 Numba 자체가 아니라 LLVM을 직접 통과 할 수 있습니다. (비슷하게, Numpy보다는 ctypes를 사용하여 Numpy 문제를 해결했습니다. Numpy가 직접적으로 말했던 것보다 더 많은 LLVM 지식을 가진 사람의 도움을 받기를 바랍니다.) –