numpy.dot比本机C++11慢100倍

numpy.dot 100 times slower than native C++11

本文关键字:100倍 C++11 本机 dot numpy      更新时间:2023-10-16

我有Matlab背景,一年前我买了一台笔记本电脑,我仔细选择了一台计算能力很强的笔记本电脑,这台机器有4个线程,在2.4GHz下为我提供了8个线程。这台机器证明了自己非常强大,使用简单的parfor循环,我可以利用所有处理器线程,用它我可以在许多问题和实验中获得接近8的加速。

这个美好的星期天,我在试验numpy,人们经常告诉我,numpy的核心业务是使用libblas高效实现的,甚至可能使用OpenMP等多个核心和库(使用OpenMP,你可以使用c风格的pragma创建类似parfor的循环)。

这是许多数值和机器学习算法的通用方法,你可以使用昂贵的高级运算(如矩阵乘法)来表达它们,但为了方便起见,你可以用昂贵的高级语言(如Matlab和python)来表达。此外,c(++)允许我们绕过GIL。

所以最酷的部分是,无论何时使用numpy,线性代数的东西都应该在python中处理得非常快。你只是有一些函数调用的开销,但如果它背后的计算量很大,那就可以忽略不计了。

因此,我甚至没有触及不是所有东西都可以用线性代数或其他numpy运算来表达的话题,而是对其进行了旋转:

t = time.time(); numpy.dot(range(100000000), range(100000000)); print(time.time() - t)
40.37656021118164

所以,在这40秒里,我看到我的机器上的8个线程中有一个100%工作,其他线程接近0%。我不喜欢这样,但即使有一个线程在工作,我也希望它能在大约0.几秒内运行。点积做100M+和*,所以我们有2400M/100M=每秒24个时钟周期,用于一个+、一个*和任何开销。

然而,对于+、*和开销,算法需要40*24=大约=1000个刻度(!!!!)。让我们在C++中这样做:

#include<iostream>
int main() {
  unsigned long long result = 0;
  for(unsigned long long i=0; i < 100000000; i++)
    result += i * i;
  std::cout << result << 'n';
}

闪电战:

herbert@machine:~$ g++ -std=c++11 dot100M.cc 
herbert@machine:~$ time ./a.out
662921401752298880
real    0m0.254s
user    0m0.254s
sys 0m0.000s

0.254秒,几乎比numpy.dot.快100倍

我想,也许python3范围生成器是速度较慢的部分,所以我首先将所有100M数字存储在std::vector中(使用迭代push_back),然后再对其进行迭代,从而阻碍了我的c++11实现。这要慢得多,只需要不到4秒的时间,但速度仍然快了10倍。

我在ubuntu上使用"pip3-install-numpy"安装了我的numpy,它开始编译了一段时间,同时使用gcc和gfortran,此外,我看到有人提到blas头文件通过编译器输出。

为什么numpy.dot如此缓慢?

所以你的比较是不公平的。在python示例中,首先生成两个range对象,将它们转换为numpy数组,然后进行标量乘积。计算花费最少的部分。这是我电脑的号码:

>>> t=time.time();x=numpy.arange(100000000);numpy.dot(x,x);print time.time()-t
1.28280997276

并且没有生成阵列:

>>> t=time.time();numpy.dot(x,x);print time.time()-t
0.124325990677

为了完成,C版本需要大致相同的时间:

real    0m0.108s
user    0m0.100s
sys 0m0.007s

range根据给定的参数生成一个列表,其中C中的for循环只增加一个数字。

我同意花这么多时间生成一个列表在计算上似乎相当昂贵——再说一遍,这是一个大列表,而你正在请求其中的两个;-)

EDIT:如注释中所述,range生成列表,而不是数组。

尝试用递增的while循环或类似的方法替换range方法,看看是否得到了更可容忍的结果。