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".

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