Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/299.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181

Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/django/24.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python 为什么线性读-无序写不比无序读-线性写快?_Python_Performance_Numpy_X86_Cpu Cache - Fatal编程技术网

Python 为什么线性读-无序写不比无序读-线性写快?

Python 为什么线性读-无序写不比无序读-线性写快?,python,performance,numpy,x86,cpu-cache,Python,Performance,Numpy,X86,Cpu Cache,我目前正试图更好地理解与内存/缓存相关的性能问题。我在某个地方读到,内存局部性对于读取比写入更重要,因为在前一种情况下,CPU实际上必须等待数据,而在后一种情况下,它只能将数据运出并忘记它们 考虑到这一点,我做了以下快速而肮脏的测试:我编写了一个脚本,创建了一个由N个随机浮点数和一个置换组成的数组,即一个以随机顺序包含数字0到N-1的数组。然后,它重复(1)线性读取数据数组并将其以排列给定的随机访问模式写入新数组,或(2)以排列顺序读取数据数组并将其线性写入新数组 令我惊讶的是,(2)似乎总是比

我目前正试图更好地理解与内存/缓存相关的性能问题。我在某个地方读到,内存局部性对于读取比写入更重要,因为在前一种情况下,CPU实际上必须等待数据,而在后一种情况下,它只能将数据运出并忘记它们

考虑到这一点,我做了以下快速而肮脏的测试:我编写了一个脚本,创建了一个由N个随机浮点数和一个置换组成的数组,即一个以随机顺序包含数字0到N-1的数组。然后,它重复(1)线性读取数据数组并将其以排列给定的随机访问模式写入新数组,或(2)以排列顺序读取数据数组并将其线性写入新数组

令我惊讶的是,(2)似乎总是比(1)快。然而,我的剧本有问题

  • 脚本是用python/numpy编写的。这是一种相当高级的语言,不清楚读/写是如何实现的
  • 我可能没有正确地平衡这两个案例
此外,下面的一些回答/评论表明,我最初的预期是不正确的,根据cpu缓存的详细信息,两种情况都可能更快

我的问题是:

  • 两者中哪一个(如果有的话)应该更快
  • 这里的相关缓存概念是什么;它们如何影响结果
初学者友好的解释将不胜感激。任何支持代码都应该使用C/cython/numpy/numba或python

可选:

  • 解释为什么绝对持续时间在问题大小上是非线性的(参见下面的计时)
  • 解释我的明显不足的python实验的行为
作为参考,我的平台是
Linux-4.12.14-lp150.11-default-x86_64-with-glibc2.3.4
。Python版本是3.6.5

以下是我编写的代码:

import numpy as np
from timeit import timeit

def setup():
    global a, b, c
    a = np.random.permutation(N)
    b = np.random.random(N)
    c = np.empty_like(b)

def fwd():
    c = b[a]

def inv():
    c[a] = b

N = 10_000
setup()

timeit(fwd, number=100_000)
# 1.4942631321027875
timeit(inv, number=100_000)
# 2.531870319042355

N = 100_000
setup()

timeit(fwd, number=10_000)
# 2.4054739447310567
timeit(inv, number=10_000)
# 3.2365565397776663

N = 1_000_000
setup()

timeit(fwd, number=1_000)
# 11.131387163884938
timeit(inv, number=1_000)
# 14.19817715883255
正如@Trilarion和@Yann Vernier所指出的,我的代码片段没有得到适当的平衡,所以我用

def fwd():
    c[d] = b[a]
    b[d] = c[a]

def inv():
    c[a] = b[d]
    b[a] = c[d]
其中
d=np.arange(N)
(我以两种方式洗牌所有内容,希望减少跨试验缓存的影响)。我还将
timeit
替换为
repeat
,并将重复次数减少了10倍

然后我得到

[0.6757169323973358, 0.6705542299896479, 0.6702114241197705]    #fwd
[0.8183442652225494, 0.8382121799513698, 0.8173762648366392]    #inv
[1.0969422250054777, 1.0725746559910476, 1.0892365919426084]    #fwd
[1.0284497970715165, 1.025063106790185, 1.0247828317806125]     #inv
[3.073981977067888, 3.077839042060077, 3.072118630632758]       #fwd
[3.2967213969677687, 3.2996009718626738, 3.2817375687882304]    #inv
因此,似乎仍然存在差异,但更微妙的是,现在可以根据问题的大小选择任何一种方式。

  • 首先反驳一下你的直觉:
    fwd
    击败
    inv
    ,即使没有numpy mecanism
numba版本就是这样:

import numba

@numba.njit
def fwd_numba(a,b,c):
    for i in range(N):
        c[a[i]]=b[i]

@numba.njit
def inv_numba(a,b,c):
    for i in range(N):
        c[i]=b[a[i]]
N=10000的计时:

%timeit fwd()
%timeit inv()
%timeit fwd_numba(a,b,c)
%timeit inv_numba(a,b,c)
62.6 µs ± 3.84 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
144 µs ± 2 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
16.6 µs ± 1.52 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
34.9 µs ± 1.57 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  • 其次,Numpy必须处理可怕的对齐和(缓存)局部性问题
它本质上是对BLAS/ATLAS/MKL中的低级程序的包装,并为此进行了调优。 花式索引是一个很好的高级工具,但对于这些问题来说是异端的;这一概念在低层次上没有直接的转换

  • 第三,细节。特别是:
