为什么 Python 的 Numpy zeros 和空函数之间的速度差异对于更大的数组大小消失了?

Ger*_*VdE 11 python performance numpy

我被一个感兴趣的博客文章由Mike槎他比较需要两个函数的时间numpy.zeros((N,N))numpy.empty((N,N))N=200N=1000。我使用%timeit魔法在 jupyter notebook 中运行了一个小循环。下面的图表给出的所需要的时间之比numpy.zeronumpy.empty。对于N=346,numpy.zero比 慢大约 125 倍numpy.empty。在N=361及以上,这两个功能所需的时间几乎相同。

后来,在 Twitter 上的讨论导致了这样的假设:要么numpy为小分配做一些特殊的事情以避免malloc调用,要么操作系统可能会主动将分配的内存页面清零。

造成这种差异的原因是什么N,而较大的所需时间几乎相等N

在此处输入图片说明

通过启动堆溢出编辑:我可以重现它(这就是为什么我来到这里的第1名),这里有一个情节np.zerosnp.empty独立。该比率看起来像 GertVdE 的原始图:

在此处输入图片说明

在 Python 3.9.0 64 位、NumPy 1.19.2、Windows 10 Pro 2004 64 位上完成,使用此脚本生成数据:

from timeit import repeat
import numpy as np

funcs = np.zeros, np.empty

number = 10
index = range(501)

# tsss[n][f] = list of times for shape (n, n) and function f, one time for each round.
tsss = [[[] for _ in funcs] for _ in index]

for round_ in range(10):
    print('Round', round_)
    for n, tss in zip(index, tsss):
        for func, ts in zip(funcs, tss):
            t = min(repeat(lambda: func((n, n)), number=number)) / number
            t = round(t * 1e6, 3)
            ts.append(t)
    
# bss[f][n] = best time for function f and shape (n, n).
bss = [[min(tss[f]) for tss in tsss]
       for f in range(len(funcs))]

print('tss =', bss)
print('index =', index)
print('names =', [func.__name__ for func in funcs])
Run Code Online (Sandbox Code Playgroud)

然后这个脚本(在 colab)绘制:

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
from google.colab import files

