#cython: boundscheck=False, wraparound=False, nonecheck=False, cdivision=True, language_level=3
cpdef int query(double[::1] q, double[:,::1] data) nogil:
cdef:
int n = data.shape[0]
int dim = data.shape[1]
int best_i = -1
double best_ip = -1
double ip
for i in range(n):
ip = 0
for j in range(dim):
ip += q[j] * data[i, j]
if ip > best_ip:
best_i = i
best_ip = ip
return best_i
컴파일 후 Python에서 코드 시간을 측정합니다.
import numpy as np
import ip
n, dim = 10**6, 10**2
X = np.random.randn(n, dim)
q = np.random.randn(dim)
%timeit ip.query(q, X)
약 100ms가 걸립니다. 한편 동등한 것 numpy code
:
%timeit np.argmax(q @ X.T)
약 50ms가 걸립니다.
이것은 NumPy
코드가 q @ X.T
argmax를 취하기 전에 큰 배열을 할당해야 하기 때문에 이상합니다. 따라서 내가 부족한 최적화가 있는지 궁금합니다.
extra_compile_args=["-O3", '-march=native'],
내 setup.py에 추가 했으며 함수 정의를 다음과 같이 변경하려고 시도했습니다.
cpdef int query(np.ndarray[double] q, np.ndarray[double, ndim=2] data):
그러나 성능에는 거의 차이가 없었습니다.
이 작업 q @ X.T
은 dgemv
OpenBlas 또는 MKL (분포에 따라 다름) 의 행렬-벡터 곱셈 ( ) 구현에 매핑됩니다. 즉, 가장 최적화 된 알고리즘 중 하나에 반대한다는 의미입니다.
결과 벡터에는 1M 요소가 있으므로 약 8MB 메모리가됩니다. 8MB는 항상 L3 캐시에 맞지는 않지만 RAM조차도 약 15GB / s의 대역폭을 가지고 있으므로 8MB 쓰기 / 읽기에는 최대 1-2ms가 소요되며 전체 실행 시간의 약 50ms에 비해 큰 이득이 아닙니다.
코드의 가장 큰 문제는 q @X.T
. 그것은 계산
((q[0]*data[i,0]+q[1]*data[i,1])+q[2]*data[i,2])+...
IEEE 754 때문에 컴파일러는 연산을 재정렬 할 수 없으며 최적이 아닌 순서로 실행합니다. 두 번째 합계를 계산하려면 첫 번째 합계가 수행 될 때까지 연산을 기다려야합니다. 이 접근 방식은 현대 아키텍처의 모든 잠재력을 사용하지 않습니다.
좋은 dgemv
구현은 훨씬 더 나은 작업 순서를 선택합니다. 비슷한 문제가 있지만 합계가이 SO-post 에서 찾을 수 있습니다 .
필드 -ffast-math
를 평준화하기 위해 컴파일러가 작업을 다시 수행하여 파이프 라인을 더 잘 사용할 수 있도록을 사용할 수 있습니다.
벤치 마크에 대한 내 컴퓨터의 결과는 다음과 같습니다.
%timeit query(q, X) # 101 ms
%timeit query_ffastmath(q, X) # 56.3 ms
%timeit np.argmax(q @ X.T) # 50.2 ms
여전히 약 10 % 정도 더 나쁘지만 컴파일러가 특히 내 프로세서를 위해 전문가가 만든 수작업 버전을 이길 수 있다면 정말 놀랄 것입니다.
이 기사는 인터넷에서 수집됩니다. 재 인쇄 할 때 출처를 알려주십시오.
침해가 발생한 경우 연락 주시기 바랍니다[email protected] 삭제
몇 마디 만하겠습니다