web-dev-qa-db-ja.com

python concurrent.futures.ProcessPoolExecutor:.submit()と.map()のパフォーマンス

_concurrent.futures.ProcessPoolExecutor_を使用して、数値範囲から数値の出現を検索しています。その目的は、同時実行によって得られるスピードアップパフォーマンスの量を調査することです。パフォーマンスをベンチマークするために、私はコントロールを持っています-上記のタスクを実行するためのシリアルコード(下に表示)同じタスクを実行するためにconcurrent.futures.ProcessPoolExecutor.submit()を使用するコードとconcurrent.futures.ProcessPoolExecutor.map()を使用するコードの2つの並行コードを作成しました。それらを以下に示します。前者と後者の起草に関するアドバイスは、それぞれ herehere を参照してください。

3つのコードすべてに発行されたタスクは、0から1E8の数値範囲で数値5の発生数を見つけることでした。 .submit().map()の両方に6つのワーカーが割り当てられ、.map()のチャンクサイズは10,000でした。ワークロードを離散化する方法は、並行コードでも同じでした。ただし、両方のコードでオカレンスを見つけるために使用される関数は異なりました。これは、.submit().map()によって呼び出される関数に引数が渡される方法が異なっていたためです。

3つのコードすべてが同じ回数、つまり56,953,279回発生したことを報告しました。ただし、タスクを完了するのにかかる時間は大きく異なりました。 .submit()は、コントロールの2倍の速度で動作しましたが、.map()は、コントロールがタスクを完了するのに2倍の時間がかかりました。

質問:

  1. .map()のパフォーマンスの低下がコーディングのアーチファクトなのか、それとも本質的に遅いのか知りたいのですが。」それを使用するインセンティブはほとんどないので、制御します。
  2. とにかく.submit()コードをさらに高速に実行できるかどうかを知りたいです。私が持っている条件は、関数_concurrent_submit()が、数値/オカレンスに数値5を含むイテラブルを返す必要があることです。

ベンチマーク結果
benchmark results

concurrent.futures.ProcessPoolExecutor.submit()

_#!/usr/bin/python3.5
# -*- coding: utf-8 -*-

import concurrent.futures as cf
from time import time
from traceback import print_exc

def _findmatch(nmin, nmax, number):
    '''Function to find the occurrence of number in range nmin to nmax and return
       the found occurrences in a list.'''
    print('\n def _findmatch', nmin, nmax, number)
    start = time()
    match=[]
    for n in range(nmin, nmax):
        if number in str(n):
            match.append(n)
    end = time() - start
    print("found {0} in {1:.4f}sec".format(len(match),end))
    return match

def _concurrent_submit(nmax, number, workers):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.submit to
       find the occurences of a given number in a number range in a parallelised
       manner.'''
    # 1. Local variables
    start = time()
    chunk = nmax // workers
    futures = []
    found =[]
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        for i in range(workers):
            cstart = chunk * i
            cstop = chunk * (i + 1) if i != workers - 1 else nmax
            futures.append(executor.submit(_findmatch, cstart, cstop, number))
        # 2.2. Instruct workers to process results as they come, when all are
        #      completed or .....
        cf.as_completed(futures) # faster than cf.wait()
        # 2.3. Consolidate result as a list and return this list.
        for future in futures:
            for f in future.result():
                try:
                    found.append(f)
                except:
                    print_exc()
        foundsize = len(found)
        end = time() - start
        print('within statement of def _concurrent_submit():')
        print("found {0} in {1:.4f}sec".format(foundsize, end))
    return found

if __name__ == '__main__':
    nmax = int(1E8) # Number range maximum.
    number = str(5) # Number to be found in number range.
    workers = 6     # Pool of workers

    start = time()
    a = _concurrent_submit(nmax, number, workers)
    end = time() - start
    print('\n main')
    print('workers = ', workers)
    print("found {0} in {1:.4f}sec".format(len(a),end))
_

concurrent.futures.ProcessPoolExecutor.map()

_#!/usr/bin/python3.5
# -*- coding: utf-8 -*-

import concurrent.futures as cf
import itertools
from time import time
from traceback import print_exc

def _findmatch(listnumber, number):    
    '''Function to find the occurrence of number in another number and return
       a string value.'''
    #print('def _findmatch(listnumber, number):')
    #print('listnumber = {0} and ref = {1}'.format(listnumber, number))
    if number in str(listnumber):
        x = listnumber
        #print('x = {0}'.format(x))
        return x 

def _concurrent_map(nmax, number, workers):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.map to
       find the occurrences of a given number in a number range in a parallelised
       manner.'''
    # 1. Local variables
    start = time()
    chunk = nmax // workers
    futures = []
    found =[]
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        for i in range(workers):
            cstart = chunk * i
            cstop = chunk * (i + 1) if i != workers - 1 else nmax
            numberlist = range(cstart, cstop)
            futures.append(executor.map(_findmatch, numberlist,
                                        itertools.repeat(number),
                                        chunksize=10000))
        # 2.3. Consolidate result as a list and return this list.
        for future in futures:
            for f in future:
                if f:
                    try:
                        found.append(f)
                    except:
                        print_exc()
        foundsize = len(found)
        end = time() - start
        print('within statement of def _concurrent(nmax, number):')
        print("found {0} in {1:.4f}sec".format(foundsize, end))
    return found

if __name__ == '__main__':
    nmax = int(1E8) # Number range maximum.
    number = str(5) # Number to be found in number range.
    workers = 6     # Pool of workers

    start = time()
    a = _concurrent_map(nmax, number, workers)
    end = time() - start
    print('\n main')
    print('workers = ', workers)
    print("found {0} in {1:.4f}sec".format(len(a),end))
_

シリアルコード:

_#!/usr/bin/python3.5
# -*- coding: utf-8 -*-

from time import time

def _serial(nmax, number):    
    start = time()
    match=[]
    nlist = range(nmax)
    for n in nlist:
        if number in str(n):match.append(n)
    end=time()-start
    print("found {0} in {1:.4f}sec".format(len(match),end))
    return match

if __name__ == '__main__':
    nmax = int(1E8) # Number range maximum.
    number = str(5) # Number to be found in number range.

    start = time()
    a = _serial(nmax, number)
    end = time() - start
    print('\n main')
    print("found {0} in {1:.4f}sec".format(len(a),end))
_

2017年2月13日更新:

@niemmiの回答に加えて、私はいくつかの個人的な調査に基づいて回答を提供しています。

  1. @niemmiの.map()および.submit()ソリューションをさらに高速化する方法、および
  2. ProcessPoolExecutor.map()ProcessPoolExecutor.submit()よりも高速化できる場合。
16
Sun Bear

概要:

私の答えには2つの部分があります。

  • パート1では、@ niemmiのProcessPoolExecutor.map()ソリューションからさらに高速化する方法を示します。
  • パート2は、ProcessPoolExecutorのサブクラス.submit()および.map()が同等でない計算時間を生成する場合を示しています。

========================================= ==============================

パート1:ProcessPoolExecutor.map()の高速化

背景:このセクションは、@ niemmiの.map()ソリューションに基づいています。それが.map()チャンクサイズの議論とどのように相互作用するかをよりよく理解するために彼の離散化スキームについていくつかの調査を行っている間に、この興味深い解決策を見つけました。

@niemmiの_chunk = nmax // workers_の定義は、チャンクサイズの定義、つまり、ワーカープール内の各ワーカーが取り組む実際の数値範囲(与えられたタスク)の小さいサイズと見なします。ここで、この定義は、コンピューターにx個のワーカーがある場合、タスクを各ワーカー間で均等に分割すると、各ワーカーが最適に使用され、タスク全体が最も速く完了することを前提としています。したがって、特定のタスクを分割するチャンクの数は、常にプールワーカーの数と等しくなければなりません。しかし、この仮定は正しいですか?

命題:ここで、私は、上記の仮定がProcessPoolExecutor.map()で使用された場合に常に最速の計算時間につながるとは限らないことを提案します。むしろ、プールワーカーの数よりも多い量にタスクを離散化すると、スピードアップ、つまり特定のタスクの完了が速くなる可能性があります。

実験:離散化されたタスクの数がプールワーカーの数を超えることができるように、@ niemmiのコードを変更しました。このコードを以下に示し、0から1E8の数値範囲で数値5が出現する回数を見つけるために使用します。このコードは、1、2、4、6のプールワーカーを使用して、離散化されたタスクの数とプールワーカーの数のさまざまな比率で実行しました。各シナリオで3回の実行が行われ、計算時間が表にまとめられました。 「Speed-up」は、ここでは、離散化されたタスクの数が数よりも多い場合の平均計算時間にわたって、同じ数のチャンクとプールワーカーを使用した平均計算時間として定義されますプール労働者の。

調査結果:

nchunk over nworkers

  1. 左の図は、実験のセクションで言及したすべてのシナリオでかかる計算時間を示しています。それは、チャンク数/ワーカー数= 1チャンクの数>ワーカーの数がかかる計算時間よりも前者のケースは、後者のケースより常に効率が劣ります。

  2. 右の図は、チャンク数/ワーカー数がしきい値に達したときに、1.2倍以上の高速化が得られたことを示しています14以上。興味深いのは、ProcessPoolExecutor.map()が1ワーカーで実行されたときにも、高速化の傾向が見られたことです。

結論:ProcessPoolExecutor.map() `が特定のタスクを解決するために使用する個別のタスクの数をカスタマイズする場合、この数を確実にすることは賢明ですこれにより、計算時間が短縮されるため、プールワーカーの数よりも大きくなります。

concurrent.futures.ProcessPoolExecutor.map()コード。 (改造パーツのみ)

_def _concurrent_map(nmax, number, workers, num_of_chunks):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.map to
       find the occurrences of a given number in a number range in a parallelised
       manner.'''
    # 1. Local variables
    start = time()
    chunksize = nmax // num_of_chunks
    futures = []
    found =[]
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        cstart = (chunksize * i for i in range(num_of_chunks))
        cstop = (chunksize * i if i != num_of_chunks else nmax
                 for i in range(1, num_of_chunks + 1))
        futures = executor.map(_findmatch, cstart, cstop,
                               itertools.repeat(number))
        # 2.2. Consolidate result as a list and return this list.
        for future in futures:
            #print('type(future)=',type(future))
            for f in future:
                if f:
                    try:
                        found.append(f)
                    except:
                        print_exc()
        foundsize = len(found)
        end = time() - start
        print('\n within statement of def _concurrent(nmax, number):')
        print("found {0} in {1:.4f}sec".format(foundsize, end))
    return found

if __name__ == '__main__':
    nmax = int(1E8) # Number range maximum.
    number = str(5) # Number to be found in number range.
    workers = 4     # Pool of workers
    chunks_vs_workers = 14 # A factor of =>14 can provide optimum performance  
    num_of_chunks = chunks_vs_workers * workers

    start = time()
    a = _concurrent_map(nmax, number, workers, num_of_chunks)
    end = time() - start
    print('\n main')
    print('nmax={}, workers={}, num_of_chunks={}'.format(
          nmax, workers, num_of_chunks))
    print('workers = ', workers)
    print("found {0} in {1:.4f}sec".format(len(a),end))
_

========================================= ==============================

パート2:ProcessPoolExecutorサブクラス.submit()と.map()を使用した場合の合計計算時間は、並べ替え/順序付けされた結果リストを返すときに異なる場合があります

背景:「Apple-to-Apple」を許可するように.submit().map()の両方のコードを修正しました"それらの計算時間の比較と、メインコードの計算時間を視覚化する機能、メインコードによって呼び出された_concurrentメソッドの計算時間、同時操作を実行する、およびによって呼び出された各離散化タスク/ワーカーの計算時間_concurrentメソッド。さらに、これらのコードのコンカレントメソッドは、.submit()のfutureオブジェクトと.map()のイテレータから直接、結果の順序付けられていない順序付けされたリストを返すように構成されています。ソースコードを以下に示します(それがあなたを助けることを願っています。)。

実験これら2つの新しく改善されたコードは、パート1で説明した同じ実験を実行するために使用され、6つのプールワーカーのみが考慮され、python組み込みのlistおよびsortedメソッドを使用して、結果の順序付けられていないリストと順序付けられたリストをそれぞれコードのメインセクションに返しました。

調査結果:.submit vs .map plus list vs sorted

  1. _concurrentメソッドの結果から、ProcessPoolExecutor.submit()のすべてのFutureオブジェクトを作成し、ProcessPoolExecutor.map()のイテレータを作成するために使用される_concurrentメソッドの計算時間をプールワーカーの数に対する離散化タスクの数は同等です。この結果は単に、ProcessPoolExecutorサブクラス.submit().map()が同等に効率的/高速であることを意味します。
  2. Mainと_concurrentメソッドの計算時間を比較すると、mainが_concurrentメソッドよりも長く実行されていることがわかります。時間差はlistおよびsortedメソッドの計算時間(およびこれらのメソッドに含まれる他のメソッドの時間)を反映するため、これは予想されることです。明らかなように、listメソッドは、sortedメソッドよりも結果リストを返すのにかかる計算時間が短縮されました。 .submit()コードと.map()コードの両方でのlistメソッドの平均計算時間はほぼ同じで、約0.47秒でした。 .submit()および.map()コードのソートされたメソッドの平均計算時間は、それぞれ1.23秒および1.01秒でした。言い換えると、listメソッドは、.submit()コードおよび.map()コードに対して、それぞれsortedメソッドより2.62倍および2.15倍高速に実行されました。
  3. 離散化されたタスクの数がプールワーカーの数よりも増加したため、sortedメソッドが.map()からよりも速く.submit()から順序付きリストを生成した理由は明らかではありません、離散化されたタスクの数がプールワーカーの数と等しくなったときに保存します。とはいえ、これらの調査結果は、ソートされたメソッドによって、同等に高速な.submit()または.map()サブクラスを使用する決定が妨げられる可能性があることを示しています。たとえば、順序付けされたリストを可能な限り最短時間で生成することを意図している場合、ProcessPoolExecutor.submit()が合計を最短にすることができるため、.map()よりもProcessPoolExecutor.map()の使用をお勧めします。時間を計算します。
  4. .submit()および.map()サブクラスの両方のパフォーマンスを高速化するために、私の回答のパート1で述べた離散化スキームをここに示します。スピードアップの量は、離散化されたタスクの数がプールワーカーの数と等しい場合よりも20%も多くなる可能性があります。

.map()コードの改善

_#!/usr/bin/python3.5
# -*- coding: utf-8 -*-

import concurrent.futures as cf
from time import time
from itertools import repeat, chain 


def _findmatch(nmin, nmax, number):
    '''Function to find the occurence of number in range nmin to nmax and return
       the found occurences in a list.'''
    start = time()
    match=[]
    for n in range(nmin, nmax):
        if number in str(n):
            match.append(n)
    end = time() - start
    #print("\n def _findmatch {0:<10} {1:<10} {2:<3} found {3:8} in {4:.4f}sec".
    #      format(nmin, nmax, number, len(match),end))
    return match

def _concurrent(nmax, number, workers, num_of_chunks):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.map to
       find the occurrences of a given number in a number range in a concurrent
       manner.'''
    # 1. Local variables
    start = time()
    chunksize = nmax // num_of_chunks
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        cstart = (chunksize * i for i in range(num_of_chunks))
        cstop = (chunksize * i if i != num_of_chunks else nmax
                 for i in range(1, num_of_chunks + 1))
        futures = executor.map(_findmatch, cstart, cstop, repeat(number))
    end = time() - start
    print('\n within statement of def _concurrent_map(nmax, number, workers, num_of_chunks):')
    print("found in {0:.4f}sec".format(end))
    return list(chain.from_iterable(futures)) #Return an unordered result list
    #return sorted(chain.from_iterable(futures)) #Return an ordered result list

if __name__ == '__main__':
    nmax = int(1E8) # Number range maximum.
    number = str(5) # Number to be found in number range.
    workers = 6     # Pool of workers
    chunks_vs_workers = 30 # A factor of =>14 can provide optimum performance 
    num_of_chunks = chunks_vs_workers * workers

    start = time()
    found = _concurrent(nmax, number, workers, num_of_chunks)
    end = time() - start
    print('\n main')
    print('nmax={}, workers={}, num_of_chunks={}'.format(
          nmax, workers, num_of_chunks))
    #print('found = ', found)
    print("found {0} in {1:.4f}sec".format(len(found),end))    
_

.submit()コードの改善
このコードは、_concurrentメソッドを次のものに置き換えることを除いて、.mapコードと同じです。

_def _concurrent(nmax, number, workers, num_of_chunks):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.submit to
       find the occurrences of a given number in a number range in a concurrent
       manner.'''
    # 1. Local variables
    start = time()
    chunksize = nmax // num_of_chunks
    futures = []
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        for i in range(num_of_chunks):
            cstart = chunksize * i
            cstop = chunksize * (i + 1) if i != num_of_chunks - 1 else nmax
            futures.append(executor.submit(_findmatch, cstart, cstop, number))
    end = time() - start
    print('\n within statement of def _concurrent_submit(nmax, number, workers, num_of_chunks):')
    print("found in {0:.4f}sec".format(end))
    return list(chain.from_iterable(f.result() for f in cf.as_completed(
        futures))) #Return an unordered list
    #return list(chain.from_iterable(f.result() for f in cf.as_completed(
    #    futures))) #Return an ordered list
_

========================================= ==============================

7
Sun Bear

ここでリンゴとオレンジを比較しています。 mapを使用する場合、すべての1E8番号を生成し、それらをワーカープロセスに転送します。これは実際の実行に比べて多くの時間がかかります。 submitを使用する場合、転送されるパラメーターの6つのセットを作成するだけです。

mapを同じ原理で動作するように変更すると、互いに近い数値が得られます。

def _findmatch(nmin, nmax, number):
    '''Function to find the occurrence of number in range nmin to nmax and return
       the found occurrences in a list.'''
    print('\n def _findmatch', nmin, nmax, number)
    start = time()
    match=[]
    for n in range(nmin, nmax):
        if number in str(n):
            match.append(n)
    end = time() - start
    print("found {0} in {1:.4f}sec".format(len(match),end))
    return match

def _concurrent_map(nmax, number, workers):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.map to
       find the occurrences of a given number in a number range in a parallelised
       manner.'''
    # 1. Local variables
    start = time()
    chunk = nmax // workers
    futures = []
    found =[]
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        cstart = (chunk * i for i in range(workers))
        cstop = (chunk * i if i != workers else nmax for i in range(1, workers + 1))
        futures = executor.map(_findmatch, cstart, cstop, itertools.repeat(number))

        # 2.3. Consolidate result as a list and return this list.
        for future in futures:
            for f in future:
                try:
                    found.append(f)
                except:
                    print_exc()
        foundsize = len(found)
        end = time() - start
        print('within statement of def _concurrent(nmax, number):')
        print("found {0} in {1:.4f}sec".format(foundsize, end))
    return found

as_completed を正しく使用すると、送信のパフォーマンスを向上させることができます。未来のイテラブルが指定されている場合、yield先物を完了する順序で返すイテレータを返します。

別の配列へのデータのコピーをスキップし、 itertools.chain.from_iterable を使用して、futureからの結果を単一の反復可能に結合することもできます。

import concurrent.futures as cf
import itertools
from time import time
from traceback import print_exc
from itertools import chain

def _findmatch(nmin, nmax, number):
    '''Function to find the occurrence of number in range nmin to nmax and return
       the found occurrences in a list.'''
    print('\n def _findmatch', nmin, nmax, number)
    start = time()
    match=[]
    for n in range(nmin, nmax):
        if number in str(n):
            match.append(n)
    end = time() - start
    print("found {0} in {1:.4f}sec".format(len(match),end))
    return match

def _concurrent_map(nmax, number, workers):
    '''Function that utilises concurrent.futures.ProcessPoolExecutor.map to
       find the occurrences of a given number in a number range in a parallelised
       manner.'''
    # 1. Local variables
    chunk = nmax // workers
    futures = []
    found =[]
    #2. Parallelization
    with cf.ProcessPoolExecutor(max_workers=workers) as executor:
        # 2.1. Discretise workload and submit to worker pool
        for i in range(workers):
            cstart = chunk * i
            cstop = chunk * (i + 1) if i != workers - 1 else nmax
            futures.append(executor.submit(_findmatch, cstart, cstop, number))

    return chain.from_iterable(f.result() for f in cf.as_completed(futures))

if __name__ == '__main__':
    nmax = int(1E8) # Number range maximum.
    number = str(5) # Number to be found in number range.
    workers = 6     # Pool of workers

    start = time()
    a = _concurrent_map(nmax, number, workers)
    end = time() - start
    print('\n main')
    print('workers = ', workers)
    print("found {0} in {1:.4f}sec".format(sum(1 for x in a),end))
2
niemmi