【问题标题】:Performance of np.empty, np.zeros and np.onesnp.empty、np.zeros 和 np.ones 的性能
【发布时间】:2019-03-13 15:29:21
【问题描述】:

我很好奇使用np.empty 而不是np.zeros 真正产生了多大的差异,以及与np.ones 的差异。我运行这个小脚本来对每个创建一个大数组所花费的时间进行基准测试:

import numpy as np
from timeit import timeit

N = 10_000_000
dtypes = [np.int8, np.int16, np.int32, np.int64,
          np.uint8, np.uint16, np.uint32, np.uint64,
          np.float16, np.float32, np.float64]
rep= 100
print(f'{"DType":8s} {"Empty":>10s} {"Zeros":>10s} {"Ones":>10s}')
for dtype in dtypes:
    name = dtype.__name__
    time_empty = timeit(lambda: np.empty(N, dtype=dtype), number=rep) / rep
    time_zeros = timeit(lambda: np.zeros(N, dtype=dtype), number=rep) / rep
    time_ones = timeit(lambda: np.ones(N, dtype=dtype), number=rep) / rep
    print(f'{name:8s} {time_empty:10.2e} {time_zeros:10.2e} {time_ones:10.2e}')

结果得到下表:

DType         Empty      Zeros       Ones
int8       1.39e-04   1.76e-04   5.27e-03
int16      3.72e-04   3.59e-04   1.09e-02
int32      5.85e-04   5.81e-04   2.16e-02
int64      1.28e-03   1.13e-03   3.98e-02
uint8      1.66e-04   1.62e-04   5.22e-03
uint16     2.79e-04   2.82e-04   9.49e-03
uint32     5.65e-04   5.20e-04   1.99e-02
uint64     1.16e-03   1.24e-03   4.18e-02
float16    3.21e-04   2.95e-04   1.06e-02
float32    6.31e-04   6.06e-04   2.32e-02
float64    1.18e-03   1.16e-03   4.85e-02

由此我得出两个有点令人惊讶的结论:

  • np.emptynp.zeros 的性能几乎没有区别,可能除了int8 的一些区别。我不明白为什么会这样。创建一个空数组应该会更快,实际上我已经看到了这方面的报告(例如Speed of np.empty vs np.zeros)。
  • np.zerosnp.ones 之间存在很大差异。我怀疑这与用于内存归零的高性能方法有关,它不适用于用常量填充内存区域,但我真的不知道它是如何工作的,或者在什么级别上工作。

这些结果的解释是什么?

我在 Windows 10(带有 MKL)上使用 NumPy 1.15.4 和 Python 3.6 Anaconda,并且我有一个 Intel Core i7-7700K CPU。

编辑:根据 cmets 中的建议,我尝试运行基准测试,将每个单独的试验交错并在最后取平均值,但我看不到结果有显着差异。不过,在相关的说明中,我不知道 NumPy 中是否有任何机制可以重用刚刚删除的数组的内存,这会使措施不切实际(尽管时间似乎确实会随着数据类型的大小而增加)对于空数组)。

【问题讨论】:

  • @PavenMinaev 在this post 中有一条评论指出,诸如 FreeBSD 之类的操作系统可以在 CPU 空闲时清零未使用的内存块,因此当调用 calloc 时,它只会返回其中之一块,而不必当场将块归零。不确定 Windows 10 是否发生了类似的事情。此外,为了最大限度地减少缓存的影响,也许不是在运行另一个 dtype 之前一次性运行相同 dtype 的所有代表,而是交错运行它们,累积时间,然后最后分开。
  • “重用刚刚删除的数组的内存” 我很确定它确实如此。在 shell 中你可以尝试类似np.empty(4)np.ones(4)np.empty(4)。从第二次打电话到empty,我收到了四个电话,这可能不是巧合。

标签: python performance numpy initialization


【解决方案1】:

这确实应该是一个评论,但它不适合。这是您的脚本的一个小扩展。带有zerosones 的一些“手工”版本。

