我已经使用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
数据: Perf
和nloop = 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 中的求和,结果在下面的答案中。
答案 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的源代码 要明白为什么会这样。
我已相应地更新了我的脚本和结果
我们还可以通过为每个调用创建一个全新的随机数组来消除调用缓存(时间局部性)的任何影响 - 只需将nloop
设置为1,将nrep
设置为相当小的数字(除非你真的喜欢看油漆干,否则说10。
nloop=1
,nreps=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=4k
到n=10k
的增加最为明显,其中缓存有助于将速度提高十倍左右。在较大的阵列中,加速仅约为2倍。我还观察到,虽然行求和非常快地收敛(在一次或两次试验之后),但列总和收敛需要更多次迭代(5次或更多次)。
为我带走课程:
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中也是如此:任何优化都将在大型操作上进行,而不是小型操作,因此不要对小型操作之间的速度差异感到困惑。此外,很难判断小操作的任何速度差异是否真的是由计算时间引起的,或者仅仅是由于为优化更昂贵的操作而产生的开销。