Передача метода большого объекта в imap: ускорение в 1000 раз за счет переноса метода - PullRequest
0 голосов
/ 16 октября 2018

Предположим, yo = Yo() - это большой объект с методом double, который возвращает свой параметр, умноженный на 2.

Если я передам yo.double в imap из multiprocessing, тогдаэто невероятно медленно, потому что каждый вызов функции создает копию yo Я думаю.

Т.е. это очень медленно:

from tqdm import tqdm
from multiprocessing import Pool
import numpy as np


class Yo:
    def __init__(self):
        self.a = np.random.random((10000000, 10))

    def double(self, x):
        return 2 * x

yo = Yo()    

with Pool(4) as p:
    for _ in tqdm(p.imap(yo.double, np.arange(1000))):
        pass

Вывод:

0it [00:00, ?it/s]
1it [00:06,  6.54s/it]
2it [00:11,  6.17s/it]
3it [00:16,  5.60s/it]
4it [00:20,  5.13s/it]

...

НО, если я оберну yo.double с помощью функции double_wrap и передам ее imap, то это будет по существу мгновенно.

def double_wrap(x):
    return yo.double(x)

with Pool(4) as p:
    for _ in tqdm(p.imap(double_wrap, np.arange(1000))):
        pass

Вывод:

0it [00:00, ?it/s]
1000it [00:00, 14919.34it/s]

Как и почему обертывание функции меняет поведение?

Я использую Python 3.6.6.

1 Ответ

0 голосов
/ 16 октября 2018

Вы правы насчет копирования.yo.double - это «связанный метод», связанный с вашим большим объектом.Когда вы передаете его в пул-метод, он извлекает весь экземпляр вместе с ним, отправляет его дочерним процессам и распаковывает его там.Это происходит для каждого куска итерируемого дочернего процесса.Значение по умолчанию для chunksize в pool.imap равно 1, поэтому вы используете эту коммуникационную нагрузку для каждого обработанного элемента в итерируемом.

Наоборот, когда вы передаете double_wrap, вы просто передаете модуль-функция уровня.Только его имя будет фактически засечено, а дочерние процессы импортируют функцию из __main__.Поскольку вы, очевидно, работаете в ОС, которая поддерживает разветвление, ваша функция double_wrap будет иметь доступ к разветвленному экземпляру yo Yo.В этом случае ваш большой объект не будет сериализован (протравлен), поэтому накладные расходы по связи незначительны по сравнению с другим подходом.


@ Darkonaut Я просто не понимаю, зачем делатьУровень функционального модуля предотвращает копирование объекта.В конце концов, функция должна иметь указатель на сам объект yo, что требует, чтобы все процессы копировали yo, поскольку они не могут совместно использовать память.

Функция, запущенная в дочернем процессе, автоматически найдетссылка на глобальный yo, потому что ваша операционная система (ОС) использует fork для создания дочернего процесса.Форкинг приводит к клонированию всего вашего родительского процесса, и пока ни родитель, ни потомок не изменят определенный объект, оба будут видеть один и тот же объект в одном и том же месте памяти.

Только если родитель или потомок что-то изменят вобъект, объект get копируется в дочерний процесс.Это называется «копирование при записи» и происходит на уровне операционной системы, когда вы не обращаете на это внимания в Python.Ваш код не будет работать в Windows, которая использует «spawn» в качестве метода запуска для новых процессов.

Теперь я упрощаю немного выше, где пишу «объект копируется», так как модуль ОСоперирует «страницей» (чаще всего это будет размер 4 КБ).Этот ответ здесь был бы хорошим продолжением для расширения вашего понимания.

...