Эффективность случайного среза на карте памяти
В качестве файла данных у меня есть двумерный массив размером 20 ГБ, 100 000 x 100 000 'float16'. Я загружаю его в память следующим образом:
fp_read = np.memmap(filename, dtype='float16', mode='r', shape=(100000, 100000))
Затем я пытаюсь прочитать фрагменты из него. Вертикальные срезы, которые мне нужно взять, являются практически случайными, но производительность для этого очень низкая, или я что-то не так делаю?
Анализ:
Я сравнил с другими формами поперечного среза, который намного лучше, хотя я не знаю, почему это должно быть:
%timeit fp_read[:,17000:17005] # slice 5 consecutive cols
1.64 µs ± 16.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,11000:11050:10]
1.67 µs ± 21 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,5000:6000:200]
1.66 µs ± 27.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,0:100000:20000] # slice 5 disperse cols
1.69 µs ± 14.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,[1,1001,27009,81008,99100]] # slice 5 rand cols
32.4 ms ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
a = np.arange(100000); b = np.array([1,1001,27009,81008,99100])
%timeit fp_read[np.ix_(a,b)]
18 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Даже эти функции timeit точно не отражают снижение производительности, так как:
import time
a = np.arange(100000)
cols = np.arange(100000)
np.random.shuffle(cols)
cols = np.sort(cols[:5])
t = time.time()
arr = fp_read[np.ix_(a,cols)]
print('Actually took: {} seconds'.format(time.time() - t))
Actually took: 24.5 seconds
По сравнению с:
t = time.time()
arr = fp_read[:,0:100000:20000]
print('Actually took: {} seconds'.format(time.time() - t))
Actually took 0.00024 seconds
1 ответ
Разница в производительности объясняется одним ключевым отличием в "базовом срезании и индексации" от "расширенного индексирования", см. Эти документы. Ключевая линия здесь
Расширенная индексация всегда возвращает копию данных (в отличие от базовой нарезки, которая возвращает представление).
Насколько сильно болит копия, видно из сравнения fp_read[:,5000:6000:200]
против fp_read[:,5000:6000:200].copy()
,
Хотя создание копии массива всегда будет медленнее, чем создание нового представления, это особенно плохо для memmap:
- Чтение с диска относительно медленное. Данные должны быть прочитаны с диска, чтобы сделать (в памяти) копию, в то время как представление вообще не должно читать никаких данных! Существует просто новый объект ndarray, созданный с новыми параметрами смещения и шага (шага) для буфера памяти.
- Макет памяти ваших данных имеет порядок основных строк (по сравнению с основными столбцами, см. Википедию). Для доступа к случайным столбцам это означает, что сектор должен быть прочитан с диска для каждого отдельного значения данных. Сравните это с непрерывным доступом, где вы читаете только один сектор для каждых 256 значений (в предположении float16 и 512 байтовых секторов). При отображении в память io этот эффект еще хуже, потому что тогда данные читаются в блоках (страницах памяти) размером 4 КБ, то есть в секторах 8 x 512 байт.
Теперь мы также можем понять, почему результаты этого времени не являются действительно репрезентативными: эта конкретная часть файла кэшируется ОС в памяти.