为什么矩阵乘法的numpy比Python的ctypes快?

我试图找出最快的方式来做矩阵乘法,并尝试3种不同的方式:

>纯python实现:这里没有惊喜。
>使用numpy.dot(a,b)
在Python中使用ctypes模块与C接口。

这是转换成共享库的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,但源是在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**/

这似乎计算一维点积,即在向量上。在我的几分钟的Github浏览我无法找到矩阵的源,但它可能是使用一个调用FLOAT_dot为结果矩阵中的每个元素。这意味着此函数中的循环对应于您的最内层循环。

它们之间的一个区别是“stride” – 输入中连续元素之间的差异 – 在调用函数之前显式计算一次。在你的情况下,没有步幅,并且每次计算每个输入的偏移量,例如。 a [i * n k]。我会期望一个好的编译器优化到类似于Numpy步幅,但也许不能证明步骤是一个常量(或它没有被优化)。

Numpy也可以在调用此函数的高级代码中使用缓存效果做一些聪明的事情。一个常见的技巧是考虑每一行是连续的还是每一列 – 并尝试首先遍历每个连续的部分。似乎很难完全最优,对于每个点积,一个输入矩阵必须被行遍历,另一个输入矩阵必须被列遍历(除非它们恰好以不同的主顺序存储)。但它至少可以为结果元素。

Numpy还包含了从不同的基本实现中选择执行某些操作的代码,包括“dot”。例如,它可以使用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有更多的选择,如何最佳地调度它们以获得尽可能多的指令流水线。

http://stackoverflow.com/questions/10442365/why-is-matrix-multiplication-faster-with-numpy-than-with-ctypes-in-python

本站文章除注明转载外,均为本站原创或编译
转载请明显位置注明出处:为什么矩阵乘法的numpy比Python的ctypes快?