【问题标题】:How to speed up numpy array-filling in python?如何加快python中的numpy数组填充?
【发布时间】:2011-08-06 16:33:21
【问题描述】:

我正在尝试使用以下代码填充预分配的字节数组:

# preallocate a block array
dt = numpy.dtype('u8')
in_memory_blocks = numpy.zeros(_AVAIL_IN_MEMORY_BLOCKS, dt)

...

# write all the blocks out, flushing only as desired
blocks_per_flush_xrange = xrange(0, blocks_per_flush)
for _ in xrange(0, num_flushes):
    for block_index in blocks_per_flush_xrange:
        in_memory_blocks[block_index] = random.randint(0, _BLOCK_MAX)

    print('flushing bytes stored in memory...')

    # commented out for SO; exists in actual code
    # removing this doesn't make an order-of-magnitude difference in time
    # m.update(in_memory_blocks[:blocks_per_flush])

    in_memory_blocks[:blocks_per_flush].tofile(f)

几点:

  • num_flushes 很低,大约在 4 - 10
  • blocks_per_flush 是一个很大的数字,大约数百万
  • in_memory_blocks 可以是一个相当大的缓冲区(我将其设置为低至 1MB 和高至 100MB),但时间非常一致......
  • _BLOCK_MAX 是 8 字节无符号整数的最大值
  • mhashilib.md5()

使用上述代码生成 1MB 大约需要 1 秒; 500MB 大约需要 376 秒。相比之下,我使用 rand() 的简单 C 程序可以在 8 秒内创建一个 500MB 的文件。

如何提高上述循环的性能?我很确定我忽略了导致运行时巨大差异的明显因素。

【问题讨论】:

  • 与编译的特定类型迭代器(numpy内部)相比,像这样迭代任意类型(python)非常慢。如果您想要一个随机整数数组,就像您在示例中所做的那样,请使用 numpy.random.randint 函数。但我怀疑这只是为了说明目的。您需要发布实际填充数组的内容以获得更具体的帮助。
  • 不,我真的是在创建随机数据并计算校验和。我认为内存分配是瓶颈,而不是迭代。我可以想象内部 numpy 迭代器会更快(假设底层 C 实现将使用指针算法)。
  • 请澄清:_BLOCK_MAX 是否等于2559223372036854775807 以及dt 是否等于numpy.uint8numpy.uint64
  • 澄清一下:我的代码将dt 作为numpy.uint64_BLOCK_MAX 作为uint64 的上限。

标签: python optimization numpy


【解决方案1】:

由于您分配的是连续块,您应该能够执行以下操作(完全摆脱内部循环):

for _ in xrange(0, num_flushes):
    in_memory_blocks[:blocks_per_flush] = numpy.random.randint(
            0, _BLOCK_MAX+1, blocks_per_flush)

    print('flushing bytes stored in memory...')

    # commented out for SO; exists in actual code
    # removing this doesn't make an order-of-magnitude difference in time
    # m.update(in_memory_blocks[:blocks_per_flush])

    in_memory_blocks[:blocks_per_flush].tofile(f)

这使用numpy.random.randint 函数分配整个内存块并用随机整数填充它(注意J.F. Sebastian 下面关于numpy.random.randintrandom.randint 的评论)。没有办法(据我所知)使用 numpy 随机例程填充预分配的数组。另一个问题是 numpy 的 randint 返回 int64 数组。如果您需要其他大小的整数,则可以使用 numpy 键入方法,例如 numpy.uint8。如果您希望 randints 涵盖类型的整个范围,那么下面使用 numpy.random.bytes 的@J. F. Sebastian 的方法将是最好的(几乎在任何情况下!)。

但是,简单的测试显示合理的时间(与 C 代码的数量级相同)。以下代码测试使用 numpy 方法分配 20,000,000 个随机整数的 uint8 数组的时间:

from timeit import Timer
t = Timer(stmt='a=numpy.uint8(numpy.random.randint(0, 100, 20000000))',
        setup='import numpy')
test_runs = 50
time = t.timeit(test_runs)/test_runs
print time

