Многопроцессорность Python

Это должен быть мой третий и последний вопрос, касающийся моих попыток повысить производительность по некоторому статистическому анализу, который я делаю с python. У меня есть 2 версии моего кода (одноядерный или многопроцессорный), я ожидал получить производительность, используя несколько ядер, поскольку я ожидаю, что мой код распакует/распаковывает несколько двоичных строк, к сожалению, я заметил, что производительность фактически уменьшилась за счет использования нескольких ядра.

Мне интересно, есть ли у кого-нибудь возможное объяснение того, что я наблюдаю (прокрутить вниз до обновления 16 апреля для получения дополнительной информации)?

Ключевой частью программы является функция numpy_array (+ декодирование при многопроцессорной обработке), фрагмент кода ниже (полный код, доступный через pastebin, далее):

def numpy_array(data, peaks):
    rt_counter=0
    for x in peaks:
        if rt_counter %(len(peaks)/20) == 0:
            update_progress()
        peak_counter=0
        data_buff=base64.b64decode(x)
        buff_size=len(data_buff)/4
        unpack_format=">%dL" % buff_size
        index=0
        for y in struct.unpack(unpack_format,data_buff):
            buff1=struct.pack("I",y)
            buff2=struct.unpack("f",buff1)[0]
            if (index % 2 == 0):
                data[rt_counter][1][peak_counter][0]=float(buff2)
            else:
                data[rt_counter][1][peak_counter][1]=float(buff2)
                peak_counter+=1
            index+=1
        rt_counter+=1

Многопроцессорная версия выполняет это с помощью набора функций, я покажу ключ 2 ниже:

def tonumpyarray(mp_arr):
    return np.frombuffer(mp_arr.get_obj())

def numpy_array(shared_arr,peaks):
    processors=mp.cpu_count()
    with contextlib.closing(mp.Pool(processes=processors,
                                    initializer=pool_init,
                                    initargs=(shared_arr, ))) as pool:
        chunk_size=int(len(peaks)/processors)
        map_parameters=[]
        for i in range(processors):
            counter = i*chunk_size
            chunk=peaks[i*chunk_size:(i+1)*chunk_size]
            map_parameters.append((chunk, counter))
        pool.map(decode,map_parameters)

def decode ((chunk, counter)):
    data=tonumpyarray(shared_arr).view(
        [('f0','<f4'), ('f1','<f4',(250000,2))])
    for x in chunk:
        peak_counter=0
        data_buff=base64.b64decode(x)
        buff_size=len(data_buff)/4
        unpack_format=">%dL" % buff_size
        index=0
        for y in struct.unpack(unpack_format,data_buff):
            buff1=struct.pack("I",y)
            buff2=struct.unpack("f",buff1)[0]
            #with shared_arr.get_lock():
            if (index % 2 == 0):
                data[counter][1][peak_counter][0]=float(buff2)
            else:
                data[counter][1][peak_counter][1]=float(buff2)
                peak_counter+=1
            index+=1
        counter+=1

Доступ к полным программным кодам можно получить через эти ссылки pastebin

Pastebin для одноядерной версии

Pastebin для многопроцессорной версии

Производительность, которую я наблюдаю с файлом, содержащим 239 временных точек и парами измерений ~ 180 тыс. за каждую точку времени, составляет ~ 2,5 м для одного ядра и ~ 3,5 для многопроцессорной обработки.

PS: два предыдущих вопроса (из моих первых попыток паралеллизации):

- 16 апреля -

Я профилировал свою программу с помощью библиотеки cProfile (с cProfile.run('main()') в __main__, которая показывает, что есть 1 шаг, который замедляет все:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
23   85.859    3.733   85.859    3.733 {method 'acquire' of 'thread.lock' objects}

То, что я не понимаю здесь, состоит в том, что thread.lock объекты используются в threading (насколько мне известно), но не должны использоваться при многопроцессорности, поскольку каждое ядро ​​должно запускать один поток (кроме того, у него есть собственный механизм блокировки), так как это происходит, и почему один вызов занимает 3,7 секунды?

Ответ 1

Общие данные - это известный случай замедлений из-за синхронизации.

Можете ли вы разделить свои данные между процессами или предоставить каждому процессу независимую копию? Тогда вашим процессам не нужно будет ничего синхронизировать до момента выполнения всех вычислений.

Затем я бы позволил мастер-процессу присоединить вывод всех рабочих процессоров к одному когерентному набору.

Подход может занять дополнительную оперативную память, но оперативная память сейчас дешевая.

Если вы спросите, я также озадачен 3700 мс на захват блокировки потоков. Профилирование OTOH может быть ошибочно принято в отношении таких специальных вызовов.

Ответ 2

Ваши пустые папки пусты.

Проблема заключается в том, что многопроцессор использует fork, если он доступен (вместо того, чтобы создавать новый python-процесс). Процесс Forked имеет один и тот же env (например, файловые дескрипторы). Может быть, у них есть некоторые замки среди них.

Вот некоторые расстройства: Многопроцессорность или os.fork, os.exec?

Ответ 3

Что касается последней части вашего вопроса, документы Python в основном говорят, что multiprocessing.lock является клоном threading.lock. Приобретение вызовов на замках может занять много времени, потому что если блокировка уже получена, она будет блокироваться до тех пор, пока блокировка не будет отпущена. Это может стать проблемой, когда несколько процессов конкурируют за доступ к тем же данным, как в вашем коде. Поскольку я не могу просмотреть ваш pastebin, я могу только догадываться, что именно происходит, но, скорее всего, вы процессы приобретаете блокировку в течение длительных периодов времени, что останавливает выполнение других процессов, даже если есть много свободное время процессора. Это не должно зависеть от GIL, поскольку это должно ограничивать только многопоточные приложения, а не многопроцессорные. Итак, как это исправить? Я предполагаю, что у вас есть какая-то блокировка, защищающая ваш общий массив, который остается заблокированным, в то время как процесс проводит интенсивные вычисления, которые занимают относительно длительное время, поэтому запрет доступа для других процессов, которые впоследствии блокируют их lock.acquire() звонки. Предполагая, что у вас достаточно ОЗУ, я решительно поддерживаю ответ, который предполагает сохранение нескольких копий массива в каждом адресном пространстве процесса. Однако обратите внимание, что передача больших структур данных через карту может вызвать неожиданные узкие места, так как требуется сбор и депиляция.