Python: многопроцессорная обработка занимает больше времени, чем последовательная, почему?

У меня есть этот код, он генерирует 2 000 000 точек, равномерно распределенных в ограничивающем прямоугольнике, и выполняет некоторые вычисления для разделения точек на основе некоторых критериев.

import numpy as np

from draw import draw
import time

X = 0
Y = 1
N = 2000000

max_x = -100000
max_y = -100000
min_x = 100000
min_y = 100000

points = np.random.uniform(-10, 10, (N, 2))

start = time.time()
max_x_index = np.argmax(points[:, X])
max_y_index = np.argmax(points[:, Y])
min_x_index = np.argmin(points[:, X])
min_y_index = np.argmin(points[:, Y])

p_right = points[max_x_index]
p_top = points[max_y_index]
p_left = points[min_x_index]
p_bottom = points[min_y_index]

top_right = points[
points[:, X] > ((points[:, Y] - p_top[Y]) / (p_right[Y] - p_top[Y])) * (p_right[X] - p_top[X]) + p_top[X]]
top_left = points[
points[:, X] < ((points[:, Y] - p_top[Y]) / (p_left[Y] - p_top[Y])) * (p_left[X] - p_top[X]) + p_top[X]]
bottom_right = points[
points[:, X] > ((points[:, Y] - p_bottom[Y]) / (p_right[Y] - p_bottom[Y])) * (p_right[X] - p_bottom[X]) + p_bottom[
X]]
bottom_left = points[
points[:, X] < ((points[:, Y] - p_bottom[Y]) / (p_left[Y] - p_bottom[Y])) * (p_left[X] - p_bottom[X]) + p_bottom[X]]

end = time.time()
print(end - start)

Выход обычно равен 0,09, что соответствует секундам. Самый трудоемкий раздел — это последние четыре вычисления для получения значений top_right, top_left, Bottom_right и Bottom_left. Я переписываю код следующим образом

import numpy as np
from draw import draw
import time
import multiprocessing

N = 2000000
X = 0
Y = 1
points = np.random.uniform(-10, 10, (N, 2))

max_x = -100000
max_y = -100000
min_x = 100000
min_y = 100000

manager = multiprocessing.Manager()
top_right = manager.list()
top_left = manager.list()
bottom_right = manager.list()
bottom_left = manager.list()


def set_top_right():
    global X, Y, points, p_top, p_right, top_right
    top_right.extend(points[
        points[:, X] > ((points[:, Y] - p_top[Y]) / (p_right[Y] - p_top[Y])) * (p_right[X] - p_top[X]) + p_top[X]])


def set_top_left():
    global X, Y, points, p_top, p_left, top_left
    top_left.extend(points[
        points[:, X] < ((points[:, Y] - p_top[Y]) / (p_left[Y] - p_top[Y])) * (p_left[X] - p_top[X]) + p_top[X]])


def set_bottom_right():
    global X, Y, points, p_bottom, p_right, bottom_right
    bottom_right.extend(points[
        points[:, X] > ((points[:, Y] - p_bottom[Y]) / (p_right[Y] - p_bottom[Y])) * (p_right[X] - p_bottom[X]) +
        p_bottom[X]])


def set_bottom_left():
    global X, Y, points, p_bottom, p_left, bottom_left
    bottom_left.extend(points[
        points[:, X] < ((points[:, Y] - p_bottom[Y]) / (p_left[Y] - p_bottom[Y])) * (p_left[X] - p_bottom[X]) +
        p_bottom[X]])


start = time.time()
max_x_index = np.argmax(points[:, X])
max_y_index = np.argmax(points[:, Y])
min_x_index = np.argmin(points[:, X])
min_y_index = np.argmin(points[:, Y])

p_right = points[max_x_index]
p_top = points[max_y_index]
p_left = points[min_x_index]
p_bottom = points[min_y_index]

p1 = multiprocessing.Process(target=set_top_right)
p2 = multiprocessing.Process(target=set_top_left)
p3 = multiprocessing.Process(target=set_bottom_right)
p4 = multiprocessing.Process(target=set_bottom_left)

p1.start()
p2.start()
p3.start()
p4.start()

p1.join()
p2.join()
p3.join()
p4.join()

end = time.time()

print(end - start)

и на удивление стало хуже, примерно на 0,15 секунды. Я почти новичок в Python, однако думаю, что оба подхода представляют собой один поток и нет ввода-вывода. операции. Процессор моего ноутбука core i5 11-го поколения, и я ожидал, что каждое ядро ​​возьмет на себя один из процессов и сделает его быстрее. Тогда почему это медленнее?

Вы делали профилирование? Где узкое место?

jabaa 07.07.2024 22:34

@jabaa Да. p1.start() p2.start() p3.start() p4.start() занимает 0,14 секунды.

M a m a D 07.07.2024 22:46

Возможно, это накладные расходы на многопроцессорную обработку. Есть ли у вас данные профилирования для set_top_right, set_top_left, set_bottom_right и set_bottom_left? Если это ~ 0,01–0,02 секунды, вероятно, это накладные расходы.

jabaa 07.07.2024 23:01