在我 4 岁的 Core2 笔记本电脑上,每次分配大约需要 0.7 秒(它运行 50 次,因此运行整个测试需要更长的时间)。每次分配 20,000,000 个随机 uint8 整数需要 0.7 秒,所以我预计整个 500MB 大约需要 20 秒。

更多内存意味着您可以一次分配更大的块,但是当您只需要 8 位时,您仍然有效地浪费时间为每个 int 分配和写入 64 位(我没有量化这种效果)。如果它仍然不够快,您可以使用 numpy ctypes 接口调用您的 C 实现。这真的很容易使用,而且你几乎不会比纯 C 减速。

一般的带回家的信息是,对于 numpy,始终尝试使用它们存在的 numpy 例程,记住使用 ctypes 回退到 C 并不会太痛苦。一般来说,这种方法可以非常有效地使用 python,而数值处理的速度几乎没有减慢。

编辑:我刚刚想到的另一件事:正如上面的实现,我认为你会制作额外的不必要的副本。如果in_memory_blocks 的长度为blocks_per_flush,那么您最好将其分配给numpy.random.randint 的返回值,而不是将其分配给某个子数组(在一般情况下必须成为副本)。所以:

in_memory_blocks = numpy.random.randint(0, _BLOCK_MAX+1, blocks_per_flush)

而不是:

in_memory_blocks[:blocks_per_flush] = numpy.random.randint(
        0, _BLOCK_MAX+1, blocks_per_flush)

但是,在计时后,第一种情况不会导致速度显着提高(仅约 2%),因此可能不值得过多担心。我猜大部分时间都花在实际生成随机数上(这是我所期望的)。

【讨论】:

  • 这样做导致了显着的加速。以前我在看~1MB/秒。通过这个更改,我可以生成 ~258MB/秒。我注意到了 numpy.random.randint function 但跳过了它,因为我错误地认为内存分配是主要的性能损失。
  • numpy.random.randint()(半开区间)不同于random.randint()(闭区间:包括两端)。使用numpy.random.random_integers()
  • numpy.dtype('u8')numpy.dtype('uint64');不是numpy.dtype('uint8')
  • 我的错误,我认为它们是相同的,我会补充说明。
  • 我已经在我的实现中删除了副本(即,当我编码它时,我使用了in_memory_blocks = numpy.random.randint(0, _BLOCK_MAX, blocks_per_flush))。由于numpy.random.randint(...) 返回了一个新缓冲区,我认为没有理由将其复制到in_memory_blocks
【解决方案2】:

由于0.._BLOCK_MAX 涵盖了numpy.uint8 的所有可能值(我假设numpy.dtype('u8')(即numpy.uint64 是一个错字),您可以使用:

import numpy as np

for _ in xrange(0, num_flushes):
    in_memory_blocks = np.frombuffer(np.random.bytes(blocks_per_flush),
                                     dtype=np.uint8)

    print('flushing bytes stored in memory...')
    # ...

这个变种比@hgomersall's one快8倍:

$ python -mtimeit -s'import numpy as np' '
>     np.uint8(np.random.randint(0,256,20000000))'
10 loops, best of 3: 316 msec per loop

$ python -mtimeit -s'import numpy as np' '
>     np.frombuffer(np.random.bytes(20000000), dtype=np.uint8)'
10 loops, best of 3: 38.6 msec per loop

如果numpy.dtype('u8') 不是拼写错误并且您确实需要numpy.uint64,那么:

a = np.int64(np.random.random_integers(0, _BLOCK_MAX, blocks_per_flush))
in_memory_blocks = a.view(np.uint64) # unsigned

注意:如果数组的 dtype 已经是 np.int64np.int64() 不会进行复制。 .view(numpy.uint64) 强制将其解释为无符号(也不执行复制)。

【讨论】:

  • 我需要执行(至少)n 写入磁盘。
  • #1(使用np.random.randint(...))实际上返回的字节数不会是 #2 的 4 到 8 倍(取决于您机器上的默认 int 类型)吗?例如,我可以天真地想象通过生成int(num_bytes_requested/unsigned_int_size)rand-ints + 填充剩余字节来实现 np.random.bytes。这可以解释性能差异。
  • @Allen George:我只在dtype=np.uint8 的情况下使用np.random.bytes(),是的np.uint8(np.random.randint(0,256,20000000)) 临时创建的数组比在这种情况下所需的大4-8 倍。对于dtype=np.uint64,请使用np.int64(np.random.random_integers(0, _BLOCK_MAX, blocks_per_flush)).view(np.uint64)
  • @J.F.Sebastian:我找不到np.uint8() 的文档:你有参考吗?我看到它将一个序列(可能是一个数组)转换为一个 dtype uint8 的数组,但很高兴知道这是记录在案的,因此是一个稳定的功能。
  • @EOL:np.uint8 的行为与 numpy 中的其他 dtype-types 相同。 docs.scipy.org/doc/numpy/user/basics.types.html
【解决方案3】:

如果您只是尝试一次填充一个文件,block_size 个字节,这可能比以前的答案更快。基于生成器并完全绕过数组创建:

import numpy as np

def random_block_generator(block_size):
    while True:
        yield np.random.bytes(block_size)

rbg = random_block_generator(BLOCK_SIZE)

那么你的用法是:

f = open('testfile.bin','wb')

for _ in xrange(blocks_to_write):
    f.write( rbg.next())

f.close()

Numpy 使用确定性随机数生成(序列中的下一个数字始终相同,它只是在初始化时从随机位置开始)。如果您需要真正的随机数据(密码级别),那么您可以使用import Crypto.Random as cryield cr.get_random_bytes(block_size) 而不是 np。

另外,如果您的 BLOCK_SIZE 是一个已定义的常量,您可以使用这样的生成器表达式(这次使用 Crypto 库):

import Crypto.Random as cr
from itertools import repeat

BLOCK_SIZE = 1000

rbg = (cr.get_random_bytes(BLOCK_SIZE) for _ in repeat(0))

f = open('testfile.bin','wb')

for _ in xrange(blocks_to_write):
    f.write( rbg.next())

f.close()

这包括实现rbg=... 和执行。这种生成器方法,即使使用稍慢的 Crypto.Random,也会在它最大化计算之前从磁盘 i/o 中最大化(尽管我确信其他答案也是如此)。

更新:

Athlon X2 245 上的一些计时数据 --

  • 加密:生成 500MB,不要写入 -- 10.8s (46 MB/s)
  • 加密:生成 500MB 并写入 -- 11.2s (44.5 MB/s)
  • Numpy:生成 500MB,不要写入 -- 1.4s (360 MB/s)
  • Numpy:生成 500MB,然后写入 -- 7.1s (70 MB/s)

所以 numpy 版本快了大约 8 倍(快到足以让我的旧盘片驱动器最大化)。我使用生成器表达式形式而不是生成器函数形式对它们进行了测试。

【讨论】:

    【解决方案4】:

    我不太擅长优化,但我看不出有什么方法可以让您的代码运行得更快。您正在使用纯迭代器和 O(1) 访问结构。

    我认为,问题在于您选择的语言。请记住,您是在虚拟机中运行的,并且是一个解释器。您的 C 程序的运行速度总是会快一个数量级。

    【讨论】:

    • 在 Python 中使用 NumPy 包实际上很快:通过编译代码完成耗时的操作。因此,使用 NumPy 的 C 程序和 Python 程序可以具有相似的运行时间。确实,Python 版本较慢(有时不会慢很多),而且它可能会慢很多(例如,如果不使用快速 NumPy 数组操作)。另一方面,我看到 Python 程序比 Fortran 程序运行得更快,因为 Fortran 程序不必要地预先分配了巨大的数组,“以防万一”有很多数据要处理。
    猜你喜欢
    • 2016-06-15
    • 1970-01-01
    • 1970-01-01
    • 2017-04-19
    • 1970-01-01
    • 1970-01-01
    • 2018-05-07
    • 1970-01-01
    • 2018-09-18
    相关资源
    最近更新 更多