繁体   English   中英

为什么我的 Python NumPy 代码比 C++ 快?

[英]Why is my Python NumPy code faster than C++?

为什么这个 Python NumPy 代码,

import numpy as np
import time

k_max = 40000
N = 10000

data = np.zeros((2,N))
coefs = np.zeros((k_max,2),dtype=float)

t1 = time.time()
for k in xrange(1,k_max+1):
    cos_k = np.cos(k*data[0,:])
    sin_k = np.sin(k*data[0,:])
    coefs[k-1,0] = (data[1,-1]-data[1,0]) + np.sum(data[1,:-1]*(cos_k[:-1] - cos_k[1:]))
    coefs[k-1,1] = np.sum(data[1,:-1]*(sin_k[:-1] - sin_k[1:]))
t2 = time.time()

print('Time:')
print(t2-t1)

我发现这个问题很有趣,因为每次我遇到关于 NumPy 速度(与 C/C++ 相比)的类似话题时,总会有人回答“它是一个薄包装器,它的核心是用 C 编写的,所以它很快”,但是这个没有解释为什么 C 应该比带有附加层(即使是薄层)的 C 慢。

答案是:正确编译后,您的 C++ 代码并不比 Python 代码慢

我做了一些基准测试,起初看起来 NumPy 的速度快得惊人。 但我忘了用GCC优化编译。

我再次计算了所有内容,并将结果与​​您的代码的纯 C 版本进行了比较。 我正在使用 GCC 版本 4.9.2 和 Python 2.7.9(从具有相同 GCC 的源代码编译)。 为了编译你的 C++ 代码,我使用了g++ -O3 main.cpp -o main ,为了编译我的 C 代码,我使用了gcc -O3 main.c -lm -o main 在所有示例中,我用一些数字 (0.1, 0.4) 填充data变量,因为它会改变结果。 我还将np.arrays更改为使用双精度数( dtype=np.float64 ),因为在 C++ 示例中有双精度数。 我的纯 C 版本的代码(类似):

#include <math.h>
#include <stdio.h>
#include <time.h>

const int k_max = 100000;
const int N = 10000;

int main(void)
{
    clock_t t_start, t_end;
    double data1[N], data2[N], coefs1[k_max], coefs2[k_max], seconds;
    int z;
    for( z = 0; z < N; z++ )
    {
        data1[z] = 0.1;
        data2[z] = 0.4;
    }

    int i, j;
    t_start = clock();
    for( i = 0; i < k_max; i++ )
    {
        for( j = 0; j < N-1; j++ )
        {
            coefs1[i] += data2[j] * (cos((i+1) * data1[j]) - cos((i+1) * data1[j+1]));
            coefs2[i] += data2[j] * (sin((i+1) * data1[j]) - sin((i+1) * data1[j+1]));
        }
    }
    t_end = clock();

    seconds = (double)(t_end - t_start) / CLOCKS_PER_SEC;
    printf("Time: %f s\n", seconds);
    return coefs1[0];
}

对于k_max = 100000, N = 10000结果如下:

  • 蟒蛇 70.284362 秒
  • C++ 69.133199 秒
  • C 61.638186 秒

Python 和 C++ 的时间基本相同,但请注意,有一个长度为 k_max 的 Python 循环,与 C/C++ 相比,它应该慢得多。 确实如此。

对于k_max = 1000000, N = 1000我们有:

  • Python 115.42766 秒
  • C++ 70.781380 秒

对于k_max = 1000000, N = 100

  • 蟒蛇 52.86826 秒
  • C++ 7.050597 秒

所以差异随着分数k_max/N增加,但即使Nk_max ,python 也不会更快,例如k_max = 100, N = 100000

  • 蟒蛇 0.651587 秒
  • C++ 0.568518 秒

显然,C/C++ 和 Python 的主要速度差异在于for循环。 但我想找出 NumPy 和 C 中对数组的简单操作之间的区别。在代码中使用 NumPy 的优点包括:1. 将整个数组乘以一个数字,2. 计算整个数组的正弦/余弦, 3. 对数组的所有元素求和,而不是分别对每个项目进行这些操作。 所以我准备了两个脚本来只比较这些操作。

蟒蛇脚本:

import numpy as np
from time import time

N = 10000
x_len = 100000

