为什么numpy求和float数组要比我的c / c ++代码快?

时间:2018-06-26 00:36:56

标签: python c numpy shared-libraries

我正在测试我的简单共享C库的效率,并将其与numpy实现进行比较。

库创建:在 sum_function.c 中定义了以下功能:

float sum_vector(float* data, int num_row){
    float value = 0.0;
    for (int i = 0; i < num_row; i++){
        value += data[i];
    }
    return value;
}

库编译:共享库sum.so由创建

clang -c sum_function.c
clang -shared -o sum.so sum_function.o

测量:使用上面的函数创建了一个简单的numpy数组,并计算了其元素的总和。

from ctypes import *
import numpy as np

N = int(1e7)
data = np.arange(N, dtype=np.float32)

libc = cdll.LoadLibrary("sum.so")
libc.sum_vector.restype = c_float
libc.sum_vector(data.ctypes.data_as(POINTER(c_float)),
                c_int(N))

以上功能需要30毫秒。但是,如果我使用numpy.sum,则执行时间仅为4毫秒。

所以我的问题是:是什么使numpy比我的C实现快得多?我不能考虑在计算矢量和的算法上有任何改进。

2 个答案:

答案 0 :(得分:2)

有许多可能涉及的原因,甚至取决于您使用的编译器。在许多情况下,您的numpy后端是C / C ++。换句话说,您必须意识到C ++之类的语言不仅可以提高效率并与硬件联系,还需要大量知识。只要您像@PaulMcKenzie的注释中那样使用STL,C ++就比C少。这些是针对运行时性能优化的例程。

接下来的事情是内存分配。现在,您的向量似乎足够大,以致<std::vector>中的分配器将对齐堆上的内存。堆栈上的内存可能最终无法对齐,从而使std::accumulate变慢。这是一个如何编写这种分配器以避免这种情况的想法:https://github.com/kvahed/codeare/blob/master/src/matrix/Allocator.hpp。这是我作为博士生写的MRI图像重建库的一部分。

SIMD上的一个词:与其他方面相同。 https://github.com/kvahed/codeare/blob/master/src/matrix/SIMDTraits.hpp如何进行最先进的算术运算并非易事。

以上两个概念最终都融合到https://github.com/kvahed/codeare/blob/master/src/matrix/Matrix.hpp中,您可以在其中轻松胜过特定计算机上的任何标准化代码。

最后但并非最不重要的一点:编译器和编译器标志。您的运行时代码一旦调试,就应该编译-O2 -g甚至是-O3。如果您具有良好的测试覆盖率,则您甚至可以摆脱-Ofast的困扰,即降低ieee的数学精度。除了数值积分外,我从未见过问题。

答案 1 :(得分:0)

您需要启用优化

此外,您还必须检查编译器是否能够使用自动矢量化。如果要分发已编译的二进制文件,则可能需要添加多个代码路径(AVX2,SS2),以在所有平台上获得可运行的高性能版本。

有关不同实现及其性能的简短概述。如果您在最新的处理器上无法击败numpy sum实现(通过pip安装的二进制版本),则说明您做错了什么,但请牢记变化的实现和依赖于编译器(快速)的精度。我懒得安装clang,但是使用了Numba,它也具有LLVM后端(与clang相同)。

import numba as nb
import numpy as np
import time

#prints information about SIMD vectorization
import llvmlite.binding as llvm
llvm.set_option('', '--debug-only=loop-vectorize')


@nb.njit(fastmath=True) #eq. O3, march-native,fastmath
def sum_nb(ar):
  s1=0. #double

  for i in range(ar.shape[0]):
    s1+=ar[i+0]

  return s1

N = int(1e7)
ar = np.random.rand(N).astype(np.float32)

#Numba solution float32 with float64 accumulator
#don't measure compilation time
sum_1=sum_nb(ar)
t1=time.time()
for i in range(1000):
  sum_1=sum_nb(ar)

print(time.time()-t1)

#Numba solution float64 with float64 accumulator
#don't measure compilation time
arr_64=ar.astype(np.float64)
sum_2=sum_nb(arr_64)
t1=time.time()
for i in range(1000):
  sum_2=sum_nb(arr_64)

print(time.time()-t1)

#Numpy solution (float32)
t1=time.time()
for i in range(1000):
  sum_3=np.sum(ar)

print(time.time()-t1)

#Numpy solution (float32, with float64 accumulator)
t1=time.time()
for i in range(1000):
  sum_4=np.sum(ar,dtype=np.float64)

print(time.time()-t1)

#Numpy solution (float64)
t1=time.time()
for i in range(1000):
  sum_5=np.sum(arr_64)

print(time.time()-t1)


print(sum_1)
print(sum_2)
print(sum_3)
print(sum_4)
print(sum_5)

性能

#Numba solution float32 with float64 accumulator: 2.29ms
#Numba solution float64 with float64 accumulator: 4.76ms
#Numpy solution (float32): 5.72ms
#Numpy solution (float32) with float64 accumulator:: 7.97ms
#Numpy solution (float64):: 10.61ms