Python的numpy代码比eigen3或纯C++更高效

9
我有一些用Python3编写的代码(使用numpy),我想将其转换为C++(使用eigen3)以获得更高效的程序。因此,我决定测试一个简单的例子来评估我将获得的性能增益。该代码包括两个随机数组,这些数组将按系数相乘。我的结论是,带有numpy的python代码比C++中的代码快约30%。我想知道为什么解释的python代码比已编译的C++代码更快。在C++代码中,我是否漏掉了什么?
我正在使用gcc 9.1.0,Eigen 3.3.7,Python 3.7.3和Numpy 1.16.4。
可能的解释:
C++程序没有使用矢量化 Numpy比我想象的要优化得多 时间在每个程序中测量的内容不同
在Stack Overflow上有一个类似的问题(Eigen Matrix vs Numpy Array multiplication performance)。我在我的计算机上进行了测试,并得到了预期的结果,即eigen比numpy更有效率,但这里的操作是矩阵乘法,而不是按系数相乘。

Python代码(main.py)
执行命令:python3 main.py

import numpy as np
import time

Lx = 4096
Ly = 4000

# Filling arrays
a = np.random.rand(Lx, Ly).astype(np.float64)
a1 = np.random.rand(Lx, Ly).astype(np.float64)

# Coefficient-wise product
start = time.time()
b = a*a1

# Compute the elapsed time
end = time.time()

print(b.sum())
print("duration: ", end-start)

C++代码使用Eigen3 (main_eigen.cpp)
编译命令: g++ -O3 -I/usr/include/eigen3/ main_eigen.cpp -o prog_eigen

#include <iostream>
#include <chrono>
#include "Eigen/Dense"

#define Lx 4096
#define Ly 4000
typedef double T;

int main(){

    // Allocating arrays
    Eigen::Array<T, -1, -1> KPM_ghosts(Lx, Ly), KPM_ghosts1(Lx, Ly), b(Lx,Ly);

    // Filling the arrays
    KPM_ghosts.setRandom();
    KPM_ghosts1.setRandom();

    // Coefficient-wise product
    auto start = std::chrono::system_clock::now();
    b = KPM_ghosts*KPM_ghosts1;

    // Compute the elapsed time
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";

    // Print the sum so the compiler doesn't optimize the code away
    std::cout << b.sum() << "\n";

    return 0;
}

普通的 C++ 代码(main.cpp)
编译命令:g++ -O3 main.cpp -o prog

#include <iostream>
#include <chrono>

#define Lx 4096
#define Ly 4000
#define N Lx*Ly
typedef double T;

int main(){
    // Allocating arrays
    T lin_vector1[N];
    T lin_vector2[N];
    T lin_vector3[N];

    // Filling the arrays
    for(unsigned i = 0; i < N; i++){
        lin_vector1[i] = std::rand()*1.0/RAND_MAX;
        lin_vector2[i] = std::rand()*1.0/RAND_MAX;
    }

    // Coefficient-wise product
    auto start = std::chrono::system_clock::now();
    for(unsigned i = 0; i < N; i++)
        lin_vector3[i] = lin_vector1[i]*lin_vector2[i];

    // Compute the elapsed time
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";

    // Print the sum so the compiler doesn't optimize the code away
    double sum = 0;
    for(unsigned i = 0; i < N; i++)
        sum += lin_vector3[i];
    std::cout << "sum: " << sum << "\n";


    return 0;
}

每个程序的运行时间为10次

普通C++
经过时间:0.210664秒
经过时间:0.215406秒
经过时间:0.222483秒
经过时间:0.21526秒
经过时间:0.216346秒
经过时间:0.218951秒
经过时间:0.21587秒
经过时间:0.213639秒
经过时间:0.219399秒
经过时间:0.213403秒

带有eigen3的普通C++
经过时间:0.21052秒
经过时间:0.220779秒
经过时间:0.216269秒
经过时间:0.229234秒
经过时间:0.212265秒
经过时间:0.256714秒
经过时间:0.212396秒
经过时间:0.248241秒
经过时间:0.241537秒
经过时间:0.323519秒

Python
持续时间:0.23946428298950195秒
持续时间:0.1663036346435547秒
持续时间:0.17225909233093262秒
持续时间:0.15922021865844727秒
持续时间:0.16628384590148926秒
持续时间:0.15654635429382324秒
持续时间:0.15859222412109375秒
持续时间:0.1633443832397461秒
持续时间:0.1685199737548828秒
持续时间:0.16393446922302246秒


4
Python中进行复杂数学计算的库依赖于C语言层面来执行它们的操作。 - JacobIRR
1
虽然Python解释器在numpy函数调用中会将a*a1转换为一定程度的Python解释,但大部分操作都是在编译后的('C')numpy代码中进行的。对于像这样的基本数学运算,numpy多维数组的实现非常高效。 - hpaulj
8
我想转换到C++(使用eigen3),以便获得更高效的程序。但我不明白为什么人们总是说这样的话。只有在使用提供的低级功能时,较低级别的工具才会更快。 - Mansoor
2
@Mansoor 如果你知道如何非常好地向量化代码,这是大多数人,甚至经验丰富的C++开发人员所不知道的。编写高性能数值代码确实需要一定的技巧。 - tadman
1
@Sermal 正如其他人所提到的,你在Python和C++中做了大致相同的事情。你可以应用向量化并进行并行运算。然而,这些微调非常棘手,如果做得不正确,将无法获得性能增益(或者至少在多线程方面可能会使情况变得更糟)。FYI,还有很多方法可以让Python运行得更快。关键是C++给你很多控制权;大多数人不知道它们,有些人可能滥用它们,他们的代码不会比高级代码运行得更快。 - Mansoor
显示剩余10条评论
2个回答

0
我很难相信C++比Numpy慢。我一次又一次地需要执行快速计算,这些计算不是Numpy直接支持的(例如,在一次遍历中找到最小值和最大值,而不是调用.min和.max,这需要两倍的时间,或者依赖于大量数值计算的实时图像处理算法)。我通常使用Visual Studio C++编译器和Cython在Windows上创建自定义的C++扩展来编写代码。最终的结果总是与Numpy相当或更快。
Numpy可能在后台使用多线程(这里有很多细微之处),但根据我的经验,它通常不会这样做(至少在我的设置中是如此)。你可以通过在Cython中释放GIL(使用nogil:)并将输入分配给不同的线程来轻松测试这一点。这通常会导致几乎线性的扩展,直到线程数达到一定数量后,内存带宽成为瓶颈。显然,C++中没有GIL,所以在那里做这个应该更容易。
关于eigen3我无法发表评论,但正如其他人提到的,尝试调整构建参数和编译器标志(如果你不关心浮点精度,我实际上建议使用-Ofast,如果你关心,你可能不应该使用浮点数,但这是另外一个故事)。

0

我想在上面的评论中添加一些假设。

其中一个是numpy正在进行多线程处理。您的C++编译时使用了-O3,这通常已经可以获得很好的加速效果。我假设numpy没有使用-O3或其他默认PyPI软件包中的优化。但它明显更快。发生这种情况的一种方式是如果一开始速度较慢,但使用了多个CPU核心。

检查的一种方法是通过设置此处提到的变量使其仅使用一个线程 here

OMP_NUM_THREADS=1 MPI_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1

或者,同时也可能是由于优化的构建,例如您可以从Anaconda安装的MKL构建。正如上面的评论所建议的那样,您还可以看看使用SSE或AVX在C++代码中如何提高其性能,使用编译器标志,例如-march=native


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接