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

rmbxnbpk  于 11个月前  发布在  Python
关注(0)|答案(5)|浏览(108)

为什么这个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)

字符串
比下面的C++代码更快?

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

using namespace std;

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

int main()
{
    time_t start, stop;
    double diff;
    // table with data
    double data1[ N ];
    double data2[ N ];
    // table of results
    double coefs1[ k_max ];
    double coefs2[ k_max ];
    // main loop
    time( & start );
    for( unsigned int j = 1; j<N; j++ )
    {
        for( unsigned int i = 0; i<k_max; i++ )
        {
            coefs1[ i ] += data2[ j-1 ]*(cos((i+1)*data1[ j-1 ]) - cos((i+1)*data1[ j ]));
            coefs2[ i ] += data2[ j-1 ]*(sin((i+1)*data1[ j-1 ]) - sin((i+1)*data1[ j ]));
        }
    }
    // end of main loop
    time( & stop );
    // speed result
    diff = difftime( stop, start );
    cout << "Time: " << diff << " seconds";
    return 0;
}


第一个显示:“时间:8秒”,第二个显示:“时间:11秒”
我知道NumPy是用C写的,但我仍然认为C示例会更快。我错过了什么吗?有没有方法改进C代码(或Python代码)?

代码版本2

我已经按照其中一条评论中的建议修改了C代码(动态表改为静态表)。C代码现在更快了,但仍然比Python版本慢得多。

代码版本3

我已经从调试模式改为发布模式,并将'k'从4000增加到40000。现在NumPy只是稍微快了一点(8秒到11秒)。

p8h8hvxi

p8h8hvxi1#

我发现这个问题很有趣,因为每次我遇到关于NumPy速度的类似主题(与C/C相比),总是有这样的答案“它是一个薄的 Package 器,它的核心是用C写的,所以它很快”,但这并不能解释为什么C应该比有额外层(即使是薄层)的C慢。
答案是:**当正确编译时,你的C
代码不会比你的Python代码慢**。
我做了一些基准测试,起初似乎NumPy的速度快得惊人。但我忘记了用GCC优化编译。
我再次计算了所有内容,并将结果与您的代码的纯C版本进行了比较。(从源代码编译,使用相同的GCC)。为了编译C代码,我使用g++ -O3 main.cpp -o main,为了编译C代码,我使用gcc -O3 main.c -lm -o main。在所有示例中,我用一些数字填充data变量(0.1,0.4),因为它改变了结果。我还改变了 np.arrays 使用doubles(dtype=np.float64),因为在C示例中有doubles。我的纯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结果,其中:

  • Python 70.284362 s
  • C++ 69.133199 s
  • C 61.638186 s

Python和C的时间基本相同,但请注意,有一个长度为k_max的Python循环,它应该比C/C慢得多。
对于k_max = 1000000, N = 1000,我们有:

  • Python 115.42766 s
  • C++ 70.781380 s

对于k_max = 1000000, N = 100

  • Python 52.86826 s
  • C++ 7.050597 s

因此,差异随着分数k_max/N的增加而增加,但即使Nk_max大得多,python也不会更快,例如k_max = 100, N = 100000

  • Python 0.651587 s
  • C++ 0.568518 s

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

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


Python结果:

  • 电话:+86-21 - 66999999
  • 多:0.841291189194秒
  • 和:1.15971088409 s

C结果:

  • cos:20.910590 s
  • multi:0.633281 s
  • 和:1.153001 s

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

jaxagkaj

jaxagkaj2#

我实际上很惊讶,没有人提到线性代数库,如BLAS LAPACK MKL和所有.

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

我从ETH的CSE实验室拍摄了下面的图像,他们在那里比较了不同实现的矩阵向量乘法。y轴代表计算强度(GFLOPs);长话短说,它是计算完成的速度。x轴是矩阵的维度。
x1c 0d1x的数据
C和C是快速语言,但实际上,如果你想模仿这些库的速度,你可能必须更深入一步,使用Fortran或intrinsic指令(这可能是你在C中可以做的最接近汇编代码的指令)。
考虑问题Benchmarking (python vs. c++ using BLAS) and (numpy),其中来自@Jfs的非常好的答案,我们观察到:**“在我的机器上,C++和numpy之间没有区别。
更多参考:
Why is a naïve C++ matrix multiplication 100 times slower than BLAS?

8zzbczxx

8zzbczxx3#

在我的电脑上,你的(当前)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代码的一个非常薄的 Package 器,只有轻微的改进是我们应该期待的。
下一个明显的步骤是使用多个内核。为了在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个以上的内核比4个以下的内核更常见)。

pprl5pva

pprl5pva4#

我试着理解你的Python代码,并在C++中复制它。我发现你没有正确地表示for循环,以便正确地计算 coeffs,因此应该switchyourfor循环。如果是这种情况,你应该有以下内容:

#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循环给了我:3秒对于C++代码,使用 -O3 优化,而Python代码运行时间为7.816秒。

ux6nzvsh

ux6nzvsh5#

