我正在测试我的简单共享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实现快得多?我不能考虑在计算矢量和的算法上有任何改进。
答案 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