Почему сохранение фрагментов из массива с помощью numpy.save происходит медленнее в зависимости от направления фрагмента? - PullRequest
3 голосов
/ 06 июня 2019

У меня есть трехмерные тома данных (x, y, z), и я хочу сохранить 2D-срезы (плоскости xy, yz, xz) и сохранить их для дальнейшего использования.

Я пытался сделать это, используя функцию (slice_data), чтобы взять кусочки, и еще одну (save_slices) для вызова slice_data, а затем использовать numpy.save для сохранения кусочков.

Если я не сохраняю срезы, время взятия срезов будет одинаковым, независимо от того, извлекаю ли я плоскости xy, yz, xz. Однако, если я сохраню срезы, время сохранения срезов зависит от направления среза и отличается для плоскостей xy, yz, xz

Почему это? Используя мои полные данные, эта разница идет от минут до часов ...

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

Не экономит, просто нарезает

mean time x-slice: 0.00011536836624145507 sec
mean time y-slice: 0.00011417627334594726 sec
mean time z-slice: 0.00011371374130249023 sec

Нарезка и сохранение:

mean time x-slice: 0.04629791975021362 sec
mean time y-slice: 0.06096100091934204 sec
mean time z-slice: 0.08996494293212891 sec

Код:

import os
import numpy as np
import time
import matplotlib.pyplot as plt

# take a slice of the data
def slice_data(roi):
    dic = {}
    data = np.zeros((512,512,256))
    dic['data'] = np.squeeze( data[roi[0]:roi[1]+1, roi[2]:roi[3]+1, roi[4]:roi[5]+1] )
    return dic


# save slices if the data
def save_slices(roi, save=False):
    var = 'data'
    for i in range(0,6):
                # iterate to simulate a time series of data
        a = slice_data(roi)[var]
        var_dir = 'save_test/'
        if not os.path.exists(var_dir): os.makedirs(var_dir)
        file = var_dir + '{0:04d}{1}'.format(i,'.npy')

        if save is True:
            np.save(file, a)


## define slices
roix=[256, 256, 0, 512, 0, 256] # yz plane slice
roiy=[0, 512, 256, 256, 0, 256] # xz plane slice
roiz=[0, 512, 0, 512, 128, 128] # xy plane slice

## Calculate slices and do not save the results
dtx = []
dty = []
dtz = []
for i in range(100):
    time0 = time.time()
    save_slices(roix)
    time1 = time.time()
    dtx.append(time1-time0)

    time0 = time.time()
    save_slices(roiy)
    time1 = time.time()
    dty.append(time1-time0)


    time0 = time.time()
    save_slices(roiz)
    time1 = time.time()
    dtz.append(time1-time0)

plt.figure(1)
plt.plot(dtx)
plt.plot(dty)
plt.plot(dtz)
plt.title('time to run code without saving data')

print('mean time x-slice: {} sec'.format(np.mean(dtx)))
print('mean time y-slice: {} sec'.format(np.mean(dty)))
print('mean time z-slice: {} sec'.format(np.mean(dtz)))


## Calculate slices and do save the results
dtx = []
dty = []
dtz = []
for i in range(100):
    time0 = time.time()
    save_slices(roix, save=True)
    time1 = time.time()
    dtx.append(time1-time0)

    time0 = time.time()
    save_slices(roiy, save=True)
    time1 = time.time()
    dty.append(time1-time0)


    time0 = time.time()
    save_slices(roiz, save=True)
    time1 = time.time()
    dtz.append(time1-time0)

plt.figure(2)
plt.plot(dtx)
plt.plot(dty)
plt.plot(dtz)
plt.title('time to run code and save data')

print('mean time x-slice: {} sec'.format(np.mean(dtx)))
print('mean time y-slice: {} sec'.format(np.mean(dty)))
print('mean time z-slice: {} sec'.format(np.mean(dtz)))

Ответы [ 2 ]

1 голос
/ 06 июня 2019

Краткий ответ

Только массив roix является c_contiguous. Таким образом, передача по шине из памяти в ЦП происходит быстрее, чем для несмежных данных (из-за того, что шина перемещает данные в чанке и кэширует их)

Вы можете получить небольшое улучшение (около 5% для роиза и 40% для роя), сделав его непрерывным C np.save(file, np.asarray(a, order='C'))

Больше объяснений

Профилирование

Вы должны использовать timeit для определения времени ваших выступлений вместо пользовательских методов.

Я сделал их, чтобы вы показали пример:

В клетку попали:

import os
import numpy as np
import time
import matplotlib.pyplot as plt

# take a slice of the data
def slice_data(roi):
    dic = {}
    data = np.zeros((512,512,256))
    dic['data'] = np.squeeze( data[roi[0]:roi[1]+1, roi[2]:roi[3]+1, roi[4]:roi[5]+1] )
    return dic