Python代码不可能比正确编码的C代码更快,因为Numpy是用C编写的,它的速度和C差不多。在某些情况下,C的性能优于C,因为它提供了C中不可能实现的优化。C和C之间的其他速度差异这取决于你使用的编译器和编译代码的性质。比较语言的速度是一个复杂的主题。
当有人使用NumPy时,他们通过编写Python代码来设置问题。与调优的C或C代码相比,Python代码的执行速度很慢。然而,由于大部分执行时间都发生在C编写的NumPy库中的数字上,因此在Python中设置问题的额外时间是无关紧要的。如果你完全用C/C编写同样的东西,你的总执行时间可能会快1%,因为无论你是用C/C还是Python编写的代码,C编写的库都会在执行请求的计算时占用大部分执行时间。出于这个原因,人们喜欢Python的细节,同时在重要的地方受益于C的速度。
举个例子,假设你设置了一个计算,你想把两个方阵相乘,每个方阵有100万个元素。Python代码执行得很慢,但总时间很短。然后,C库将执行巨大的矩阵乘法,占执行时间的大部分。
如果你将纯Python与调优后的C或C
进行比较,那么Python的速度可能会比C或C慢10到1000倍,这取决于你的程序正在执行什么任务。纯Python也会倾向于使用更多的内存。所以说你只用Python而不是使用NumPy编写了那个巨大的矩阵乘法。与使用C/C相比,执行时间会非常长或者使用NumPy。
看看the Benchmark Game,人们用各种语言提交各种算法的解决方案,网站跟踪每个算法的最快提交(算法,语言)对。你甚至可以查看每次提交的源代码。对于大多数测试用例,Python比C慢2-15倍。如果我没弄错的话,顶级Python解决方案最终调用编译的C或C库来完成大部分繁重的工作,所以这个测试并不是真的将纯Python与C进行比较。同样的事情发生在其他语言中,例如,顶级Java解决方案倾向于调用用C/C编写的代码在一些问题上。
C/C++比纯Python快的原因有很多。

  • Python是一种解释性语言,而C/C编译成机器代码。这一额外的解释步骤增加了执行时间。此外,代码在虚拟机上执行,这也必须占用一些CPU和RAM资源。C/C代码直接在硬件上执行。

  • Python有一个垃圾收集器,它必须运行一个算法来每隔一段时间分析程序的状态,以证明分配的内存块不再需要存在,只有这样程序才会释放该内存。C/C++不同。所有的分配和释放都是在机器代码中的某些点完成的,完全避免运行垃圾收集器算法。这将影响分配和释放大量内存的程序的性能,而不是不分配内存的程序。要了解为什么垃圾收集器如果一个对象的引用计数比较慢,那么每个对象都必须有一个引用计数。该算法会进行簿记,以了解一个对象仍然可以在哪里使用,以便在使用它的情况下保持它。此外,该算法必须处理循环,就像我有对象A,B和C,其中B持有对C的引用,C持有对B的引用,A持有对B的引用。如果程序中的所有内容都没有对A的引用,垃圾收集器必须弄清楚它可以为A、B和C释放内存。

  • 在Python中没有原语。所有的东西,包括最低的整数,都是一个完整的对象,有元数据和方法。在C/C中,一个int是一小块直接表示数字的1和0。传递和使用整数的这种基本表示形式要快得多,而不是处理包含这种表示形式的对象。假设你想检查两个整数是否equal。在C/C中,机器代码执行一个快速的单一汇编指令来比较位于这里和那里的两个整数。在Python中,调用Integer类的方法。这需要Python查找实际引用的对象的类型,查找该对象上的属性名称,检查该属性实际上是一个方法,编译这个方法如果它还没有被jit'd,如果jit没有被启用,那么程序开始逐行执行该方法的代码。这也与Python是一个动态的而C/C++有关。是静态的。动态的意思是在运行时查找,而静态的意思是在编译时计算,答案直接写入机器代码。

  • 所有东西都不仅仅是一个对象,而且是指向对象的指针。在C/C中,你可以有一个内存块直接容纳原语或对象,所以你可以在一个坚实的内存块中并排放置10个整数的位。在Python中,你在内存中并排放置10个指针,你有一个间接层来访问每个整数,跳到堆的随机部分来计算每个整数的值。这很重要,原因有两个:首先,间接寻址比没有间接寻址要慢。其次,大多数计算机体系结构使用空间局部性,这意味着当您访问内存的一部分(RAM)时,附近的内存块被加载到令人难以置信的快速CPU缓存中。我们正在谈论的是读取RAM比从1级缓存读取慢100倍。硬件的设计者这样做,因为通常,当您访问内存中的位置0时,接下来,你访问内存中的位置1,然后是2,然后是3。因此,在C/C中,像迭代n整数一样,你可以尽可能快地遍历n,而在Python中,你只会快速遍历n指针。然后,你将访问堆中保存整数的随机内存块,并且n整数甚至可以不在堆中彼此相邻地存储。

  • C/C++有许多特性更接近于汇编语言中的典型操作。这种接近实际硬件的特性使程序员能够编写出荒谬的调优代码。另一方面,Python几乎完全脱离了硬件,这意味着程序员即使想要速度也不能做这样的手动优化。这是一个很好的观点,可以说你不使用Python的速度。开发和希望更少的bug。

  • 在C++有更高级别的抽象的情况下,你会得到一大堆编译器应该实现这些抽象的方式的选项。如果需要的话,这种控制可以编写更快的代码。Python试图尽可能方便和简单地使用它的抽象。

  • 使用C/C++,编译器可以在生成机器码时执行高级分析和优化,因为在处理这些语言时,等待代码编译是程序员可以接受的时间成本。由于Python逐行解释代码,它根本无法执行类似的优化,因为“编译”时间会增加执行时间!如果你在Python解释器中引入大量的分析和优化,你的程序在开始执行调优的Python字节码之前会冻结几分钟或更长时间。

  • C/C++以速度的名义有大量不方便的未定义行为。如果你搞砸了,那是你的责任。程序将幸福地继续执行其机器代码,其输出将变得未定义。它可能很快就会崩溃,但它可以轻松地继续运行数天,将错误的结果写入数据库。Python不同。它更方便,做各种检查,一出现错误就报告。检查东西比不检查东西慢。

相关问题