Вы сравниваете общую память list с пустым ndarray. Это требует огромных затрат на сериализацию. Честно говоря, спасибо реализации общей памяти за то, что она лишь немного медленнее.

Homer512 07.07.2024 23:15

Что мне тогда делать? Как я уже сказал, я новичок в Python

M a m a D 08.07.2024 00:06

Почему вы хотите использовать многопроцессорность для приложения, которое работает 0,09 секунды?

jabaa 08.07.2024 00:17

Это первый шаг алгоритма предварительной обработки. Математически алгоритм работает за O(n), мне нужна лучшая реализация. Поэтому я стараюсь сделать каждый шаг как можно лучше.

M a m a D 08.07.2024 00:23

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

juanpa.arrivillaga 08.07.2024 01:03

работа с numpy часто на порядки быстрее, чем работа с list, особенно с manager.List, который требует сериализации и десериализации. Обратите внимание: вы по-прежнему копируете points и все остальные состояния в каждом подпроцессе. Использование manager.List вряд ли победит серийный номер.

juanpa.arrivillaga 08.07.2024 01:07

@juanpa.arrivillaga Могу ли я разделить массив между процессами? Я думаю, что это решит проблему

M a m a D 08.07.2024 02:10

@MamaD да, потенциально, особенно с numpy, это возможно. Однако я сверился с библиотекой под названием numexpr, на которую вам следует обратить внимание. Смотри мой

juanpa.arrivillaga 08.07.2024 02:21

numpy уже выполняет распараллеливание, и все это происходит внутри одного процесса (избегая затрат на перемещение данных через границы процесса, что, как правило, важно). Когда у вас есть задание, которое numpy может распараллелить, вы почти никогда не захотите использовать многопроцессорность, которая требует гораздо больше накладных расходов. (многопроцессорность — это не серебряный молоток, она ускоряет работу только в очень узких, специфических случаях). А совместное использование как numpy, так и многопроцессорной обработки означает, что вы получаете конкуренцию за процессор, замедляющую ваше работу, поскольку каждый из них пытается использовать все ваши ядра, а это означает, что в конечном итоге у вас будет больше требований, чем у вас есть процессоры.

Charles Duffy 08.07.2024 02:43

@CharlesDuffy, это зависит. Но об этом определенно стоит помнить.

juanpa.arrivillaga 08.07.2024 02:44

@juanpa.arrivillaga, честно, отредактировано, добавлено «почти». :)

Charles Duffy 08.07.2024 02:45

(другими словами: многопроцессорность — это инструмент, специально предназначенный для решения проблемы глобальной блокировки интерпретатора, предотвращающей одновременное изменение состояния интерпретатора Python несколькими потоками, но часть смысла numpy заключается в том, что он реализован на C и не подлежит конкуренции за GIL между различными потоками, которые сам numpy порождает как часть одной операции, поэтому, когда вы используете numpy корректным и подходящим способом, вы, как правило, не сталкиваетесь с узким местом из-за GIL, поэтому вы; оплатить затраты на многопроцессорную обработку, но не получить никакой выгоды).

Charles Duffy 08.07.2024 02:49

@CharlesDuffy Все это очень верно, и, конечно, первый шаг — узнать, можете ли вы поэкспериментировать со своим numpy-дистрибутивом (например, использовать Intel VML), хотя вы можете использовать numpy и multiprodessing.shared_memory.SharedMemory, чтобы использовать numpy.ndarray, который напрямую использует общую память. . Вот интересный вопрос/ответ, который касается различных способов работы numpy (а также рассказывает о numba и numexpr)

juanpa.arrivillaga 08.07.2024 02:53
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
16
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Многопроцессорная обработка с помощью multiprocessing.Manager.List не будет быстрее, вы понесете накладные расходы IPC из-за копирования состояния. Теперь вы, возможно, сможете эффективно использовать multiprocessing.SharedMemory с массивами numpy, однако я бы также предложил попробовать numexpr, который должен помочь в этом конкретном случае использования множества операций, создающих ненужные промежуточные массивы. Вот простой тест:

import numpy as np
import numexpr as ne

import time


X = 0
Y = 1
N = 2000000

max_x = -100000
max_y = -100000
min_x = 100000
min_y = 100000

points = np.random.uniform(-10, 10, (N, 2))

max_x_index = np.argmax(points[:, X])
max_y_index = np.argmax(points[:, Y])
min_x_index = np.argmin(points[:, X])
min_y_index = np.argmin(points[:, Y])

p_right = points[max_x_index]
p_top = points[max_y_index]
p_left = points[min_x_index]
p_bottom = points[min_y_index]

start = time.perf_counter()

def pure_numpy():
    top_right = points[
        points[:, X] > ((points[:, Y] - p_top[Y]) / (p_right[Y] - p_top[Y])) * (p_right[X] - p_top[X]) + p_top[X]
    ]
    top_left = points[
        points[:, X] < ((points[:, Y] - p_top[Y]) / (p_left[Y] - p_top[Y])) * (p_left[X] - p_top[X]) + p_top[X]
    ]
    bottom_right = points[
        points[:, X] > ((points[:, Y] - p_bottom[Y]) / (p_right[Y] - p_bottom[Y])) * (p_right[X] - p_bottom[X]) + p_bottom[X]
    ]
    bottom_left = points[
        points[:, X] < ((points[:, Y] - p_bottom[Y]) / (p_left[Y] - p_bottom[Y])) * (p_left[X] - p_bottom[X]) + p_bottom[X]
    ]