# save slices if the data
def save_slices(roi, save=False):
    var = 'data'
    for i in range(0,6):
                # iterate to simulate a time series of data
        a = slice_data(roi)[var]
        var_dir = 'save_test/'
        if not os.path.exists(var_dir): os.makedirs(var_dir)
        file = var_dir + '{0:04d}{1}'.format(i,'.npy')

        if save is True:
            np.save(file, a)


## define slices
roix=[256, 256, 0, 512, 0, 256] # yz plane slice
roiy=[0, 512, 256, 256, 0, 256] # xz plane slice
roiz=[0, 512, 0, 512, 128, 128] # xy plane slice

в других:

%%timeit -n 100
save_slices(roix) # 19.8 ms ± 285 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%%timeit -n 100
save_slices(roiy) # 20.5 ms ± 948 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%%timeit -n 100
save_slices(roiz) # 20 ms ± 345 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

С сохранением

%%timeit -n 10 -r 3
save_slices(roix, True) # 32.7 ms ± 2.31 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)

%%timeit -n 10 -r 3
save_slices(roiy, True) # 101 ms ± 2.61 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)

%%timeit -n 10 -r 3
save_slices(roix, True) # 1.9 s ± 21.1 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)

Итак, как вы уже заметили, без сохранения показатели одинаковы! Давайте познакомимся с методом np.save()

Np.save метод

np.save заботится о потоковой передаче io и вызывает метод write_array . Что действительно быстро для массива C_contigous. (Быстрый доступ к памяти)

Давайте проверим эту гипотезу:

np.squeeze( np.zeros((512,512,256))[roix[0]:roix[1]+1, roix[2]:roix[3]+1, roix[4]:roix[5]+1] ).flags.c_contiguous # returns True
np.squeeze( np.zeros((512,512,256))[roiy[0]:roiy[1]+1, roiy[2]:roiy[3]+1, roiy[4]:roiy[5]+1] ).flags.c_contiguous # returns False
np.squeeze( np.zeros((512,512,256))[roiz[0]:roiz[1]+1, roiz[2]:roiz[3]+1, roiz[4]:roiz[5]+1] ).flags.c_contiguous # returns False

Так что это может объяснить разницу между roix и roiy / roiz.

Потенциальное объяснение разницы между roiy и roiz. Передача данных замедляет работу программы

После этого я мог только делать предположения, roiz кажется гораздо более фрагментированным, чем roiy. Что занимает много времени для метода write_array.

Я не могу проверить это сам сейчас, но эту часть можно проверить с помощью команды perf в linux. (Чтобы увидеть количество использованных шин, например, количество пропущенных кешей). Если бы мне пришлось делать дикие предположения, я бы сказал, что ошибки кэширования довольно высоки из-за того, что данные не являются смежными. Таким образом, передача данных из ОЗУ в ЦП действительно замедляет процесс.

Другие способы обращения с хранилищем

Я не пробовал, но есть хороший вопрос с несколькими полезными ответами: лучший способ сохранить массивы на диске

1 голос
/ 06 июня 2019

Причина в том, что Numpy по умолчанию сохраняет данные в главном порядке строк.Если вы измените

data = np.zeros((512,512,256))

на

# order F means column major
data = np.zeros((512,512,256), order='F')

, вы увидите, что сохранение X-фрагментов займет больше всего времени.

Если вы собираетесь сохранить несколько срезов плоскости XY (когда вы изменяете координату Z), вы увидите лучшую производительность, переставив массив и скопировав его в , заставив новую памятьlayout. Это обеспечит соответствие макета памяти вашему шаблону доступа, что приведет к более быстрому чтению (и сохранению).Более подробное объяснение ниже.


Давайте возьмем в качестве примера следующую матрицу (из Numpy glossary ):

m = [[1, 2, 3],
     [4, 5, 6]]

Если это представлено в памяти впорядок старших строк (порядок C в numy lingo) раскладывается следующим образом:

[1, 2, 3, 4, 5, 6]

Если матрица представлена ​​в памяти в порядке старших столбцов (или F для порядка Fortran), она раскладываетсянапример:

[1, 4, 2, 5, 3, 6]

Теперь, если вы индексируете в этот массив с помощью m[:,2], вы получите [3, 6], а с m[1,:] вы получите [4, 5, 6].Если вы посмотрите на макеты памяти, вы увидите, что значения [3, 6] будут смежными в основном представлении столбца, а [4, 5, 6] будут смежными в основном представлении строки.

При чтении большого количества элементовиз массива (как при сохранении одного) гораздо эффективнее считывать эти значения непрерывно, поскольку это позволяет вам использовать кэш-память ЦП, которая на 1-2 порядка * на 1033 * быстрее чем чтение из памяти.

...