列方式总和V行明智总和:为什么我没有看到使用NumPy的差异?

时间:2014-07-14 12:48:33

标签: python performance numpy

我已经使用numpy(第20/57页)测试了此talk [pytables]中演示的示例。

据说,a[:,1].sum()需要9.3毫秒,而a[1,:].sum()只需要72微秒。

我试图重现它,但未能这样做。我错误地测量了吗?或者自2010年以来NumPy发生了变化?

$ python2 -m timeit -n1000 --setup \ 
  'import numpy as np; a = np.random.randn(4000,4000);' 'a[:,1].sum()' 
1000 loops, best of 3: 16.5 usec per loop

$ python2 -m timeit -n1000 --setup \ 
  'import numpy as np; a = np.random.randn(4000,4000);' 'a[1,:].sum()' 
1000 loops, best of 3: 13.8 usec per loop

$ python2 --version
Python 2.7.7
$ python2 -c 'import numpy; print numpy.version.version'
1.8.1

虽然我可以测量第二个版本的好处(假设缓存未命中因为numpy使用C风格的行排序),但我没有看到pytables贡献者所说的那么大的差异。

此外,在使用列V行求和时,似乎看不到更多缓存未命中。


修改

  • 到目前为止,我的洞察力是我以错误的方式使用timeit模块。使用相同数组(或数组的行/列)的重复运行几乎肯定会被缓存(我已经获得了32KiB的L1数据缓存,因此一条线很适合内部:4000 * 4 byte = 15k < 32k

  • 使用@alim的answer中的脚本使用单个循环(nloop=1)和10次试验nrep=10,并改变随机数组的大小({{ 1}})我正在测量

    n x n

    * n row/us col/us penalty col 1k 90 100 1 4k 100 210 2 10k* 110 350 3.5 20k* 120 1200 10 及更高版本不再适合L1d缓存。

由于n=10k显示更快的行总和的缓存未命中率(有时甚至更高的速率),我仍然不确定如何追查原因。

perf数据:

Perfnloop = 2,所以我希望有些数据仍在缓存中...第二次运行。

行总和nrep=2

n=10k

列总和 perf stat -B -e cache-references,cache-misses,L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores,L1-dcache-store-misses,L1-dcache-prefetches,cycles,instructions,branches,faults,migrations ./answer1.py 2>&1 | sed 's/^/ /g' row sum: 103.593 us Performance counter stats for './answer1.py': 25850670 cache-references [30.04%] 1321945 cache-misses # 5.114 % of all cache refs [20.04%] 5706371393 L1-dcache-loads [20.00%] 11733777 L1-dcache-load-misses # 0.21% of all L1-dcache hits [19.97%] 2401264190 L1-dcache-stores [20.04%] 131964213 L1-dcache-store-misses [20.03%] 2007640 L1-dcache-prefetches [20.04%] 21894150686 cycles [20.02%] 24582770606 instructions # 1.12 insns per cycle [30.06%] 3534308182 branches [30.01%] 3767 faults 6 migrations 7.331092823 seconds time elapsed

n=10k

EDIT2 我想我已经了解了一些方面,但我认为这个问题尚未得到解答。目前我认为这个总结示例根本没有透露任何有关CPU缓存的信息。为了消除numpy / python的不确定性,我尝试使用 perf stat -B -e cache-references,cache-misses,L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores,L1-dcache-store-misses,L1-dcache-prefetches,cycles,instructions,branches,faults,migrations ./answer1.py 2>&1 | sed 's/^/ /g' column sum: 377.059 us Performance counter stats for './answer1.py': 26673628 cache-references [30.02%] 1409989 cache-misses # 5.286 % of all cache refs [20.07%] 5676222625 L1-dcache-loads [20.06%] 11050999 L1-dcache-load-misses # 0.19% of all L1-dcache hits [19.99%] 2405281776 L1-dcache-stores [20.01%] 126425747 L1-dcache-store-misses [20.02%] 2128076 L1-dcache-prefetches [20.04%] 21876671763 cycles [20.00%] 24607897857 instructions # 1.12 insns per cycle [30.00%] 3536753654 branches [29.98%] 3763 faults 9 migrations 7.327833360 seconds time elapsed 来进行 C 中的求和,结果在下面的答案中。

4 个答案:

答案 0 :(得分:4)

我认为您的复制尝试没有任何问题,但请记住,这些幻灯片是从2010年开始的,从那时起numpy发生了相当大的变化。基于dates of numpy releases,我猜想Francesc可能正在使用v1.5。

使用此脚本对行v列求和进行基准测试:

#!python

import numpy as np
import timeit

print "numpy version == " + str(np.__version__)

setup = "import numpy as np; a = np.random.randn(4000, 4000)"

rsum = "a[1, :].sum()"
csum = "a[:, 1].sum()"

nloop = 1000
nrep = 3

print "row sum:\t%.3f us" % (
    min(timeit.repeat(rsum, setup, repeat=nrep, number=nloop)) / nloop * 1E6)
print "column sum:\t%.3f us" % (
    min(timeit.repeat(csum, setup, repeat=nrep, number=nloop)) / nloop * 1E6)

我通过numpy v1.5检测到列总和减少了50%:

$ python sum_benchmark.py 
numpy version == 1.5.0
row sum:        8.472 us
column sum:     12.759 us

与你正在使用的v1.8.1大约减少30%相比:

$ python sum_benchmark.py 
numpy version == 1.8.1
row sum:        12.108 us
column sum:     15.768 us

值得注意的是,在最近的numpy版本中,这两种类型的缩减实际上都变慢了。我将不得不深入研究numpy的源代码 要明白为什么会这样。

更新

  • 为了记录,我在一台四核i7-2630QM CPU @ 2.0GHz上运行Ubuntu 14.04(内核v3.13.0-30)。两个版本的numpy都是使用GCC-4.8.1进行pip安装和编译的。
  • 我意识到我的原始基准测试脚本并不是完全不言自明的 - 您需要将总时间除以循环次数(1000)以获得每次调用的时间。
  • 它也是probably makes more sense to take the minimum across repeats rather than the average,因为这更有可能代表执行时间的下限(在此基础上,由于后台进程等原因,你会获得可变性)。

我已相应地更新了我的脚本和结果

我们还可以通过为每个调用创建一个全新的随机数组来消除调用缓存(时间局部性)的任何影响 - 只需将nloop设置为1,将nrep设置为相当小的数字(除非你真的喜欢看油漆干,否则说10。

在4000x4000阵列上

nloop=1nreps=10

numpy version == 1.5.0
row sum:        47.922 us
column sum:     103.235 us

numpy version == 1.8.1
row sum:        66.996 us
column sum:     125.885 us

这有点像它,但我仍然无法真正复制Francesc幻灯片显示的巨大影响。也许这并不令人惊讶 - 效果可能非常依赖于编译器,体系结构和/或内核。

答案 1 :(得分:3)

有趣。我可以重现塞巴斯蒂安的表现:

In [21]: np.__version__ 
Out[21]: '1.8.1'

In [22]: a = np.random.randn(4000, 4000)

In [23]: %timeit a[:, 1].sum()
100000 loops, best of 3: 12.4 µs per loop

In [24]: %timeit a[1, :].sum()
100000 loops, best of 3: 10.6 µs per loop

但是,如果我尝试使用更大的数组:

In [25]: a = np.random.randn(10000, 10000)

In [26]: %timeit a[:, 1].sum()
10000 loops, best of 3: 21.8 µs per loop

In [27]: %timeit a[1, :].sum()
100000 loops, best of 3: 15.8 µs per loop

但是,如果我再试一次:

In [28]: a = np.random.randn(10000, 10000)

In [29]: %timeit a[:, 1].sum()
10000 loops, best of 3: 64.4 µs per loop

In [30]: %timeit a[1, :].sum()
100000 loops, best of 3: 15.9 µs per loop

所以,不确定这里发生了什么,但这种抖动可能是由于缓存效应造成的。也许新架构在预测模式访问方面更明智,因此可以做更好的预取?

无论如何,为了比较,我使用的是NumPy 1.8.1,Linux Ubuntu 14.04和一台i5-3380M CPU @ 2.90GHz的笔记本电脑。

编辑:在考虑了一下之后,是的,我会说第一次timeit执行总和,列(或行)是从RAM中获取的,但第二次操作运行时,数据是在缓存中(对于行式和列式版本),因此它执行 fast 。由于timeit需要最少的运行时间,因此我们没有看到时间上的巨大差异。

另一个问题是为什么我们看到差异有时(使用timeit)。但是缓存是奇怪的野兽,特别是在多核机器中一次执行多个进程。

答案 2 :(得分:2)

我在 C 中编写了求和示例:结果显示为CPU time次测量,我总是使用gcc -O1 using-c.c进行编译(gcc版本:gcc版本4.9.0 20140604)。源代码如下。

我选择矩阵大小为n x n。对于n<2k,行和列求和没有任何可衡量的差异(n=2k每次运行6-7 us)。

行总和

n     first/us      converged/us
 1k       5                 4
 4k      19                12
10k      35                31
20k      70                61
30k     130                90
例如n=20k
Run 0 taken 70 cycles. 0 ms 70 us
Run 1 taken 61 cycles. 0 ms 60 us # this is the minimum I've seen in all tests
Run 1 taken 61 cycles. 0 ms 61 us
<snip> (always 60/61 cycles)

n     first/us      converged/us
 1k       5                 4
 4k     112                14
10k     228                32
20k     550               246
30k    1000               300

例如n=20k

Run 0 taken 552 cycles. 0 ms 552 us
Run 1 taken 358 cycles. 0 ms 358 us
Run 2 taken 291 cycles. 0 ms 291 us
Run 3 taken 264 cycles. 0 ms 264 us
Run 4 taken 252 cycles. 0 ms 252 us
Run 5 taken 275 cycles. 0 ms 275 us
Run 6 taken 262 cycles. 0 ms 262 us
Run 7 taken 249 cycles. 0 ms 249 us
Run 8 taken 249 cycles. 0 ms 249 us
Run 9 taken 246 cycles. 0 ms 246 us

讨论

行总和更快。我没有从任何缓存中获益很多,即重复的总和并不比初始总和快得多。列总和要慢得多,但它会在5-8次迭代中稳定增加。对n=4kn=10k的增加最为明显,其中缓存有助于将速度提高十倍左右。在较大的阵列中,加速仅约为2倍。我还观察到,虽然行求和非常快地收敛(在一次或两次试验之后),但列总和收敛需要更多次迭代(5次或更多次)。

为我带走课程:

  • 对于大型阵列(超过2k个元素),总和速度存在差异。我相信这是由于从RAM获取数据到L1d缓存时的协同作用。虽然我不知道一个读取的块/行大小,但我认为它大于8个字节。所以总结的下一个元素已经在缓存中了。
  • 列总和速度首先受内存带宽的限制。由于从RAM读取扩展块,CPU似乎饿死了数据。
  • 当重复执行求和时,人们期望某些数据不需要从RAM获取并且已经存在于L2 / L1d高速缓存中。对于行求和,这仅对n>30k显而易见,对于列总和,它已在n>2k处显而易见。

使用perf,我看不出有很大差异。但是C程序的大量工作是用随机数据填充阵列。我不知道如何消除这种情况&#34;设置&#34;数据...

以下是此示例的 C 代码:

#include <stdio.h>
#include <stdlib.h> // see `man random`
#include <time.h> // man  time.h, info clock

int
main (void)
{
  // seed
  srandom(62);
  //printf ("test %g\n", (double)random()/(double)RAND_MAX);
  const size_t SIZE = 20E3;
  const size_t RUNS = 10;
  double (*b)[SIZE];
  printf ("Array size: %dx%d, each %d bytes. slice = %f KiB\n", SIZE, SIZE, 
      sizeof(double), ((double)SIZE)*sizeof(double)/1024);

  b = malloc(sizeof *b * SIZE);

  //double a[SIZE][SIZE]; // too large!
  int i,j;
  for (i = 0; i< SIZE; i++) {
    for (j = 0; j < SIZE; j++) {
      b[i][j] = (double)random()/(double)RAND_MAX;
    }
  }
  double sum = 0;
  int run = 0;
  clock_t start, diff;
  int usec;
  for (run = 0; run < RUNS; run++) {
    start = clock();
    for (i = 0; i<SIZE; i++) {
      // column wise (slower?)
      sum += b[i][1];
      // row wise (faster?)
      //sum += b[1][i];
    }
    diff = clock() - start;
    usec = ((double) diff*1e6) / CLOCKS_PER_SEC; // https://stackoverflow.com/a/459704/543411
    printf("Run %d taken %d cycles. %d ms %d us\n",run, diff, usec/1000, usec%1000);
  }
  printf("Sum: %g\n", sum);
  return 0;
}

答案 3 :(得分:1)

我使用Numpy 1.9.0.def-ff7d5f9,在执行你发布的两个测试行时,我看到了10倍的差异。如果您的机器以及您用来构建Numpy的编译器与Numpy版本的加速一样重要,我不会感到惊讶。

在实践中,我并不认为想要减少这样的单个列或行是太常见了。我认为更好的测试是比较所有行的减少量

a.sum(axis=0)

减少所有列

a.sum(axis=1)

对我来说,这两个操作的速度差别很小(跨列减少大约需要95%的时间减少行数)。

编辑:一般来说,我非常谨慎地比较微秒级的操作速度。在安装Numpy时,将BLAS library与它联系起来是非常重要的,因为这是大多数大型矩阵运算(例如矩阵 - 矩阵乘法)的繁重之处。在比较BLAS库时,您肯定希望使用矩阵 - 矩阵点积等密集型操作作为比较点,因为这是您花费绝大部分时间的地方。我发现有时候,一个更糟糕的BLAS库实际上会比一个更好的BLAS库有更快的矢量矢量点操作。然而,更糟糕的是,像矩阵 - 矩阵点积和特征值分解这样的操作需要花费数十倍的时间,而这些操作比廉价的矢量矢量点更重要。我认为这些差异经常出现是因为你可以在C中编写一个相当快的矢量矢量点而不需要太多考虑,但编写一个好的矩阵 - 矩阵点需要大量的思考和优化,而且操作成本更高,所以这就是好的BLAS套餐付出了努力。

在Numpy中也是如此:任何优化都将在大型操作上进行,而不是小型操作,因此不要对小型操作之间的速度差异感到困惑。此外,很难判断小操作的任何速度差异是否真的是由计算时间引起的,或者仅仅是由于为优化更昂贵的操作而产生的开销。