import numpy as np
from timeit import timeit

N = 10_000_000
dtypes = [np.int8, np.int16, np.int32, np.int64,
          np.uint8, np.uint16, np.uint32, np.uint64,
          np.float16, np.float32, np.float64]
rep= 100
print(f'{"DType":8s} {"Empty":>10s} {"Zeros":>10s} {"Ones":>10s}')
for dtype in dtypes:
    name = dtype.__name__
    time_empty = timeit(lambda: np.empty(N, dtype=dtype), number=rep) / rep
    time_zeros = timeit(lambda: np.zeros(N, dtype=dtype), number=rep) / rep
    time_ones = timeit(lambda: np.ones(N, dtype=dtype), number=rep) / rep
    time_full_zeros = timeit(lambda: np.full(N, 0, dtype=dtype), number=rep) / rep
    time_full_ones = timeit(lambda: np.full(N, 1, dtype=dtype), number=rep) / rep
    time_empty_zeros = timeit(lambda: np.copyto(np.empty(N, dtype=dtype), 0), number=rep) / rep
    time_empty_ones = timeit(lambda: np.copyto(np.empty(N, dtype=dtype), 1), number=rep) / rep
    print(f'{name:8s} {time_empty:10.2e} {time_zeros:10.2e} {time_ones:10.2e} {time_full_zeros:10.2e} {time_full_ones:10.2e}  {time_empty_zeros:10.2e} {time_empty_ones:10.2e} ')

时间是建议性的。

DType         Empty      Zeros       Ones
int8       1.37e-06   6.33e-04   5.73e-04   5.76e-04   5.73e-04    6.05e-04   5.82e-04 
int16      1.61e-06   1.55e-03   3.54e-03   3.54e-03   3.56e-03    3.54e-03   3.54e-03 
int32      7.22e-06   6.99e-06   1.24e-02   1.20e-02   1.25e-02    1.19e-02   1.21e-02 
int64      8.26e-06   8.06e-06   2.62e-02   2.64e-02   2.61e-02    2.62e-02   2.62e-02 
uint8      1.32e-06   6.30e-04   5.85e-04   5.86e-04   5.77e-04    5.70e-04   5.83e-04 
uint16     1.32e-06   1.63e-03   3.61e-03   3.65e-03   4.08e-03    4.08e-03   3.58e-03 
uint32     7.08e-06   7.20e-06   1.48e-02   1.41e-02   1.63e-02    1.44e-02   1.32e-02 
uint64     7.14e-06   7.13e-06   2.69e-02   2.67e-02   2.82e-02    2.68e-02   2.72e-02 
float16    1.31e-06   1.55e-03   3.56e-03   3.79e-03   3.54e-03    3.53e-03   3.55e-03 
float32    7.11e-06   6.95e-06   1.36e-02   1.35e-02   1.37e-02    1.35e-02   1.37e-02 
float64    7.27e-06   7.33e-06   3.13e-02   3.00e-02   2.75e-02    2.80e-02   2.75e-02 

Re zerosones 快 我似乎记得正如 cmets 中建议的那样 zeros 确实使用 calloc 这是一个系统例程,其唯一目的是分配零块可能很擅长.

【讨论】:

  • 谢谢。似乎有一个或多或少固定的时间来填充数组,一种或另一种方式,这适用于np.ones,但不适用于np.zeros。有趣的是,在您的情况下,np.emptynp.zeros 之间存在一些显着差异,但仅适用于 8 位和 16 位类型。
  • np.zeros 确实比空的快。 np.zeros 必须在像 malloc 一样清空接缝时初始化条目,而这一切都不必用 0 填充每个内存槽。
  • 好吧,按照thisempty其实更快...
猜你喜欢
  • 2019-02-15
  • 1970-01-01
  • 1970-01-01
  • 2022-11-14
  • 1970-01-01
  • 1970-01-01
  • 2014-10-09
  • 2017-06-16
  • 2021-12-09
相关资源
最近更新 更多