_concurrent.futures.ProcessPoolExecutor
_を使用して、数値範囲から数値の出現を検索しています。その目的は、同時実行によって得られるスピードアップパフォーマンスの量を調査することです。パフォーマンスをベンチマークするために、私はコントロールを持っています-上記のタスクを実行するためのシリアルコード(下に表示)同じタスクを実行するためにconcurrent.futures.ProcessPoolExecutor.submit()
を使用するコードとconcurrent.futures.ProcessPoolExecutor.map()
を使用するコードの2つの並行コードを作成しました。それらを以下に示します。前者と後者の起草に関するアドバイスは、それぞれ here と here を参照してください。
3つのコードすべてに発行されたタスクは、0から1E8の数値範囲で数値5の発生数を見つけることでした。 .submit()
と.map()
の両方に6つのワーカーが割り当てられ、.map()
のチャンクサイズは10,000でした。ワークロードを離散化する方法は、並行コードでも同じでした。ただし、両方のコードでオカレンスを見つけるために使用される関数は異なりました。これは、.submit()
と.map()
によって呼び出される関数に引数が渡される方法が異なっていたためです。
3つのコードすべてが同じ回数、つまり56,953,279回発生したことを報告しました。ただし、タスクを完了するのにかかる時間は大きく異なりました。 .submit()
は、コントロールの2倍の速度で動作しましたが、.map()
は、コントロールがタスクを完了するのに2倍の時間がかかりました。
質問:
.map()
のパフォーマンスの低下がコーディングのアーチファクトなのか、それとも本質的に遅いのか知りたいのですが。」それを使用するインセンティブはほとんどないので、制御します。.submit()
コードをさらに高速に実行できるかどうかを知りたいです。私が持っている条件は、関数_concurrent_submit()
が、数値/オカレンスに数値5を含むイテラブルを返す必要があることです。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の回答に加えて、私はいくつかの個人的な調査に基づいて回答を提供しています。
.map()
および.submit()
ソリューションをさらに高速化する方法、およびProcessPoolExecutor.map()
がProcessPoolExecutor.submit()
よりも高速化できる場合。概要:
私の答えには2つの部分があります。
ProcessPoolExecutor.map()
ソリューションからさらに高速化する方法を示します。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」は、ここでは、離散化されたタスクの数が数よりも多い場合の平均計算時間にわたって、同じ数のチャンクとプールワーカーを使用した平均計算時間として定義されますプール労働者の。
調査結果:
左の図は、実験のセクションで言及したすべてのシナリオでかかる計算時間を示しています。それは、チャンク数/ワーカー数= 1チャンクの数>ワーカーの数。がかかる計算時間よりも前者のケースは、後者のケースより常に効率が劣ります。
右の図は、チャンク数/ワーカー数がしきい値に達したときに、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
メソッドを使用して、結果の順序付けられていないリストと順序付けられたリストをそれぞれコードのメインセクションに返しました。
ProcessPoolExecutor.submit()
のすべてのFutureオブジェクトを作成し、ProcessPoolExecutor.map()
のイテレータを作成するために使用される_concurrentメソッドの計算時間をプールワーカーの数に対する離散化タスクの数は同等です。この結果は単に、ProcessPoolExecutor
サブクラス.submit()
と.map()
が同等に効率的/高速であることを意味します。list
およびsorted
メソッドの計算時間(およびこれらのメソッドに含まれる他のメソッドの時間)を反映するため、これは予想されることです。明らかなように、list
メソッドは、sorted
メソッドよりも結果リストを返すのにかかる計算時間が短縮されました。 .submit()コードと.map()コードの両方でのlist
メソッドの平均計算時間はほぼ同じで、約0.47秒でした。 .submit()および.map()コードのソートされたメソッドの平均計算時間は、それぞれ1.23秒および1.01秒でした。言い換えると、list
メソッドは、.submit()コードおよび.map()コードに対して、それぞれsorted
メソッドより2.62倍および2.15倍高速に実行されました。sorted
メソッドが.map()
からよりも速く.submit()
から順序付きリストを生成した理由は明らかではありません、離散化されたタスクの数がプールワーカーの数と等しくなったときに保存します。とはいえ、これらの調査結果は、ソートされたメソッドによって、同等に高速な.submit()
または.map()
サブクラスを使用する決定が妨げられる可能性があることを示しています。たとえば、順序付けされたリストを可能な限り最短時間で生成することを意図している場合、ProcessPoolExecutor.submit()
が合計を最短にすることができるため、.map()
よりもProcessPoolExecutor.map()の使用をお勧めします。時間を計算します。.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
_
========================================= ==============================
ここでリンゴとオレンジを比較しています。 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))