【问题标题】:Why is allocation using np.empty not O(1)为什么使用 np.empty 分配不是 O(1)
【发布时间】:2021-04-21 05:48:08
【问题描述】:

官方上是这么说的numpydocs

返回给定形状和类型的新数组,而不初始化条目。

对于np.empty,这意味着创建(分配)该数组所需的时间为 O(1),但在 timeit 中进行的一些简单测试表明情况并非如此:

>>> timeit.timeit(lambda: np.empty(100000000 ), number=10000)
0.2733485999999914
>>> timeit.timeit(lambda: np.empty(1000000000), number=10000)
0.8293009999999867

作为一个附带问题,未触及的np.empty 数组中存在哪些值?它们都是非常小的值,但我希望它们只是该地址内存中存在的任何值。 (示例数组:np.empty(2) = array([-6.42940774e-036, 2.07409447e-117])。这些看起来不像存储在内存中的东西)

【问题讨论】:

  • 使用这么大的数组,您可能会遇到内存管理的复杂性。
  • 为什么你认为,你观察到的数字并不代表“内存中那个地址的任何值”?是什么让这些小值比您预期的随机数更不可能?
  • @Holger 我猜是因为它们始终是小数字,我认为它们应该是 4 字节长的随机整数,或者但是,就像我确定它是 some 表示内存,它似乎完全从二进制数据中删除了
  • 为什么将随机数据解释为浮点值会产生整数?
  • 在符合 IEEE-754 的系统上使用 np.arange(10, dtype=np.uint64).view('<f8') 将前 64 位值重新解释为 64 位浮点数会产生如下值:array([0.0e+000, 4.9e-324, 9.9e-324, 1.5e-323, 2.0e-323, 2.5e-323, 3.0e-323, 3.5e-323, 4.0e-323, 4.4e-323])。因此,@Holger 对此完全正确。

标签: python numpy time-complexity


【解决方案1】:

首先,我尝试在不同尺寸的机器上重现这种行为。以下是原始结果:

np.empty(10**1)   # 421 ns ± 23.7 ns per loop    (on 7 runs, 1000000 loops each)
np.empty(10**2)   # 406 ns ± 1.44 ns per loop    (on 7 runs, 1000000 loops each)
np.empty(10**3)   # 471 ns ± 5.8 ns per loop     (on 7 runs, 1000000 loops each)
np.empty(10**4)   # 616 ns ± 1.56 ns per loop    (on 7 runs, 1000000 loops each)
np.empty(10**5)   # 620 ns ± 2.83 ns per loop    (on 7 runs, 1000000 loops each)
np.empty(10**6)   # 9.61 µs ± 34.2 ns per loop   (on 7 runs, 100000 loops each)
np.empty(10**7)   # 11.1 µs ± 17.6 ns per loop   (on 7 runs, 100000 loops each)
np.empty(10**8)   # 22.1 µs ± 173 ns per loop    (on 7 runs, 10000 loops each)
np.empty(10**9)   # 62.8 µs ± 220 ns per loop    (on 7 runs, 10000 loops each)
np.empty(10**10)  # => Memory Error

因此,您是对的:这不是O(1)(至少在我的 Windows 机器和您的系统上也是如此)。请注意,在这么短的时间内无法(急切地)初始化这些值,因为这意味着我的机器上显然没有超过 127 TB/s 的 RAM 吞吐量。

对于 np.empty,这意味着创建(分配)这个数组所花费的时间是 O(1)

分配在O(1) 中完成的假设并不完全正确。为了检查这一点,我构建了一个简单的 C 程序,执行了一个简单的 malloc+free 循环并测量了时间。以下是原始结果:

./malloc.exe 10           # Average time:  41.815 ns (on 1 run, 1000000 loops each)
./malloc.exe 100          # Average time:  45.295 ns (on 1 run, 1000000 loops each)
./malloc.exe 1000         # Average time:  47.400 ns (on 1 run, 1000000 loops each)
./malloc.exe 10000        # Average time: 122.457 ns (on 1 run, 1000000 loops each)
./malloc.exe 100000       # Average time: 123.032 ns (on 1 run, 1000000 loops each)
./malloc.exe 1000000      # Average time:   8.351 us (on 1 run, 1000000 loops each)
./malloc.exe 10000000     # Average time:   9.342 us (on 1 run, 100000 loops each)
./malloc.exe 100000000    # Average time:  18.972 us (on 1 run, 10000 loops each)
./malloc.exe 1000000000   # Average time:  64.527 us (on 1 run, 10000 loops each)
./malloc.exe 10000000000  # => Memory error

如您所见,结果与 Numpy 的结果相匹配(由于在 CPython 中调用 Python 函数的开销导致的小结果除外)。因此,问题不在于 Numpy,而在于标准 libc 中的分配算法或操作系统本身。

作为一个附带问题,未触及的 np.empty 数组中存在哪些值?

这是未初始化的数据。在实践中,它通常是零初始化(但并非总是如此),因为出于安全原因,主流平台会清理分配的内存(以便密码等关键数据在先前存储在另一个进程的内存中时不会泄漏)。 你不应该依赖这个


malloc 时序的更深入解释:

如您所见,分配 100K 项和 1M 项之间存在差距。这可以通过使用快速用户空间分配器(在Unix和Linux系统上称为sbrk)来解释:当数据较小时,大多数主流平台的libc不会直接请求内存操作系统。它宁愿使用快速预分配的本地内存池。实际上,在大多数主流平台上,预先分配了多个不同大小的池,libc 根据分配的大小选择“正确的”,因此对于小数据大小的时序变化。请注意,此过程是为了提高分配速度,同时考虑到memory fragmentation。此策略要快得多,因为内核调用(如mmap非常昂贵(在我的机器上至少需要几微秒)。

此外,大多数操作系统 (OS) 都有多个内存池。 Linux、MacOS 和 Windows 将 虚拟内存 分割成小的 页面(通常为 4KB)。由于在处理 GB/TB 的已分配数据时处理太小的页面会带来很大的开销,因此这些操作系统还提供称为超级页面或大页面(通常为 2MB 到几 GB)的大页面。操作系统中采用的路径可能会根据分配的内存量而改变,并且大多数操作系统都针对分配小块虚拟内存而不是大块进行了优化。

请注意,用于管理系统内存的数据结构的大小通常受限于 RAM 的大小,而 RAM 的大小通常在运行时是恒定的。此外,在给定操作系统中用于管理内存碎片的算法的复杂度可能在理论上O(1)(或接近)。因此,有些人认为分配/释放数据是在恒定时间内完成的。但这有争议,因为人们应该考虑实际结果,而不仅仅是理论渐近界


有关更多信息,您可以查看以下帖子:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-13
    • 2020-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-01
    • 1970-01-01
    相关资源
    最近更新 更多