除非在获取项目期间只有一个索引数组,否则 事先检查指标的有效性。否则就要处理了 在内部循环本身进行优化

我们就在这里。我认为这可以解释这种差异,以及为什么set比get慢


这也解释了为什么手工制作的numba通常更快:它不会检查任何东西,也不会在不一致的索引上崩溃。

您的函数
fwd
没有触及全局变量
c
。您没有告诉它(仅在
设置
),因此它有自己的局部变量,并在cpython中使用
STORE\u FAST

>>> import dis
>>> def fwd():
...     c = b[a]
...
>>> dis.dis(fwd)
  2           0 LOAD_GLOBAL              0 (b)
              3 LOAD_GLOBAL              1 (a)
              6 BINARY_SUBSCR
              7 STORE_FAST               0 (c)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE
现在,让我们用一个全局

>>> def fwd2():
...     global c
...     c = b[a]
...
>>> dis.dis(fwd2)
  3           0 LOAD_GLOBAL              0 (b)
              3 LOAD_GLOBAL              1 (a)
              6 BINARY_SUBSCR
              7 STORE_GLOBAL             2 (c)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE
即使如此,它在时间上也可能与调用全局设置项的
inv
函数有所不同

无论哪种方式,如果您希望将其写入
c
,您需要类似
c[:]=b[a]
c.fill(b[a])
的内容。赋值用右侧的对象替换变量(名称),因此旧的
c
可能会被释放,而不是新的
b[a]
,而这种内存洗牌可能代价高昂

至于我想你想要衡量的效果,基本上是正向或反向排列是否更昂贵,这将是高度依赖缓存的。前向置换(以线性读取的随机顺序索引存储)原则上可以更快,因为它可以使用写掩蔽,并且永远不会获取新数组,前提是缓存系统足够智能,可以在写缓冲区中保留字节掩蔽。如果阵列足够大,则在执行随机读取时向后运行缓存冲突的高风险


这是我最初的印象;正如你所说,结果是相反的。这可能是由于缓存实现没有大的写缓冲区或无法利用小的写操作造成的。如果缓存外访问仍然需要相同的内存总线时间,则读访问将有机会加载在需要之前不会从缓存中删除的数据。使用多路缓存时,部分写入的行也有可能不会被选择进行驱逐;只有脏缓存线需要内存总线时间才能删除。使用其他知识编写的较低级别程序(例如,排列完整且不重叠)可以使用非时态SSE写入等提示改进行为

下面的实验证实了随机写入比随机读取快。对于较小尺寸的数据(当它完全适合缓存时),随机写入代码比随机读取代码慢(可能是因为
numpy
中的某些实现特性),但随着数据大小的增长,执行时间的初始差为1.7倍
$ cat test.py 
import numpy as np
from timeit import timeit
import numba

def fwd(a,b,c):
    c = b[a]

def inv(a,b,c):
    c[a] = b

@numba.njit
def fwd_numba(a,b,c):
    for i,j in enumerate(a):
        c[i] = b[j]

@numba.njit
def inv_numba(a,b,c):
    for i,j in enumerate(a):
        c[j] = b[i]


for p in range(4, 8):
    N = 10**p
    n = 10**(9-p)
    a = np.random.permutation(N)
    b = np.random.random(N)
    c = np.empty_like(b)
    print('---- N = %d ----' % N)
    for f in 'fwd', 'fwd_numba', 'inv', 'inv_numba':
        print(f, timeit(f+'(a,b,c)', number=n, globals=globals()))

$ python test.py 
---- N = 10000 ----
fwd 1.1199337750003906
fwd_numba 0.9052993479999714
inv 1.929507338001713
inv_numba 1.5510062070025015
---- N = 100000 ----
fwd 1.8672701190007501
fwd_numba 1.5000483989970235
inv 2.509873716000584
inv_numba 2.0653326050014584
---- N = 1000000 ----
fwd 7.639554155000951
fwd_numba 5.673054756000056
inv 7.685382894000213
inv_numba 5.439735023999674
---- N = 10000000 ----
fwd 15.065879136000149
fwd_numba 12.68919651500255
inv 15.433822674000112
inv_numba 14.862108078999881
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
32K
for i in a:
    c[i] = b[i]
#1. (iteration 1) c[0] = b[0]
1a. read memory at b[0] and store result in register c0
1b. write register c0 at memory address c[0]
#2. (iteration 2) c[1] = b[1]
2a. read memory at b[1] and store result in register c1
2b. write register c1 at memory address c[1]
#1. (iteration 2) c[2] = b[2]
3a. read memory at b[2] and store result in register c2
3b. write register c2 at memory address c[2]
# etc
size    4000    6000    9000    13496   20240   30360   45536   68304   102456  153680  230520  345776  518664  777992  1166984
rd-rand 1.86821 2.52813 2.90533 3.50055 4.69627 5.10521 5.07396 5.57629 6.13607 7.02747 7.80836 10.9471 15.2258 18.5524 21.3811
wr-rand 7.07295 7.21101 7.92307 7.40394 8.92114 9.55323 9.14714 8.94196 8.94335 9.37448 9.60265 11.7665 15.8043 19.1617 22.6785