基准测试(Python vs. C++使用BLAS和NumPy)

117

我想编写一个程序,广泛利用BLAS和LAPACK线性代数功能。由于性能是一个问题,我进行了一些基准测试,并想知道我采取的方法是否合法。

我有三个参赛者,想要通过一个简单的矩阵乘法来测试它们的性能。这三个参赛者分别是:

  1. Numpy,仅使用“点”功能。
  2. Python,通过共享对象调用BLAS功能。
  3. C++,通过共享对象调用BLAS功能。

场景

我为不同维度的矩阵乘法实现了代码,其中i从5到500,间隔为5,矩阵m1m2设置如下:

m1 = numpy.random.rand(i,i).astype(numpy.float32)
m2 = numpy.random.rand(i,i).astype(numpy.float32)

1. Numpy

所使用的代码如下:

tNumpy = timeit.Timer("numpy.dot(m1, m2)", "import numpy; from __main__ import m1, m2")
rNumpy.append((i, tNumpy.repeat(20, 1)))

2. Python,通过共享对象调用BLAS

使用该函数

_blaslib = ctypes.cdll.LoadLibrary("libblas.so")
def Mul(m1, m2, i, r):

    no_trans = c_char("n")
    n = c_int(i)
    one = c_float(1.0)
    zero = c_float(0.0)

    _blaslib.sgemm_(byref(no_trans), byref(no_trans), byref(n), byref(n), byref(n), 
            byref(one), m1.ctypes.data_as(ctypes.c_void_p), byref(n), 
            m2.ctypes.data_as(ctypes.c_void_p), byref(n), byref(zero), 
            r.ctypes.data_as(ctypes.c_void_p), byref(n))

测试代码如下:

r = numpy.zeros((i,i), numpy.float32)
tBlas = timeit.Timer("Mul(m1, m2, i, r)", "import numpy; from __main__ import i, m1, m2, r, Mul")
rBlas.append((i, tBlas.repeat(20, 1)))

3. c++,通过共享对象调用BLAS库

现在c++代码自然会比较长,所以我将信息减少到最少。
我使用以下代码加载函数:

void* handle = dlopen("libblas.so", RTLD_LAZY);
void* Func = dlsym(handle, "sgemm_");

我使用 gettimeofday 来测量时间,代码如下:

gettimeofday(&start, NULL);
f(&no_trans, &no_trans, &dim, &dim, &dim, &one, A, &dim, B, &dim, &zero, Return, &dim);
gettimeofday(&end, NULL);
dTimes[j] = CalcTime(start, end);

其中j是一个循环,运行20次。我使用下面的代码计算经过的时间:

double CalcTime(timeval start, timeval end)
{
double factor = 1000000;
return (((double)end.tv_sec) * factor + ((double)end.tv_usec) - (((double)start.tv_sec) * factor + ((double)start.tv_usec))) / factor;
}

结果

下图显示了结果:

enter image description here

问题

  1. 您认为我的方法是否公平,是否可以避免一些不必要的开销?
  2. 您是否预计结果会显示C++和Python方法之间如此巨大的差异?两者都使用共享对象进行计算。
  3. 由于我更愿意在我的程序中使用Python,我该怎么做才能提高调用BLAS或LAPACK例程的性能?

下载

完整的基准测试可以从这里下载。(J.F. Sebastian使该链接成为可能^^)


在您的ctypes方法中,您在测量函数内部进行了内存分配。您的c++代码是否遵循这种方法?但与矩阵乘法相比,这应该不会有太大的区别... - rocksportrocker
@rocksportrocker 您是正确的。r 矩阵的内存分配是不公平的。我正在解决这个“问题”,并将发布新的结果。 - Woltan
1
@Woltan:不要使用filefactory,这个服务太糟糕了。我已经将你的基准测试添加到github上:woltan-benchmark。如果你使用github,我可以将你添加为合作者。 - jfs
1
我运行了你的Python 3基准测试,并且得到了Python和C++之间类似的性能差异。虽然我知道numpy的性能取决于它链接的blas库,但我不明白为什么Python / BLAS和C++ / BLAS之间会有差异,因为它们都链接到同一个库。你是如何解决这个问题的? - D R
抱歉评论这篇旧文章。到目前为止,有没有解释为什么numpy-BLAS比C++慢至少一个数量级? - AlphaF20
显示剩余7条评论
5个回答

78

更新 (2014年7月30日):

