Python, используя multiprocess медленнее, чем не используя его
потратив много времени, пытаясь обернуть голову вокруг многопроцессорной обработки, я придумал этот код, который является эталонным тестом:
Пример 1:
from multiprocessing import Process
class Alter(Process):
def __init__(self, word):
Process.__init__(self)
self.word = word
self.word2 = ''
def run(self):
# Alter string + test processing speed
for i in range(80000):
self.word2 = self.word2 + self.word
if __name__=='__main__':
# Send a string to be altered
thread1 = Alter('foo')
thread2 = Alter('bar')
thread1.start()
thread2.start()
# wait for both to finish
thread1.join()
thread2.join()
print(thread1.word2)
print(thread2.word2)
это завершается за 2 секунды (половина времени многопоточности). Из любопытства я решил запустить следующее:--4-->
Пример 2:
word2 = 'foo'
word3 = 'bar'
word = 'foo'
for i in range(80000):
word2 = word2 + word
word = 'bar'
for i in range(80000):
word3 = word3 + word
print(word2)
print(word3)
к моему ужасу, это произошло менее чем за полсекунды!
что здесь происходит? Я ожидал многопроцессорная обработка для более быстрого запуска - не должна ли она завершиться за половину времени примера 2, учитывая, что Пример 1-Это пример 2, разделенный на два процесса?
обновление:
после рассмотрения обратной связи Криса я включил "фактический" код, потребляющий наибольшее время процесса, и привел меня к рассмотрению многопроцессорной обработки:
self.ListVar = [[13379+ strings],[13379+ strings],
[13379+ strings],[13379+ strings]]
for b in range(len(self.ListVar)):
self.list1 = []
self.temp = []
for n in range(len(self.ListVar[b])):
if not self.ListVar[b][n] in self.temp:
self.list1.insert(n, self.ListVar[b][n] + '(' +
str(self.ListVar[b].count(self.ListVar[b][n])) +
')')
self.temp.insert(0, self.ListVar[b][n])
self.ListVar[b] = list(self.list1)
4 ответов
ETA: теперь, когда вы опубликовали свой код, я могу сказать вам, что есть простой способ сделать то, что вы делаете намного быстрее (>в 100 раз быстрее).
Я вижу, что вы добавляете частоту в скобках к каждому элементу в списке строк. Вместо подсчета всех элементов каждый раз (что, как вы можете подтвердить с помощью cProfile, является самым большим узким местом в вашем коде), вы можете просто создать словарь это сопоставляет каждый элемент с его частотой. Что таким образом, вам нужно только пройти через список дважды - один раз, чтобы создать частотный словарь, один раз, чтобы использовать его для добавления частоты.
здесь я покажу свой новый метод, время и сравните его со старым методом, используя сгенерированный тестовый случай. Тестовый случай даже показывает, что новый результат будет ровно идентично старому. Примечание: все, что вам действительно нужно обратить внимание ниже, это new_method.
import random
import time
import collections
import cProfile
LIST_LEN = 14000
def timefunc(f):
t = time.time()
f()
return time.time() - t
def random_string(length=3):
"""Return a random string of given length"""
return "".join([chr(random.randint(65, 90)) for i in range(length)])
class Profiler:
def __init__(self):
self.original = [[random_string() for i in range(LIST_LEN)]
for j in range(4)]
def old_method(self):
self.ListVar = self.original[:]
for b in range(len(self.ListVar)):
self.list1 = []
self.temp = []
for n in range(len(self.ListVar[b])):
if not self.ListVar[b][n] in self.temp:
self.list1.insert(n, self.ListVar[b][n] + '(' + str(self.ListVar[b].count(self.ListVar[b][n])) + ')')
self.temp.insert(0, self.ListVar[b][n])
self.ListVar[b] = list(self.list1)
return self.ListVar
def new_method(self):
self.ListVar = self.original[:]
for i, inner_lst in enumerate(self.ListVar):
freq_dict = collections.defaultdict(int)
# create frequency dictionary
for e in inner_lst:
freq_dict[e] += 1
temp = set()
ret = []
for e in inner_lst:
if e not in temp:
ret.append(e + '(' + str(freq_dict[e]) + ')')
temp.add(e)
self.ListVar[i] = ret
return self.ListVar
def time_and_confirm(self):
"""
Time the old and new methods, and confirm they return the same value
"""
time_a = time.time()
l1 = self.old_method()
time_b = time.time()
l2 = self.new_method()
time_c = time.time()
# confirm that the two are the same
assert l1 == l2, "The old and new methods don't return the same value"
return time_b - time_a, time_c - time_b
p = Profiler()
print p.time_and_confirm()
когда я запускаю это, он получает время (15.963812112808228, 0.05961179733276367), что означает, что это примерно в 250 раз быстрее, хотя это преимущество зависит как от длины списков, так и от частотного распределения в каждом списке. Я уверен, вы согласитесь, что с этим преимуществом скорости вам, вероятно, не нужно будет использовать многопроцессорную обработку :)
(мой первоначальный ответ оставлен ниже для потомков)
ETA: кстати, стоит отметить, что этот алгоритм примерно линейный по длине списков, в то время как код вы использовали квадратичный. Это означает, что он выполняет с еще большим преимуществом, чем больше количество элементов. Например, если увеличить длину каждого списка до 1000000, то для запуска потребуется всего 5 секунд. Основываясь на экстраполяции, старый код займет день:)
Это зависит от операции, которую вы выполняете. Например:
import time
NUM_RANGE = 100000000
from multiprocessing import Process
def timefunc(f):
t = time.time()
f()
return time.time() - t
def multi():
class MultiProcess(Process):
def __init__(self):
Process.__init__(self)
def run(self):
# Alter string + test processing speed
for i in xrange(NUM_RANGE):
a = 20 * 20
thread1 = MultiProcess()
thread2 = MultiProcess()
thread1.start()
thread2.start()
thread1.join()
thread2.join()
def single():
for i in xrange(NUM_RANGE):
a = 20 * 20
for i in xrange(NUM_RANGE):
a = 20 * 20
print timefunc(multi) / timefunc(single)
на моей машине, операция multiprocessed занимает только ~60% время однопоточная одна.
этот пример слишком мал, чтобы извлечь выгоду из многопроцессорность.
есть много накладных расходов при запуске нового процесса. Если бы это было связано с тяжелой обработкой, это было бы незначительно. Но ваш пример действительно не так интенсивен, и поэтому вы обязательно заметите накладные расходы.
вы, вероятно, заметили бы большую разницу с реальными потоками, слишком плохой python (ну, CPython) имеет проблемы с потоками, связанными с процессором.
многопроцессорная обработка может быть полезна для того, что вы делаете, но не так, как вы думаете об ее использовании. Поскольку вы в основном делаете некоторые вычисления для каждого члена списка, вы можете сделать это с помощью multiprocessing.Pool.map
метод, чтобы выполнить вычисление по элементам списка параллельно.
вот пример, который показывает производительность вашего кода с помощью одного процесса и с помощью multiprocessing.Pool.map
:
from multiprocessing import Pool
from random import choice
from string import printable
from time import time
def build_test_list():
# Builds a test list consisting of 5 sublists of 10000 strings each.
# each string is 20 characters long
testlist = [[], [], [], [], []]
for sublist in testlist:
for _ in xrange(10000):
sublist.append(''.join(choice(printable) for _ in xrange(20)))
return testlist
def process_list(l):
# the time-consuming code
result = []
tmp = []
for n in range(len(l)):
if l[n] not in tmp:
result.insert(n, l[n]+' ('+str(l.count(l[n]))+')')
tmp.insert(0, l[n])
return result
def single(l):
# process the test list elements using a single process
results = []
for sublist in l:
results.append(process_list(sublist))
return results
def multi(l):
# process the test list elements in parallel
pool = Pool()
results = pool.map(process_list, l)
return results
print "Building the test list..."
testlist = build_test_list()
print "Processing the test list using a single process..."
starttime = time()
singleresults = single(testlist)
singletime = time() - starttime
print "Processing the test list using multiple processes..."
starttime = time()
multiresults = multi(testlist)
multitime = time() - starttime
# make sure they both return the same thing
assert singleresults == multiresults
print "Single process: {0:.2f}sec".format(singletime)
print "Multiple processes: {0:.2f}sec".format(multitime)
выход:
Building the test list...
Processing the test list using a single process...
Processing the test list using multiple processes...
Single process: 34.73sec
Multiple processes: 24.97sec
этот поток был очень полезен!
просто быстрое наблюдение за хорошим второй код Дэвид Робинсон выше (ответил 8 января ' 12 в 5: 34), что было кодом, более подходящим для моих текущих потребностей.
в моем случае у меня были предыдущие записи времени выполнения целевой функции без многопроцессорной обработки. При использовании его кода для реализации многопроцессорной функции его timefunc (multi) не отражал фактическое время несколько, и это, скорее, отражало время, затраченное на родителя.
то, что я сделал, было экстернализировать функцию синхронизации и время, которое я получил, выглядело более ожидаемым:
start = timefunc()
multi()/single()
elapsed = (timefunc()-start)/(--number of workers--)
print(elapsed)
в моем случае с двойным ядром общее время, проведенное работниками "x", использующими целевую функцию, было в два раза быстрее, чем выполнение простого цикла for над целевой функцией с итерациями "x".
Я новичок в многопроцессорной обработке, поэтому, пожалуйста, будьте осторожны с этим хотя наблюдательность.