Как ускорить заполнение массива numpy в python?

Я пытаюсь заполнить предварительно выделенный bytearray, используя следующий код:

# 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-байтового unsigned int
  • m - это hashilib.md5()

генерация 1MB с использованием вышеуказанного кода занимает ~1s; 500MB занимает ~376s. Для сравнения, моя простая программа C, которая использует rand (), может создать файл 500MB в 8s.

как я могу улучшить производительность в приведенном выше цикле? Я уверен, что игнорирую что-то очевидное, что вызывает эту огромную разницу во времени выполнения.

4 ответов


поскольку вы выделяете непрерывные блоки, вы должны иметь возможность сделать следующее (Полностью избавиться от внутреннего цикла):

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.randint и random.randint). Нет никакого способа (насколько я могу видеть) заполнить предварительно выделенный массив, используя случайные подпрограммы numpy. Другая проблема заключается в том, что randint numpy возвращает int64 матрицы. Если вам нужны целые числа другого размера, вы можете использовать методы ввода numpy, например numpy.тип uint8. Если вы хотите, чтобы randints охватывал весь диапазон типа, то @Джей Ф. Себастьянметод ниже с помощью numpy.случайность.bytes будет лучшим (почти в любом случае!).

однако простые тесты показывают разумные времена (того же порядка величины, что и код C). Следующий код проверяет время выделения массивов uint8 из 20,000,000 случайных целые числа, использующие метод numpy:

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

я получаю это занимает около 0,7 секунды за распределение на моем 4-летнем ноутбуке Core2 (он работает 50 раз, поэтому для запуска всего теста потребуется больше времени). Это 0.7 s за распределение 20,000,000 случайных целых чисел uint8, поэтому я ожидаю что-то около 20s для всего 500MB.

больше памяти означало бы, что вы можете выделить большие куски сразу, но вы все еще эффективно тратите время на выделение и запись 64 бит для каждого int когда вам нужно только 8 (не количественно этот эффект). Если его все еще недостаточно быстро, вы можете вызвать свою реализацию C с помощью интерфейса numpy ctypes. Это действительно довольно прост в использовании, и вы получите практически никакого замедления над чистым C.

общее сообщение Take home заключается в том, что с numpy всегда старайтесь использовать процедуры numpy там, где они существуют, помня, что возврат к C с ctypes не слишком болезнен. В целом, данная методика позволяет действительно достаточно эффективно использовать python с очень небольшим замедлением для численной обработки.

Edit: что-то еще, что только что пришло мне в голову: как было реализовано выше, я думаю, вы сделаете дополнительную ненужную копию. Если 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%), поэтому, вероятно, не стоит беспокоиться о слишком много. Я предполагаю, что подавляющее количество времени тратится на создание случайных чисел (чего я и ожидал).


из-за того, что 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...')
    # ...

этот вариант ~в 8 раз быстрее, чем @hgomersall один:

$ 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

Примечание: np.int64() не делает копию, если dtype массива уже np.int64. .view(numpy.uint64) заставляет его интерпретацию как неподписанную (также не выполняется копирование).


если вы просто пытаетесь заполнить файл, 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 cr и yield cr.get_random_bytes(block_size) вместо np.

кроме того, если ваш BLOCK_SIZE является определенной константой, вы можете использовать выражение генератора, подобное этому (используя криптографическую библиотеку на этот раз):

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=... и исполнения. Этот метод генератора, даже с немного более медленной криптографией.Random, будет Макс из диска ввода-вывода задолго до его вычисления Макса (хотя я уверен, что другие ответы тоже).

обновление:

некоторые данные по времени на Athlon X2 245 --

  • Crypto: генерировать 500 МБ, не писать -- 10.8 s (46 МБ/с)
  • Crypto: генерировать 500 МБ и писать -- 11.2 s (44.5 МБ/с)
  • Numpy: генерировать 500 МБ, не писать -- 1.4 s (360 МБ/с)
  • Numpy: создайте 500 МБ и напишите -- 7.1 s (70 МБ/с)

таким образом, версия numpy примерно в 8 раз быстрее (достаточно быстро, чтобы максимизировать мой старый диск). Я тестировал оба из них, используя форму выражения генератора, а не чем форма функции генератора.


Я не очень хорош в оптимизации, но я не вижу, как ваш код может работать быстрее. Вы используете чисто итераторы и структуру доступа O(1).

проблема, я думаю, в вашем языке выбора. Помните, что вы работаете в виртуальной машине, и интерпретатор при этом. Ваша программа всегда будет работать на порядок быстрее.