我在我们的新HPC上重新运行了基准测试。硬件和软件栈都与原始答案中的设置不同。

我把结果放在了一个google电子表格中(也包含原来答案的结果)。

硬件

我们的HPC有两个不同的节点,一个使用Intel Sandy Bridge CPU,另一个使用更新的Ivy Bridge CPU:

Sandy (MKL、OpenBLAS、ATLAS):

  • CPU:2 x 16 Intel(R) Xeon(R) E2560 Sandy Bridge @ 2.00GHz (16核)
  • RAM:64 GB

Ivy (MKL、OpenBLAS、ATLAS):

  • CPU:2 x 20 Intel(R) Xeon(R) E2680 V2 Ivy Bridge @ 2.80GHz (20核,HT = 40核)
  • RAM:256 GB

软件

这个软件堆栈对于两个节点来说是相同的。除了GotoBLAS2,使用了OpenBLAS,还有一个设置为8线程(硬编码)的多线程ATLAS BLAS。

  • 操作系统: Suse
  • 英特尔编译器: ictce-5.3.0
  • Numpy: 1.8.0
  • OpenBLAS: 0.2.6
  • ATLAS:: 3.8.4

点积基准测试

基准测试代码与下面相同。但是对于新机器,我还运行了矩阵大小为50008000的基准测试。
下表包括原始答案的基准测试结果(重命名:MKL --> Nehalem MKL,Netlib Blas --> Nehalem Netlib BLAS等)

Matrix multiplication (sizes=[1000,2000,3000,5000,8000])

单线程性能: 单线程性能

多线程性能(8个线程): 多线程(8个线程)性能

线程数 vs 矩阵大小(Ivy Bridge MKL)矩阵大小 vs 线程数

基准测试套件

benchmark suite

单线程性能: enter image description here

多线程(8线程)性能: enter image description here

结论

新的基准测试结果与原答案中的相似。 OpenBLASMKL表现相同,除了特征值测试。 在单线程模式下,OpenBLAS仅表现得合理。 在多线程模式下,性能更差。

"矩阵大小与线程图表"也显示出MKL以及OpenBLAS通常随着核心数/线程数的增加而扩展,但这取决于矩阵的大小。对于小矩阵,添加更多核心不会显著提高性能。

Sandy BridgeIvy Bridge还有大约30%的性能提升,这可能是由于更高的时钟频率(+0.8 GHz)和/或更好的架构。


原始回答(04.10.2011):

前段时间,我需要优化一些使用numpy和BLAS编写的线性代数计算/算法,因此我对不同的numpy/BLAS配置进行了基准测试。

具体来说,我测试了:

  • Numpy与ATLAS
  • Numpy与GotoBlas2(1.13)
  • Numpy与MKL(11.1/073)
  • Numpy与加速框架(Mac OS X)

我运行了两个不同的基准测试:

  1. 不同大小矩阵的简单点积
  2. 可以在此处找到的基准测试套件。

这是我的结果:

机器

Linux(MKL、ATLAS、无MKL、GotoBlas2):

  • 操作系统: Ubuntu Lucid 10.4 64位。
  • CPU: 2 x 4 Intel(R) Xeon(R) E5504 @ 2.00GHz (8核心)
  • 内存: 24 GB
  • Intel编译器: 11.1/073
  • Scipy: 0.8
  • Numpy: 1.5

Mac Book Pro (Accelerate Framework):

  • 操作系统: Mac OS X Snow Leopard (10.6)
  • CPU: 1 Intel Core 2 Duo 2.93 Ghz (2核心)
  • 内存: 4 GB
  • Scipy: 0.7
  • Numpy: 1.3

Mac Server (Accelerate Framework):

  • 操作系统: Mac OS X Snow Leopard Server (10.6)
  • CPU: 4 X Intel(R) Xeon(R) E5520 @ 2.26 Ghz (8核心)
  • 内存: 4 GB
  • Scipy: 0.8
  • Numpy: 1.5.1

点积基准测试

代码:

import numpy as np
a = np.random.random_sample((size,size))
b = np.random.random_sample((size,size))
%timeit np.dot(a,b)

结果:

    系统           |  大小 = 1000  | 大小 = 2000 | 大小 = 3000 |