def main():
    x = np.ones(x_len, dtype=np.float64) * 1.2345

    start = time()
    for i in xrange(N):
        y1 = np.cos(x, dtype=np.float64)
    end = time()
    print('cos: {} s'.format(end-start))

    start = time()
    for i in xrange(N):
        y2 = x * 7.9463
    end = time()
    print('multi: {} s'.format(end-start))

    start = time()
    for i in xrange(N):
        res = np.sum(x, dtype=np.float64)
    end = time()
    print('sum: {} s'.format(end-start))

    return y1, y2, res

if __name__ == '__main__':
    main()

# results
# cos: 22.7199969292 s
# multi: 0.841291189194 s
# sum: 1.15971088409 s

C脚本:

#include <math.h>
#include <stdio.h>
#include <time.h>

const int N = 10000;
const int x_len = 100000;

int main()
{
    clock_t t_start, t_end;
    double x[x_len], y1[x_len], y2[x_len], res, time;
    int i, j;
    for( i = 0; i < x_len; i++ )
    {
        x[i] = 1.2345;
    }

    t_start = clock();
    for( j = 0; j < N; j++ )
    {
        for( i = 0; i < x_len; i++ )
        {
            y1[i] = cos(x[i]);
        }
    }
    t_end = clock();
    time = (double)(t_end - t_start) / CLOCKS_PER_SEC;
    printf("cos: %f s\n", time);

    t_start = clock();
    for( j = 0; j < N; j++ )
    {
        for( i = 0; i < x_len; i++ )
        {
            y2[i] = x[i] * 7.9463;
        }
    }
    t_end = clock();
    time = (double)(t_end - t_start) / CLOCKS_PER_SEC;
    printf("multi: %f s\n", time);

    t_start = clock();
    for( j = 0; j < N; j++ )
    {
        res = 0.0;
        for( i = 0; i < x_len; i++ )
        {
            res += x[i];
        }
    }
    t_end = clock();
    time = (double)(t_end - t_start) / CLOCKS_PER_SEC;
    printf("sum: %f s\n", time);

    return y1[0], y2[0], res;
}

// results
// cos: 20.910590 s
// multi: 0.633281 s
// sum: 1.153001 s

蟒蛇结果:

  • cos: 22.7199969292 秒
  • 多:0.841291189194 s
  • 总和:1.15971088409 秒

C 结果:

  • cos:20.910590 秒
  • 多:0.633281秒
  • 总和:1.153001 秒

正如你所看到的 NumPy 非常快,但总是比纯 C 慢一点。

在我的计算机上,您的(当前)Python 代码运行时间为 14.82 秒(是的,我的计算机很慢)。

