numpy比Eigen C++更快更高效吗?

lokaqttq  于 2023-01-09  发布在  其他
关注(0)|答案(2)|浏览(732)

最近,我和一位同事就python和C的性能比较进行了辩论。我们两个都在使用这两种语言进行线性代数运算。所以我写了两个脚本,一个用python3,使用numpy,另一个用C,使用Eigen。
python3数字版本matmul_numpy. py:

import numpy as np
import time
a=np.random.rand(2000,2000)
b=np.random.rand(2000,2000)
start=time.time()
c=a*b
end=time.time()
print(end-start)

如果我运行这个脚本

python3 matmul_numpy.py

这将返回:

0.07 seconds

C++特征值版本matmul_eigen. cpp:

#include <iostream>
#include <Eigen/Dense>
#include "time.h"
int main(){
        clock_t start,end;
        size_t n=2000;
        Eigen::MatrixXd a=Eigen::MatrixXd::Random(n,n);
        Eigen::MatrixXd b=Eigen::MatrixXd::Random(n,n);
        start=clock();
        Eigen::MatrixXd c=a*b;
        end=clock();
        std::cout<<(double)(end-start)/CLOCKS_PER_SEC<<std::endl;
        return 0;}

我编译它的方法是,

g++ matmul_eigen.cpp -I/usr/include/eigen3 -O3 -march=native -std=c++17 -o matmul_eigen

这将返回(c11和c17):

0.35 seconds

这对我来说很奇怪,1-为什么numpy在这里比c快?我错过了任何其他的优化标志吗?
我想也许是因为python解释器的缘故,它在这里执行程序的速度更快了,所以我在stacks中使用这个线程用cython编译了代码。
编译后的Python脚本仍然更快(0. 11秒)。这再次给我增加了两个问题:
2-为什么它变长了?解释器做了更多的优化吗?
3-为什么python脚本的二进制文件(37kb)比c
(57kb)小?
我很感激你的帮助,
谢谢

9gm1akwq

9gm1akwq1#

最大的问题是您正在比较两个完全不同的东西

  • 在Numpy中,a*b执行元素乘法,因为ab是2D数组,不被视为矩阵。a@b执行矩阵乘法。
  • 在Eigen中,a*b执行矩阵乘法,而不是元素乘法(参见文档),这是因为ab是矩阵,而不仅仅是二维数组。

两者给出了完全不同的结果。此外,矩阵乘法在O(n**3)时间内运行,而元素乘法在O(n**2)时间内运行。矩阵乘法内核通常是高度优化和计算受限的。它们通常被大多数BLAS库并行化。元素乘法是内存受限的(特别是这里由于 * 页面错误 *)。结果,这并不奇怪,矩阵乘法比元素式乘法慢,并且由于后者是受存储器限制的,差距也不是很大。
在我的i5- 9600 KF处理器(6核)上,Numpy执行a*b(顺序)需要9毫秒,执行a@b(并行,使用OpenBLAS)需要65毫秒。
注意像这样的Numpy元素乘法不是并行的(至少在Numpy的标准默认实现中不是)。Numpy的矩阵乘法使用BLAS库,默认情况下通常是OpenBLAS(这取决于实际的目标平台)。Eigen也应该使用BLAS库,但它可能与Numpy的BLAS库不同。
另请注意,clock不是测量并行代码的好方法,因为它 * 不 * 测量挂钟时间,而是CPU时间(有关更多信息,请参见this post)。std::chrono::steady_clock通常是C中更好的选择。
3-为什么python脚本的二进制文件(37 kb)比c
(57 kb)小?
Python通常被编译成不是本机汇编代码的字节码。C++通常被编译成包含汇编二进制代码的可执行程序,用于运行程序的附加信息以及元信息。字节码通常非常紧凑,因为它们是高级的。本机编译器可以执行优化,使程序更大,例如循环展开和内联。CPython(默认的Python解释器)不会进行这样的优化,事实上,CPython不会对字节码进行(高级的)优化,请注意,您可以使用-Os-s这样的标志来告诉GCC这样的本地编译器生成更小的代码(尽管通常更慢)。

ljsrvy3e

ljsrvy3e2#

所以根据我从@Jérôme Richard的回复和评论@user17732522中了解到的情况,我似乎在对比中犯了两个错误,
1-我在python脚本中定义乘法时犯了一个错误,它应该是np.matmul(a,b)或np.dot(a,b)或a@b。而不是a*b,这是一个元素级乘法。2-我没有正确测量C代码中的时间。clock_t不适合此计算,std::chrono::steady_clock工作得更好。
加上这些注解,c
的特征码比python的快10倍。
matmul_eigen.cpp的更新代码:

#include <iostream>
#include <Eigen/Dense>
#include <chrono>
int main(){

    size_t n=2000;
    Eigen::MatrixXd a=Eigen::MatrixXd::Random(n,n);
    Eigen::MatrixXd b=Eigen::MatrixXd::Random(n,n);
    auto t1=std::chrono::steady_clock::now();
    Eigen::MatrixXd c=a*b;
    auto t2=std::chrono::steady_clock::now();
    std::cout<<(double)std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count()/1000000.0f<<std::endl;

    return 0;}

要进行编译,应同时考虑矢量化和多线程标志。

g++ matmul_eigen.cpp -I/usr/include/eigen3 -O3 -std=c++17 -march=native -fopenmp -o eigen_matmul

要使用多线程运行代码:

OMP_NUM_THREADS=4 ./eigen_matmul

其中“4”是openmp可以使用的CPU数量,您可以使用以下命令查看您拥有的CPU数量:

lscpu | grep "CPU(s):"

这将返回0.104秒。
更新后的python脚本matmul_numpy.py:

import numpy as np
import time
a=np.random.rand(2000,2000)
b=np.random.rand(2000,2000)

a=np.array(a, dtype=np.float64)
b=np.array(b, dtype=np.float64)

start=time.time()
c=np.dot(a,b)
end=time.time()
print(end-start)

为了运行代码,

python3 matmul_numpy.py

这将返回1.0531秒。
关于它是这样的原因,我认为@Jérôme Richard的回应是一个更好的参考。

相关问题