netlib BLAS       |  1350 毫秒    |   10900 毫秒|  39200 毫秒 |    
ATLAS (1 CPU)     |   314 毫秒    |    2560 毫秒|   8700 毫秒 |     
MKL (1 CPUs)      |   268 毫秒    |    2110 毫秒|   7120 毫秒 |
MKL (2 CPUs)      |    -          |       -     |   3660 毫秒 |
MKL (8 CPUs)      |    39 毫秒    |     319 毫秒|   1000 毫秒 |
GotoBlas2 (1 CPU) |   266 毫秒    |    2100 毫秒|   7280 毫秒 |
GotoBlas2 (2 CPUs)|   139 毫秒    |    1009 毫秒|   3690 毫秒 |
GotoBlas2 (8 CPUs)|    54 毫秒    |     389 毫秒|   1250 毫秒 |
Mac OS X (1 CPU)  |   143 毫秒    |    1060 毫秒|   3605 毫秒 |
Mac Server (1 CPU)|    92 毫秒    |     714 毫秒|   2130 毫秒 |

Dot product benchmark - chart

基准测试套件

代码:
有关基准测试套件的其他信息,请参见 此处

结果:

    系统              | 特征值分解时间 | SVD时间  | 行列式时间 | 求逆时间 | 点乘时间 |
netlib BLAS         |  1688毫秒      | 13102毫秒 | 438毫秒     | 2155毫秒 | 3522毫秒 |
ATLAS (1 CPU)       |   1210毫秒     |  5897毫秒 | 170毫秒     |  560毫秒 |  893毫秒 |
MKL (1 CPUs)        |   691毫秒      |  4475毫秒 | 141毫秒     |  450毫秒 |  736毫秒 |
MKL (2 CPUs)        |   552毫秒      |  2718毫秒 |  96毫秒     |  267毫秒 |  423毫秒 |
MKL (8 CPUs)        |   525毫秒      |  1679毫秒 |  60毫秒     |  137毫秒 |  197毫秒 |  
GotoBlas2 (1 CPU)   |  2124毫秒      |  4636毫秒 | 147毫秒     |  456毫秒 |  743毫秒 |
GotoBlas2 (2 CPUs)  |  1560毫秒      |  3278毫秒 | 116毫秒     |  295毫秒 |  460毫秒 |
GotoBlas2 (8 CPUs)  |   741毫秒      |  2914毫秒 |  82毫秒     |  262毫秒 |  192毫秒 |
Mac OS X (1 CPU)    |   948毫秒      |  4339毫秒 | 151毫秒     |  318毫秒 |  566毫秒 |
Mac Server (1 CPU)  |  1033毫秒      |  3645毫秒 |  99毫秒     |  232毫秒 |  342毫秒 |

Benchmark suite - chart

安装

安装MKL需要安装完整的Intel编译器套件,非常简单。但是由于一些错误/问题,配置和编译支持MKL的numpy有些麻烦。

GotoBlas2是一个小型软件包,可以轻松编译为共享库。但是由于一个bug,您必须在构建后重新创建共享库才能将其与numpy一起使用。
此外,对于多个目标平台进行构建出现了问题。因此,我必须为每个平台创建一个.so文件,以便拥有优化的libgoto2.so文件。

如果从Ubuntu的存储库安装numpy,则会自动安装并配置numpy以使用ATLAS。从源代码安装ATLAS可能需要一些时间,并且需要一些额外的步骤(fortran等)。

如果在带有FinkMac Ports的Mac OS X机器上安装numpy,则会将numpy配置为使用ATLASApple的Accelerate Framework。您可以通过运行numpy.core._dotblas文件上的ldd或调用numpy.show_config()来检查。

结论

MKL表现最佳,紧随其后的是GotoBlas2。 在特征值测试中,GotoBlas2的表现比预期要差得多,不确定原因为何。 苹果的Accelerate Framework的表现非常好,特别是在单线程模式下(与其他BLAS实现相比)。

GotoBlas2MKL都能很好地随着线程数量进行扩展。因此,如果您需要处理大矩阵,则在多个线程上运行它会有很大帮助。

无论如何,不要使用默认的netlib blas实现,因为这对于任何严肃的计算工作来说速度太慢了。

在我们的集群上,我还安装了AMD的ACML,性能类似于MKLGotoBlas2。但我没有任何数字。

我个人建议使用GotoBlas2,因为它更容易安装并且是免费的。

如果你想用C++/C编写代码,请也看看Eigen3,它在某些cases中被认为比MKL/GotoBlas2表现更好,并且使用起来也非常容易。


非常全面,谢谢!我想知道三年后,OpenBLAS(据我所知,它是GotoBLAS的后代)是否会表现更好。我曾经在某个地方读到过它比MKL表现更好,但现在找不到来源了。 - user2321808
谢谢!这是我的印象(我在想这是否只是我的安装问题):当对角化矩阵时(我使用与OpenBLAS链接的scipy进行对角化),OpenBLAS在多线程模式下表现不佳。 - user2321808
@William:通常情况下,你不需要专门将scipy链接到openblas,因为它会在安装过程中使用numpy配置,并且实际上大多数BLAS/Lapack调用都将被转发到numpy。因此,如果numpy已经正确地链接到openblas,一切都应该正常工作。 - Ümit
@Ümit:谢谢!我现在正尝试设置numpy以链接到MKL。 - user2321808
2
抱歉,我有点困惑。原始基准测试似乎是将C++性能与Numpy进行比较,但这仅适用于具有不同linalg库和架构的Numpy。我错过了什么吗? - eriophora

63

我已经运行了你的基准测试。在我的机器上,C++和numpy没有任何区别:

woltan's benchmark

你认为我的方法公平吗?还是有些不必要的开销可以避免?

由于结果没有什么差异,所以看起来很公平。

你是否预计结果会显示C++和Python方法之间有如此大的差距?两者都使用共享对象进行计算。

不会。

既然我更愿意使用Python编写我的程序,那么在调用BLAS或LAPACK例程时,我该怎么做才能提高性能?

确保numpy在您的系统上使用优化版本的BLAS/LAPACK库。


6
原贴的错误在哪里?我希望他在这篇文章中发表评论。他确认Numpy和C++的速度一样快吗? - wmac
1
你的 C++ 代码运行速度比原帖慢。你是否进行了优化编译? - cdcdcd
@cdcdcd 这不是我的代码。点击链接并使用不同的优化选项自己运行基准测试(参见 Makefile)。虽然代码不重新编译blas和lapack。 - jfs

20

这里有另一个基准测试(在Linux上,只需键入make):http://dl.dropbox.com/u/5453551/blas_call_benchmark.zip

http://dl.dropbox.com/u/5453551/blas_call_benchmark.png

我认为在处理大矩阵时,Numpy、Ctypes和Fortran之间的方法基本上没有什么区别。(如果这很重要的话,使用Fortran而不是C++,你的基准测试可能会出问题。)

在C++中,您的CalcTime函数似乎存在符号错误。... + ((double)start.tv_usec))应该改为... - ((double)start.tv_usec))。也许您的基准测试还有其他错误,例如在不同的BLAS库之间进行比较,或者在不同的BLAS设置(如线程数)之间进行比较,或者在实时时间和CPU时间之间进行比较?

作为指南:如果您进行基准测试,请始终在某个地方发布所有代码。在没有完整代码的情况下评论基准测试,特别是当结果令人惊讶时,通常是无效的。

要查看Numpy链接的BLAS版本,请执行以下操作:

$ python
Python 2.7.2+ (default, Aug 16 2011, 07:24:41) 
[GCC 4.6.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy.core._dotblas
>>> numpy.core._dotblas.__file__
'/usr/lib/pymodules/python2.7/numpy/core/_dotblas.so'
>>> 
$ ldd /usr/lib/pymodules/python2.7/numpy/core/_dotblas.so
    linux-vdso.so.1 =>  (0x00007fff5ebff000)
    libblas.so.3gf => /usr/lib/libblas.so.3gf (0x00007fbe618b3000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbe61514000)

更新:如果您无法导入numpy.core._dotblas,则说明您的Numpy正在使用其内部备用复制品,这会导致速度变慢,并且不适合用于性能计算!@Woltan下面的回复表明,这是他/她在Numpy vs. Ctypes+BLAS中看到差异的解释。

为了解决这个问题,你需要安装ATLAS或MKL --- 请参考以下指南: http://scipy.org/Installing_SciPy/Linux 大多数Linux发行版都预装了ATLAS,因此最好的选择是安装他们的libatlas-dev软件包(名称可能有所不同)。

我已经运行了你的基准测试;结果是相同的。链接 - jfs
2
@Woltan:你无法导入numpy.core._dotblas的事实意味着你的Numpy正在使用其内部备用的BLAS副本(速度较慢,不适合用于性能计算!),而不是你系统上的BLAS库。这解释了你从基准测试中得到的结果。要解决这个问题,你需要安装一个Numpy可以使用的BLAS版本---也就是ATLAS或MKL。以下是一组说明:http://scipy.org/Installing_SciPy/Linux - pv.
@pv.: 你能否运行Woltan基准测试来比较结果。 - jfs
你的C++性能令人困惑。我无法复现它,也似乎没有其他人成功过。100倍的差异似乎不可能,所以我建议你仔细检查你的C++代码:矩阵大小是否相同,调用是否产生相同的结果等。同时也要对比Fortran。 - pv.
1
在 Mac 上,您可以使用“otool -L”代替 Linux 上的“ldd”。 - RichVel
显示剩余3条评论

9
鉴于您在分析方面的严谨态度,我对迄今为止的结果感到惊讶。我将其作为“答案”发布,但仅仅是因为它太长了,不适合作为评论,并提供了一种可能性(尽管我认为您已经考虑过这一点)。
我本以为对于一个相对复杂的矩阵,numpy/python方法不会增加太多开销,因为随着复杂度的增加,python参与的比例应该很小。我更关心图表右侧的结果,但是显示出的数量级差异令人不安。
我想知道您是否正在使用numpy可以利用的最佳算法。来自Linux编译指南:

"构建 FFTW (3.1.2): SciPy 版本 >= 0.7 和 Numpy 版本 >= 1.2: 因为许可证、配置和维护问题,支持 FFTW 的功能在 SciPy 版本 >= 0.7 和 NumPy 版本 >= 1.2 中已被删除。现在使用内置的 fftpack 版本。 如果必要,有几种方法可以利用 FFTW 的速度进行分析。 降级到包含支持的 Numpy/Scipy 版本。 安装或创建自己的 FFTW 包装器。参见 http://developer.berlios.de/projects/pyfftw/ 作为一个未经认可的示例。"

你是否使用mkl编译了numpy?(http://software.intel.com/en-us/articles/intel-mkl/) 如果你正在linux上运行,编译numpy与mkl的指南在这里: http://www.scipy.org/Installing_SciPy/Linux#head-7ce43956a69ec51c6f2cedd894a4715d5bfff974 (尽管网址有误)。关键部分是:

[mkl]
library_dirs = /opt/intel/composer_xe_2011_sp1.6.233/mkl/lib/intel64
include_dirs = /opt/intel/composer_xe_2011_sp1.6.233/mkl/include
mkl_libs = mkl_intel_lp64,mkl_intel_thread,mkl_core 

如果您使用的是Windows操作系统,您可以在http://www.lfd.uci.edu/~gohlke/pythonlibs/上获取已编译的二进制文件,并获得MKL、pyfftw以及其他相关算法。感谢加州大学欧文分校荧光动力学实验室的Christoph Gohlke提供帮助。需要注意的是,在任何情况下,都有许多许可问题等需要注意,但英特尔页面会解释这些问题。同样,我想您已经考虑过这个问题,但如果您满足许可要求(在Linux上非常容易做到),相对于使用简单的自动构建甚至没有FFTW,这将大大加速numpy部分。我很乐意关注这个话题并看看其他人的想法。无论如何,非常严谨和出色的问题。感谢您的发布。

谢谢你详尽的“评论”^^。为了澄清我的python/numpy/BLAS设置:我遵循了这个安装指南。我使用的是Linux操作系统,版本如下: Python 2.7,Scipy 0.9 Numpy 1.6。不幸的是,我事先没有构建FFTW,也没有使用mkl... - Woltan
某种程度上,这是幸运的。这意味着在Python结果方面有巨大的改进空间,而且听起来你想使用Python。我认为如果你将构建修改为链接中显示的构建方式,你会对NumPy的速度感到更满意,尽管我仍然很想看看它与你的C++实现相比如何。 - Profane
你也可以尝试构建ATLAS,但考虑到我的性能需求,这听起来太麻烦了,所以我没有任何经验。我想,如果你有兴趣使用Python但也能使用C ++,那么在进行大量特殊编译的设置成本超过语言节省的某个时刻,使用C++可能会更容易一些。但是MKL和FFTW都应该相当简单。 - Profane
1
目前MKL、Accelerate和OpenBLAS在性能方面类似。然而,相比于MKL,OpenBLAS更具可扩展性。 - Sturla Molden

0

让我来贡献一些奇怪的发现result

我的numpymkl链接,如numpy.show_config()所示。我不知道使用了什么类型的libblas.so C++/BLAS。希望有人能告诉我找出它的方法。

我认为结果强烈依赖于所使用的库。我无法分离C++/BLAS的效率。


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