tss = ... (copied from above script's output)
index = range(0, 501)
names = ['np.zeros', 'np.empty']

df = pd.DataFrame(dict(zip(names, tss)), index=index)
ax = df.plot(ylim=0, grid=True)
ax.set(xlabel='n', ylabel='time in ?s for shape (n, n)')
ax.legend(prop=font_manager.FontProperties(family='monospace'))
if 0:  # Make this true to create and download image files.
    plt.tight_layout()
    filename = f'np_zeros_vs_empty{cut}.png'
    ax.get_figure().savefig(filename, dpi=200)
    files.download(filename)
Run Code Online (Sandbox Code Playgroud)

堆溢出编辑结束。

use*_*ica 6

熟悉 NumPy 源代码后,我可以将这种影响缩小到malloc/calloc行为的结果- NumPy 并没有做任何特别的事情。由于不熟悉特定于操作系统的内存分配细节,我无法进一步缩小范围。

NumPy 有自己的用于小分配的空闲列表,但它们不用于任何大于 1024 字节的分配,并且这些测试中涉及的数组比这大得多。在这样的尺寸下,基本上numpy.empty和之间的唯一区别numpy.zerosempty使用malloczeros使用calloc

如果要检查这个你自己,你可以看看的代码empty,并zeros在NumPy的源代码库,并按照代码路径下至malloccalloc电话。


所以整个事情归结为malloccalloc行为。此行为高度特定于晦涩的库实现和操作系统版本详细信息。

博客文章中的计时是在 Windows 10 上运行的,并且malloc在高分配大小时显示速度变慢。

在 Linux 设置上运行的其他计时显示calloc 加速而不是malloc减速,因为calloc不需要物理归零内存。

来自另一个答案的 meTchaikovsky 的时间显示没有任何影响。

我对 Windows 10 内存分配的详细信息了解不够,无法确切说明为什么博客作者看到了他们所做的结果,而且我对您的设置也不太了解,甚至无法说出您看到的效果 - 您发布了一张图表时间比率,但比率没有显示calloc是加速还是malloc减速,而且您还没有说明您使用的是什么操作系统。

所有这一切都可能随着新的 Linux 内核或新的 Windows 更新而改变。


meT*_*sky 3

我的 NumPy/Python/操作系统版本

  • numpy 1.16.4
  • Python 3.6.8
  • macOS Catalina 10.15.5

据我了解,比从内存空间向分配的数组分配零np.zeros更进一步。np.empty基于这种理解,我相信np.empty会不断表现得比 更好np.zeros,因此我进行了自己的测试

import timeit
import numpy as np
from matplotlib import pyplot as plt


def zeros():
    zeros_array = np.zeros((N,N))
    assert zeros_array.data.contiguous
    return zeros_array
    
def empty():
    empty_array = np.empty((N,N))
    assert empty_array.data.contiguous
    return empty_array

def empty_assigned():
    empty_array = np.empty((N,N))
    empty_array[:,:] = 0
    return empty_array

zero_runs,empty_runs,empty_assigned_runs = [],[],[]
for N in range(10,500):

    time_zeros = np.mean(timeit.repeat("zeros()", "from __main__ import zeros",number=20))
    time_empty = np.mean(timeit.repeat("empty()", "from __main__ import empty",number=20))
    time_empty_assigned = np.mean(timeit.repeat("empty_assigned()", "from __main__ import empty_assigned",number=20))

    zero_runs.append(time_zeros)
    empty_runs.append(time_empty)
    empty_assigned_runs.append(time_empty_assigned)

fig,ax = plt.subplots(nrows=1,ncols=2,figsize=(12,8))
ax[0].plot(zero_runs,c='navy',label='zero')
ax[0].plot(empty_runs,c='r',label='empty',lw=2,linestyle='--')
ax[0].plot(empty_runs,c='seagreen',label='empty_assigned',lw=1)
ax[0].legend(loc='upper right')
ax[0].set_xlabel('N')
ax[0].set_ylabel('Time (s)')

ax[1].plot(np.array(zero_runs)/np.array(empty_runs),c='navy',label='zeros/empty')
ax[1].legend(loc='upper right')
ax[1].set_xlabel('N')
ax[1].set_ylabel('ratio')

plt.show()
Run Code Online (Sandbox Code Playgroud)

该脚本的示例结果是

结果

正如你所看到的,我无法重现你的结果,在这个测试中,np.empty性能始终优于np.zeros,并且随着 N 的增加,性能差异变得越来越显着。


更新

我 pip 安装了 Divakar 的软件包benchit,并运行了脚本

import numpy as np
import benchit
t = benchit.timings([np.zeros, np.empty], {n:(n,n) for n in 2**np.arange(20)}, input_name='len')
t.plot(logx=True, save='timings.png',figsize=(12,8))
Run Code Online (Sandbox Code Playgroud)

这是输出

输出

所以,我仍然无法使用我的计算机完全重现结果。此外,我多次运行我的脚本,结果相似,np.empty仍然比np.zeros.

如果我改变的话,还有一个有趣的效果

time_zeros = np.mean(timeit.repeat("zeros()", "from __main__ import zeros",number=20))
time_empty = np.mean(timeit.repeat("empty()", "from __main__ import empty",number=20))
time_empty_assigned = np.mean(timeit.repeat("empty_assigned()", "from __main__ import empty_assigned",number=20))
Run Code Online (Sandbox Code Playgroud)

time_empty = np.mean(timeit.repeat("empty()", "from __main__ import empty",number=20))
time_zeros = np.mean(timeit.repeat("zeros()", "from __main__ import zeros",number=20))
time_empty_assigned = np.mean(timeit.repeat("empty_assigned()", "from __main__ import empty_assigned",number=20))
Run Code Online (Sandbox Code Playgroud)

的表现np.empty会更好

输出


更新

N使用我自己的代码(在我的 12 英寸 MacBook 上),我对in进行了测试range(10,9000,200),这是输出

输出

似乎在 4000 左右有一些东西,所以我又对Nin进行了测试range(4000,4200),这似乎N=4096是临界点。

输出