我将你的 C++ 代码重写为我认为合理的东西(基本上,我几乎忽略了你的 C++ 代码,只是将你的 Python 重写为 C++。这给了我这个:

#include <cstdio>
#include <iostream>
#include <cmath>
#include <chrono>
#include <vector>
#include <assert.h>

const unsigned int k_max = 40000;
const unsigned int N = 10000;

template <class T>
class matrix2 {
    std::vector<T> data;
    size_t cols;
    size_t rows;
public:
    matrix2(size_t y, size_t x) : cols(x), rows(y), data(x*y) {}
    T &operator()(size_t y, size_t x) {
        assert(x <= cols);
        assert(y <= rows);
        return data[y*cols + x];
    }

    T operator()(size_t y, size_t x) const {
        assert(x <= cols);
        assert(y <= rows);
        return data[y*cols + x];
    }
};

int main() {
    matrix2<double> data(N, 2);
    matrix2<double> coeffs(k_max, 2);

    using namespace std::chrono;

    auto start = high_resolution_clock::now();

    for (int k = 0; k < k_max; k++) {
        for (int j = 0; j < N - 1; j++) {
            coeffs(k, 0) += data(j, 1) * (cos((k + 1)*data(j, 0)) - cos((k + 1)*data(j+1, 0)));
            coeffs(k, 1) += data(j, 1) * (sin((k + 1)*data(j, 0)) - sin((k + 1)*data(j+1, 0)));
        }
    }

    auto end = high_resolution_clock::now();
    std::cout << duration_cast<milliseconds>(end - start).count() << " ms\n";
}

这运行了大约 14.4 秒,所以它比 Python 版本略有改进——但考虑到 Python 主要是一些 C 代码的非常薄的包装器,因此只获得轻微的改进几乎是我们应该期望的。

下一个明显的步骤是使用多个内核。 要在 C++ 中做到这一点,我们可以添加以下行:

#pragma omp parallel for

...在外部for循环之前:

#pragma omp parallel for
for (int k = 0; k < k_max; k++) {
    for (int j = 0; j < N - 1; j++) {
        coeffs(k, 0) += data(j, 1) * (cos((k + 1)*data(j, 0)) - cos((k + 1)*data(j+1, 0)));
        coeffs(k, 1) += data(j, 1) * (sin((k + 1)*data(j, 0)) - sin((k + 1)*data(j+1, 0)));
    }
}

-openmp添加到编译器的命令行(当然,确切的标志取决于您使用的编译器),这运行了大约 4.8 秒。 如果您有 4 个以上的内核,您可能会期待比这更大的改进(相反,如果您的内核少于 4 个,则预计会有较小的改进——但如今,4 个以上的内核更为常见)。

我试图理解你的 Python 代码并用 C++ 重现它。 我发现您没有正确表示 for-loops 以正确计算coeffs ,因此应该切换您的for-loops 如果是这种情况,您应该具备以下条件:

#include <iostream>
#include <cmath>
#include <time.h>

const int k_max = 40000;
const int N = 10000;

double cos_k, sin_k;

int main(int argc, char const *argv[])
{
    time_t start, stop;
    double data[2][N];
    double coefs[k_max][2];

    time(&start);

    for(int i=0; i<k_max; ++i)
    {
        for(int j=0; j<N; ++j)
        {
            coefs[i][0] += data[1][j-1] * (cos((i+1) * data[0][j-1]) - cos((i+1) * data[0][j]));
            coefs[i][1] += data[1][j-1] * (sin((i+1) * data[0][j-1]) - sin((i+1) * data[0][j]));
        }
    }
    // End of main loop

    time(&stop);

    // Speed result
    double diff = difftime(stop, start);
    std::cout << "Time: " << diff << " seconds" << std::endl;

    return 0;
}

切换 for 循环给了我: C++ 代码3 秒,使用-O3优化,而 Python 代码运行时间为 7.816 秒。

Python 代码不能比正确编码的 C++ 代码快,因为 Numpy 是用 C 编码的,这通常比 C++ 慢,因为 C++ 可以做更多的优化。 当在 Python 推送到编译的二进制文件进行计算的大型计算中进行大部分计算时,它们只会在 Python 与 C++ 的某个时间运行到大约两倍 C++ 之间的某个时间相互靠近。 除了大矩阵乘法、加法、矩阵乘法的标量等之外,大多数东西在 Python 中的表现都会差得多。 例如,看看基准游戏,人们用各种语言提交各种算法的解决方案,网站会跟踪每个(算法、语言)对的最快提交。 您甚至可以查看每次提交的源代码。 对于大多数测试用例,Python 比 C++ 慢 2-15 倍。 这是有道理的太多,如果你做的不是简单的数学运算其他任何东西-用链表,二叉搜索树,程序代码等的Python解释性质与它的元数据存储为每个对象(结合东西甚至intdoublefloat ,等.) 以一种任何 Python 程序员都无法修复的方式使事情陷入困境。

我真的很惊讶没有人提到像 BLAS LAPACK MKL 这样的线性代数库和所有......

Numpy 正在使用复杂的线性代数库! 从本质上讲,Numpy 大部分时间都不是建立在纯 c/cpp/fortran 代码上的……它实际上是建立在复杂的库上的,这些库利用了最高性能的算法和思想来优化代码。 这些复杂的库很难与经典线性代数计算的简单实现相匹配。 最简单的第一个改进示例是阻塞技巧。

我从 ETH 的 CSE 实验室拍摄了以下图片,他们在其中比较了不同实现的矩阵向量乘法。 y 轴代表计算强度(以 GFLOP 为单位); 长话短说,这是计算完成的速度。 x 轴是矩阵的维度。

在此处输入图像描述

C 和 C++ 是快速语言,但实际上如果您想模仿这些库的速度,您可能需要更深入地使用 Fortran 或内部指令(这可能是您可以在 C++ 中执行的最接近汇编代码的指令) .

考虑Benchmarking (python vs. c++ using BLAS) 和 (numpy)问题,@Jfs 给出了非常好的答案,我们观察到: “我的机器上的 C++ 和 numpy 没有区别。”

更多参考:

为什么天真的 C++ 矩阵乘法比 BLAS 慢 100 倍?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM