Python, используя многопроцессорность медленнее, чем не используя его - PullRequest
9 голосов
/ 08 января 2012

Потратив много времени, пытаясь обернуть голову вокруг многопроцессорности, я пришел к этому коду, который является эталонным тестом:

Пример 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 секунды (половина времени многопоточности).Из любопытства я решил запустить следующее:

Пример 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 ]

11 голосов
/ 08 января 2012

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)

На моей машине многопроцессорная операция занимает всего ~ 60% времени однопоточной.

11 голосов
/ 08 января 2012

Этот пример слишком мал, чтобы извлечь выгоду из многопроцессорной обработки.

Существует много накладных расходов при запуске нового процесса. Если бы была тяжелая обработка, это было бы пренебрежимо мало. Но ваш пример на самом деле не такой уж и интенсивный, и вы наверняка заметите накладные расходы.

Вы, вероятно, заметите большую разницу с реальными потоками, слишком плохой python (ну, CPython) имеет проблемы с многопоточным процессом, связанным с процессором.

10 голосов
/ 08 января 2012

Многопроцессорность может быть полезна для того, что вы делаете, но не так, как вы думаете об этом.Поскольку вы в основном выполняете вычисления для каждого члена списка, вы можете сделать это с помощью метода 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
0 голосов
/ 27 мая 2014

Эта тема была очень полезна!

Просто быстрое наблюдение за хорошим вторым кодом, предоставленным Дэвидом Робинсоном выше (ответил 8 января 12 в 5: 34), который был кодом, более подходящим для моих текущих потребностей.

В моем случае у меня были предыдущие записи времени выполнения целевой функции без многопроцессорной обработки.При использовании своего кода для реализации многопроцессорной функции его timefunc (multi) не отражал фактического времени multi, а скорее отображал время, затраченное на родительский элемент.

То, что я сделал, - это экстернализацияфункция синхронизации и время, которое я получил, выглядело больше, чем ожидалось:

 start = timefunc()
 multi()/single()
 elapsed = (timefunc()-start)/(--number of workers--)
 print(elapsed)

В моем случае с двойным ядром общее время, проведенное работниками 'x' с использованием целевой функции, было в два раза быстрее, чем выполнение простогоцикл for для целевой функции с итерациями 'x'.

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

...