for _ in range(100):
    pure_numpy()

print(time.perf_counter() - start)

start = time.perf_counter()

def using_numexpr():
    top_right = points[
        ne.evaluate(
            # points[:, X] > ((points[:, Y] - p_top[Y]) / (p_right[Y] - p_top[Y])) * (p_right[X] - p_top[X]) + p_top[X]
            "pointsX > ((pointsY - p_topY) / (p_rightY - p_topY)) * (p_rightX - p_topX) + p_topX",
            dict(pointsX=points[:, X], pointsY=points[:, Y], p_topY=p_top[Y], p_rightY=p_right[Y], p_rightX=p_right[X], p_topX=p_top[X])
        )
    ]
    top_left = points[
        # points[:, X] < ((points[:, Y] - p_top[Y]) / (p_left[Y] - p_top[Y])) * (p_left[X] - p_top[X]) + p_top[X]
        ne.evaluate(
            "pointsX < ((pointsY - p_topY) / (p_leftY - p_topY)) * (p_leftX - p_topX) + p_topX",
            dict(pointsX=points[:, X], pointsY=points[:, Y], p_topY=p_top[Y], p_leftY=p_left[Y], p_leftX=p_left[X], p_topX=p_top[X])
        )
    ]
    bottom_right = points[
        # points[:, X] > ((points[:, Y] - p_bottom[Y]) / (p_right[Y] - p_bottom[Y])) * (p_right[X] - p_bottom[X]) + p_bottom[X]
        ne.evaluate(
            "pointsX > ((pointsY - p_bottomY) / (p_rightY - p_bottomY)) * (p_rightX - p_bottomX) + p_bottomX",
            dict(pointsX=points[:, X], pointsY=points[:, Y], p_bottomY=p_bottom[Y], p_rightY=p_right[Y], p_rightX=p_right[X], p_bottomX=p_bottom[X])
        )
    ]
    bottom_left = points[
        #points[:, X] < ((points[:, Y] - p_bottom[Y]) / (p_left[Y] - p_bottom[Y])) * (p_left[X] - p_bottom[X]) + p_bottom[X]
        ne.evaluate(
            "pointsX < ((pointsY - p_bottomY) / (p_leftY - p_bottomY)) * (p_leftX - p_bottomX) + p_bottomX",
            dict(pointsX=points[:, X], pointsY=points[:, Y], p_bottomY=p_bottom[Y], p_leftY=p_left[Y], p_leftX=p_left[X], p_bottomX=p_bottom[X])
            
        )
    ]

for _ in range(100):
    using_numexpr()

print(time.perf_counter() - start)

Я получаю такие результаты:

(py312) Juans-MBP:foo juan$ python testing.py
10.898050098097883
7.235401042038575

Так стабильно быстрее.

Я использую свой 10-летний ноутбук только с 4 логическими ядрами, numexpr использует несколько ядер, он мог бы работать лучше на более качественной машине. Вы также можете изучить различные способы повышения производительности, например, используя библиотеку векторных математических вычислений Intel.

Если вы хотите использовать multiprocessing, вы можете использовать multiprocessing.shared_memory, но для восстановления объекта массива numpy из общей памяти требуется определенная осторожность (отслеживание типов и формы). См. этот ответ на другой вопрос, конкретно о том, как это сделать

Это заняло всего 0,03 секунды. Большой! Спасибо. Не могли бы вы порекомендовать хорошие ссылки на метод, который вы использовали? Это только первая часть алгоритма в области вычислительной геометрии в информатике, и есть еще много оставшихся частей, которые мне нужно реализовать с помощью многопроцессорной обработки, поэтому мне нужно изучить

M a m a D 08.07.2024 10:08

@MamaD Я не использовал многопроцессорность, я использовал numexpr специфичную для numpy библиотеку, я дал ссылку на руководство пользователя для numexpr библиотеки выше, но вот это снова. Я только что перевел ваш код напрямую. Подробнее о библиотеке читайте по ссылке. Он в основном предназначен для этого конкретного случая использования.

juanpa.arrivillaga 08.07.2024 10:34

Алгоритм должен рекурсивно повторить аналогичный процесс для точек в четырех регионах top_right, top_left, bottom_right, bottom_left. Рекомендуете ли вы мне использовать многопроцессорность для такого рекурсивного процесса?

M a m a D 08.07.2024 15:25

@MamaD в этом вопросе рекомендуется использовать numexpr, а не многопроцессорность. Мультиобработка — это вообще другой подход, который вам определенно не следует совмещать с numpexpr, потому что numexpr уже задействует все ядра. многопроцессорная обработка с общей памятью немного сложна, но осуществима. Хотя вам придется протестировать, чтобы сравнить его с этим numexpr подходом.

juanpa.arrivillaga 08.07.2024 22:04

Другие вопросы по теме