Pull to refresh

Параллельный Питон, начало

Reading time4 min
Views16K

Disclaimer


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

Практика


Собственно перевести алгоритм оказалось делом совсем несложным, но вот скорость его работы оставляла желать лучшего.
Первым делом в ход пошел Psyco, ускорив обработку в 6 раз.

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

Найден был модуль Parallel Python. Подключить его оказалось делом совсем несложным:

Сначала import pp, а потом (первый вариант):
ppservers = ()
job_server = pp.Server(ppservers=ppservers)                     

job_server.set_ncpus(2)
print "Starting pp with", job_server.get_ncpus(), "workers"

jobs = [job_server.submit(tighina_check, (), (find_geo_coords, compare, get_dist_bearing,), ("math", ))  for i in range(3)]

for job in jobs:
	job()

job_server.print_stats()

Код, в принципе, сам за себя говорящий — используем только локальный сервер (а вообще модуль позволяет распараллеливать и на сетевые), стараемся запустить на 2-х процессорах, указываем какую функцию вызывать и от каких она зависит, импортируем math и запускаем 3 задачи, в конце печатаем статистику.

Первой засадой оказалось отключение psyco, что опять отбросило нас на стартовую позицию.
Решение было очевидным — добавить импорт psyco при создании job'а
jobs = [job_server.submit(tighina_check, (), (find_geo_coords, compare, get_dist_bearing,), ("math", "psyco", )) for i in range(3)]
и вызывать psyco.full уже в tighina_check:
def tighina_check():
        psyco.full()
        #а вот тут много математики


Вторая проблема оказалась весьма неожиданной.
Код в tighina_check был изначально заточен под импорт вида «from math import sin, pow, cos, asin, sqrt, fabs, acos». Но он не работал под pp, т.к. создает среду выполнения функции только с модулями, указанными при создании job'а. Вполне логичным было переделать все вызовы sin на math.sin и т.д. Вот тут-то и возникло небольшое недоумение — интенсивное и постоянное использоваение мат.функций во втором формате вызова приводило к замедлению в 1.3-1.4 раза.

Решением было ручное импортирование нужных функций в глобальную область видимости в начале каждого job'a:
def tighina_check():
     psyco.full()
     math_func_imports = ['sin', 'cos', 'asin', 'acos', 'sqrt', 'pow', 'fabs']
     for func in math_func_imports:
	 setattr(__builtins__, func, getattr(math, func))


Дальше подумалось, что неплохо бы ускорить сам pp с помощью psyco. Для этого нужно немного подпатчить pyworker.py из комплекта, добавив в начало:
import psyco
psyco.full()


и заменив
eval(__fobj)
на
exec __fobj


При этом отпадает необходимость в импорте psyco при создании job'а и соответсвенно в вызове psyco.full() в job'e.

Остальное — только подборка нужного числа процессоров

Что в итоге?


Запускалось 100 job'ов.

Исходный вариант (никакого распараллеливания, только psycho)
100 последовательных job'ов 257 секунд

2 процессора (pp, psyco)
Starting pp with 2 workers
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
       100 |        100.00 |     389.8933 |     3.898933 | local
Time elapsed since server creation 195.12789011


4 процессора (pp, psyco)
Starting pp with 4 workers
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
       100 |        100.00 |     592.9463 |     5.929463 | local
Time elapsed since server creation 148.77167201


Дальше тестировать не хотелось, казалось, что 2 ядра, каждое с гипертредингом, а значит 4 job'а — оптимальный вариант. Но любопытство взяло вверх (и как оказалось — не зря):
8 процессоров (pp, psyco)
Starting pp with 8 workers
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
       100 |        100.00 |     1072.3920 |    10.723920 | local
Time elapsed since server creation 137.681350946


16 процессоров (pp, psyco)

Starting pp with 16 workers
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
       100 |        100.00 |     2050.8158 |    20.508158 | local
Time elapsed since server creation 133.345046043


32 процессора (pp, psyco)

Starting pp with 32 workers
Job execution statistics:
 job count | % of all jobs | job time sum | time per job | job server
       100 |        100.00 |     4123.8550 |    41.238550 | local
Time elapsed since server creation 136.022897005


Т.о. в лучшем варианте 133 секунды против 257 в первоначальном варианте = ускорение в 1.93 раза для нашей конкретной задачи только за счет распараллеливания.

Следует отметить, что все 100 job'ов друг от друга не зависят и не нуждаются в «общении» между собой, что облегчает задачу и увеличивает скорость.

Итоговые примеры кода:
ppservers = ()
job_server = pp.Server(ppservers=ppservers)                     

job_server.set_ncpus(16)
print "Starting pp with", job_server.get_ncpus(), "workers"

jobs = [job_server.submit(tighina_check, (), (find_geo_coords, compare, get_dist_bearing,), ("math", ))  for i in range(3)]

for job in jobs:
    job()

job_server.print_stats()


def tighina_check():
    math_func_imports = ['sin', 'cos', 'asin', 'acos', 'sqrt', 'pow', 'fabs']
    for func in math_func_imports:
        setattr(__builtins__, func, getattr(math, func)) 

        #а вот тут много математики
Tags:
Hubs:
Total votes 47: ↑44 and ↓3+41
Comments37

Articles