왜 numpy가 Python의 ctype보다 매트릭스 곱셈이 빠를까요?
행렬 곱셈을 가장 빨리 할 수 있는 방법을 알아내려고 세 가지 방법을 시도했습니다.
- 순수 Python 구현: 놀라운 점은 없습니다.
- Numpy를 사용한
numpy.dot(a, b)
- 'C'를
ctypes
【피톤】
공유 라이브러리로 변환되는 C 코드는 다음과 같습니다.
#include <stdio.h>
#include <stdlib.h>
void matmult(float* a, float* b, float* c, int n) {
int i = 0;
int j = 0;
int k = 0;
/*float* c = malloc(nay * sizeof(float));*/
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
int sub = 0;
for (k = 0; k < n; k++) {
sub = sub + a[i * n + k] * b[k * n + j];
}
c[i * n + j] = sub;
}
}
return ;
}
Python 코드는 다음과 같습니다.
def C_mat_mult(a, b):
libmatmult = ctypes.CDLL("./matmult.so")
dima = len(a) * len(a)
dimb = len(b) * len(b)
array_a = ctypes.c_float * dima
array_b = ctypes.c_float * dimb
array_c = ctypes.c_float * dima
suma = array_a()
sumb = array_b()
sumc = array_c()
inda = 0
for i in range(0, len(a)):
for j in range(0, len(a[i])):
suma[inda] = a[i][j]
inda = inda + 1
indb = 0
for i in range(0, len(b)):
for j in range(0, len(b[i])):
sumb[indb] = b[i][j]
indb = indb + 1
libmatmult.matmult(ctypes.byref(suma), ctypes.byref(sumb), ctypes.byref(sumc), 2);
res = numpy.zeros([len(a), len(a)])
indc = 0
for i in range(0, len(sumc)):
res[indc][i % len(a)] = sumc[i]
if i % len(a) == len(a) - 1:
indc = indc + 1
return res
빨랐을 제가 졌을 라고 생각합니다!은 제가 C를 사용한 것 벤치마크입니다.!!고!!!!!!!!다음은 제가 잘못 했거나 잘못 했음을 나타내는 벤치마크입니다.numpy
무니없::::::
이랬는지 numpy
.ctypes
버전입니다. Python의 순수한 구현에 대해 말하는 것도 아닙니다. 왜냐하면 그것은 좀 명백하기 때문입니다.
NumPy는 행렬 곱셈을 위해 고도로 최적화되고 신중하게 조정된 BLAS 방법을 사용합니다(ATLAS 참조).이 경우의 고유 함수는 GEMM(범용 매트릭스 곱셈용)입니다.검색하면 원본을 찾을 수 있습니다.dgemm.f
(Netlib에 있습니다).
참고로 최적화는 컴파일러 최적화를 넘어서는 것입니다.위에서 필립은 코퍼스미스-위노그라드를 언급했다.제 기억이 맞다면, 이것은 ATLAS의 행렬 곱셈의 대부분의 경우에 사용되는 알고리즘입니다(코멘터는 이것이 Strassen의 알고리즘일 수 있다고 지적합니다).
바꿔 말하면,matmult
알고리즘은 간단한 구현입니다.이 노래의 주인공입니다.
Numpy에 대해 잘 모르지만, Github에 대한 정보원이 있어요.도트 제품의 일부는 https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/arraytypes.c.src,에 구현되어 있으며, 각 데이터 유형에 대한 특정 C 구현으로 변환되어 있다고 가정합니다.예를 들어 다음과 같습니다.
/**begin repeat
*
* #name = BYTE, UBYTE, SHORT, USHORT, INT, UINT,
* LONG, ULONG, LONGLONG, ULONGLONG,
* FLOAT, DOUBLE, LONGDOUBLE,
* DATETIME, TIMEDELTA#
* #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint,
* npy_long, npy_ulong, npy_longlong, npy_ulonglong,
* npy_float, npy_double, npy_longdouble,
* npy_datetime, npy_timedelta#
* #out = npy_long, npy_ulong, npy_long, npy_ulong, npy_long, npy_ulong,
* npy_long, npy_ulong, npy_longlong, npy_ulonglong,
* npy_float, npy_double, npy_longdouble,
* npy_datetime, npy_timedelta#
*/
static void
@name@_dot(char *ip1, npy_intp is1, char *ip2, npy_intp is2, char *op, npy_intp n,
void *NPY_UNUSED(ignore))
{
@out@ tmp = (@out@)0;
npy_intp i;
for (i = 0; i < n; i++, ip1 += is1, ip2 += is2) {
tmp += (@out@)(*((@type@ *)ip1)) *
(@out@)(*((@type@ *)ip2));
}
*((@type@ *)op) = (@type@) tmp;
}
/**end repeat**/
이것은 벡터에 대한 1차원 점곱을 계산하는 것으로 보인다.몇 분 매트릭스의 수 Github에 대한 수 .FLOAT_dot
을 사용법, 이 는 가장 루프에 합니다.
이들 사이의 한 가지 차이점은 "스트라이드" 즉, 입력에서 연속되는 요소 간의 차이가 함수를 호출하기 전에 명시적으로 한 번 계산된다는 것입니다.이 경우 보폭은 없으며 각 입력의 오프셋이 매번 계산됩니다.a[i * n + k]
좋은 컴파일러가 Numpy stread와 비슷한 것을 최적화해 줄 것으로 기대했지만, 스텝이 일정하거나 최적화되어 있지 않다는 것을 증명할 수는 없습니다.
또한 Numpy는 이 함수를 호출하는 상위 수준의 코드로 캐시 효과를 사용하여 스마트한 작업을 수행하고 있을 수 있습니다.일반적인 방법은 각 행이 연속인지 또는 각 열인지를 고려하여 먼저 각 연속 부분에 대해 반복하는 것입니다.각 점곱에 대해 하나의 입력 행렬은 행으로 이동하고 다른 입력 행렬은 열로 이동해야 하므로 완벽하게 최적화하는 것은 어렵습니다(다른 큰 순서로 저장되지 않는 한).그러나 적어도 결과 요소에 대해서는 그렇게 할 수 있습니다.
Numpy에는, 「닷」을 포함한 특정의 조작의 실장을 다른 기본 실장으로부터 선택하는 코드도 포함되어 있습니다.예를 들어 BLAS 라이브러리를 사용할 수 있습니다.위에서 설명하면 CBLAS가 사용되고 있는 것 같습니다.이것은 Fortran에서 C로 변환되었습니다.테스트에 사용된 구현은 http://www.netlib.org/clapack/cblas/sdot.c에서 찾을 수 있는 구현이라고 생각합니다.
이 프로그램은 다른 기계에서 읽을 수 있도록 기계에서 작성되었습니다.그러나 아래쪽에 볼 수 있는 것은 롤되지 않은 루프를 사용하여 한 번에 5개의 요소를 처리하는 것입니다.
for (i = mp1; i <= *n; i += 5) {
stemp = stemp + SX(i) * SY(i) + SX(i + 1) * SY(i + 1) + SX(i + 2) *
SY(i + 2) + SX(i + 3) * SY(i + 3) + SX(i + 4) * SY(i + 4);
}
이 언롤 팩터는 여러 프로파일링 후에 선택되었을 가능성이 있습니다.그러나 이 방법의 이론적인 이점 중 하나는 각 분기점 간에 더 많은 산술 연산이 이루어지며 컴파일러와 CPU는 가능한 한 많은 명령 파이프라인링을 얻도록 최적의 스케줄 방법을 선택할 수 있다는 것입니다.
NumPy를 쓴 사람들은 분명히 그들이 무엇을 하고 있는지 알고 있다.
행렬 곱셈을 최적화하는 방법은 여러 가지가 있습니다.예를 들어 행렬을 통과하는 순서는 메모리 액세스 패턴에 영향을 미쳐 성능에 영향을 미칩니다.
SSE를 적절히 사용하는 것도 최적화의 또 다른 방법이며, NumPy는 이를 채택하고 있을 것입니다.
NumPy 개발자는 알고 있지만 저는 모르는 방법이 더 있을 수 있습니다.
그나저나, 당신의 C 코드를 옵티오마이제이션으로 컴파일했습니까?
C에 대해 다음과 같은 최적화를 시도할 수 있습니다.병렬로 동작합니다. NumPy 。
메모: 균일한 크기에서만 작동합니다.추가 작업을 통해 이 제한을 제거하고 성능 향상을 유지할 수 있습니다.
for (i = 0; i < n; i++) {
for (j = 0; j < n; j+=2) {
int sub1 = 0, sub2 = 0;
for (k = 0; k < n; k++) {
sub1 = sub1 + a[i * n + k] * b[k * n + j];
sub1 = sub1 + a[i * n + k] * b[k * n + j + 1];
}
c[i * n + j] = sub;
c[i * n + j + 1] = sub;
}
}
}
Numpy는 고도로 최적화된 코드이기도 합니다.아름다운 코드라는 책에 그것의 일부에 대한 에세이가 있다.
ctype은 C에서 Python으로 동적 변환을 거쳐 오버헤드를 추가해야 합니다.Numpy에서는 대부분의 매트릭스 연산이 Numpy 내부에서 완전히 수행됩니다.
숫자 코드에서 Fortran의 속도 우위 afaik의 가장 일반적인 이유는 언어가 에일리어싱을 검출하기 쉽게 하기 때문입니다.- 컴파일러는 곱셈되는 행렬이 같은 메모리를 공유하지 않기 때문에 캐시를 개선할 수 있습니다(결과를 "공유" 메모리에 즉시 다시 쓸 필요가 없습니다).이것이 C99가 제한을 도입한 이유입니다.
다만, 이 경우, Numpy 코드도 C 코드가 아닌 특별한 명령어를 사용하고 있지 않은가(특히 차이가 큰 것 같기 때문에).
특정 기능을 구현하기 위해 사용되는 언어는 그 자체로 성능을 측정하는 데 좋지 않습니다.대부분의 경우 보다 적합한 알고리즘을 사용하는 것이 결정적 요인입니다.
당신의 경우, 당신은 학교에서 가르친 것처럼 행렬 곱셈에 대한 순진한 접근법을 사용하고 있습니다. O(n^3)에서요.그러나 정사각형 행렬, 예비 행렬 등과 같은 특정 종류의 행렬에 대해 훨씬 더 나은 작업을 수행할 수 있습니다.
빠른 행렬 곱셈의 좋은 시작점을 위해 Coppersmith-Winograd 알고리즘(O(n^2.3737)의 제곱 행렬 곱셈)을 살펴 보십시오.더 빠른 메서드에 대한 몇 가지 포인터를 나열하는 "참조" 섹션도 참조하십시오.
향상에 대한 보다 , .strlen()
.glibc 합니다.glibc를 .strlen()
소스, 꽤 좋은 코멘트를 가지고 있습니다.
언급URL : https://stackoverflow.com/questions/10442365/why-is-matrix-multiplication-faster-with-numpy-than-with-ctypes-in-python
'sourcecode' 카테고리의 다른 글
C++: 1개의 피연산자를 레지스터에 유지하는 것이 불가사의할 정도로 고속화됨 (0) | 2022.08.29 |
---|---|
VueJ: 어레이가 아닌 옵서버 객체 (0) | 2022.08.29 |
c와 c++의 컨텍스트에서 static 변수, auto 변수, global 변수 및 local 변수의 차이 (0) | 2022.08.29 |
가장 작은 int인 -2147483648의 유형이 '롱'인 이유는 무엇입니까? (0) | 2022.08.29 |
GCC -g vs -g3 GDB 플래그:차이점은 무엇입니까? (0) | 2022.08.29 |