質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

9481閲覧

【Python】ProcessPoolExecutor について

reishisu

総合スコア39

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

1グッド

0クリップ

投稿2018/07/22 16:29

編集2018/07/24 12:46

前提・実現したいこと

現在、Pythonで画像のグレースケールに変換する処理を並列で実行しようとしています。

そこでconcurrent.futuresのProcessPoolExecutorでmax_workerに5コア以上入れた場合2〜4コアと比べてあまり高速化されてないでのですが、原因がわからず困っております。

他のサイトを見てみても5コア以上はそこまで高速化できてないようですが、それがなぜかまでは説明されておりませんでした、、、

なので、原因ともしうまく高速化出来るのであれば教えて頂きたいです。
また、同じ要領でThredPoolExecutorを利用した場合スレッドの本数を2〜40にしてもシングルスレッドの実行結果と変わらなかったのですがなぜマルチスレッドでは全く高速化出来なかったのかも合わせて質問さて頂きたいです。

今回初めて、マルチプロセスやマルチスレッドを触るので理解が甘いのと処理がおかしいところもあるかもしれないです。。。

該当のソースコード

Python3

1""" 2並列で画像をモノクロにする 3画像をコマンドライン引数で渡しておく 4例)python ParallelMono.py 画像.pngとか 5事前に 6 pip install opencv-python 7 pip install matplotlib 8 pip install futures 9をしておく""" 10 11# スレッドで並列化を利用する為に必要なモジュール 12import concurrent.futures 13 14# その他各ライブラリをインポート 15import matplotlib.pyplot as plt 16import numpy as np 17import cv2 18import common 19import sys 20import os 21import time 22 23# コンソールをクリア 24os.system('clear') 25 26# コマンドライン引数から画像を読み込む 27img = common.getRGBImage( sys.argv[1] ) 28 29# 使用数を初期化 30useThread = 1 31useCPU = 1 32 33def main(): 34 # スレッドかCPUか選ぶ 35 msg = "マルチスレッドかマルチプロセスどちらにしますか?\n"\ 36 "[1:マルチスレッド 2:マルチプロセス] : " 37 multchType = int( input(msg.format(os.cpu_count())) ) 38 # 使用する数を選択 39 if multchType == 1: 40 useThread = int( input("使用するスレッドの数を入力してください : ") ) 41 if useThread > 1: 42 mulchThread(useThread= useThread) 43 plt.imshow(img) 44 plt.show() 45 else: 46 print("0以下なので終了") 47 elif multchType == 2: 48 useCPU = int( input("使用するCPUのコアを入力してください[ 1 ~ {0} ] : ".format(os.cpu_count())) ) 49 if useCPU >= 1 and useCPU <= os.cpu_count(): 50 mulchProcess(useCPU= useCPU) 51 plt.imshow(img) 52 plt.show() 53 else: 54 print("選択の範囲外なので終了") 55 else: 56 print("どちらでもないので終了") 57 58def changeToGray( number: int, width: np.ndarray ): 59 """ 60 並列化する処理 61 @param number (int) : このプロセスの番号 62 @param width (np.ndarray) : 横1行の配列[ [R, G, B], ・・・・ ,[R, G, B] ] 63 @return number (int) : このプロセスの番号 64 @return width (np.ndarray) : 引数で受け取った配列をグレースケールに変換した配列 65 """ 66 for pixel in width: 67 # グレースケールにするする処理 68 gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) 69 pixel[0] = gray # Red 70 pixel[1] = gray # Green 71 pixel[2] = gray # Blue 72 return number, width 73 74def mulchProcess(useCPU: int): 75 """ 76 マルチコアでプロセスを生成して実行させる処理 77 @param useCPU (int) : 使用するCPUのコア数 78 """ 79 print("") 80 start = time.time() 81 count = 0 82 print("{0}コアで処理を開始します!!".format(useCPU)) 83 with concurrent.futures.ProcessPoolExecutor(max_workers=useCPU) as executer: 84 fs = [ executer.submit(changeToGray, i, width) for width, i in zip( img, range(len(img)) ) ] 85 for future in concurrent.futures.as_completed(fs): 86 line_number = future.result()[0] 87 gray_width = future.result()[1] 88 img[line_number] = gray_width 89 count += 1 90 common.progressBar(count, len(img)) 91 print("\n終了しました!!") 92 print("かかった時間:{0}秒".format( time.time()-start )) 93 94def mulchThread(useThread: int): 95 """ 96 スレッドを生成して実行させる処理 97 @param useThread (int) : 使用するスレッドの数 98 """ 99 print("") 100 start = time.time() 101 count = 0 102 print("{0}スレッドで処理を開始します!!".format(useThread)) 103 with concurrent.futures.ThreadPoolExecutor(max_workers=useThread) as executer: 104 fs = [ executer.submit(changeToGray, i, width) for width, i in zip( img, range(len(img)) ) ] 105 for future in concurrent.futures.as_completed(fs): 106 line_number = future.result()[0] 107 gray_width = future.result()[1] 108 img[line_number] = gray_width 109 count += 1 110 common.progressBar(count, len(img)) 111 print("\n終了しました!!") 112 print("かかった時間:{0}秒".format( time.time()-start )) 113 114if __name__ == '__main__': 115 main()

試したこと

色々、コアの数を変更したりスレッドを利用したりしてみましたがなかなか成果か現れません。

補足情報(FW/ツールのバージョンなど)

[実行環境]
MacBook Pro (15-inch, 2016)
プロセッサ : 2.6 GHz Intel Core i7 ( 4コア8スレッド )
メモリ : 16 GB 2133 MHz LPDDR3
Python 3.6.4

[実行結果]
1コア:93.36475276947021秒 100%
2コア:45.95268726348877秒  約203%
3コア:32.04803204536438秒  約291%
4コア:25.691081047058105秒  約363%
5コア:25.711262941360474秒 約363%
6コア:24.469857692718506秒 約381%
7コア:23.86842966079712秒 約391%
8コア:23.69063401222229秒 約394%

yohhoy👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

umyu

2018/07/22 18:18

画像をグレースケールに変換するならば、 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)やcv2.cvtColor(img, cv2.COLOR_RGB2GRAY)を使えば良いと思いますが。 あくまでも質問用に例示しただけという認識でよいでしょうか。
reishisu

2018/07/22 20:48

他の画像処理も合わせてやるつもりなのでその認識で大丈夫です!
guest

回答2

0

(直接的には hayataka2049 さん回答にお任せしつつ、補足的な情報をいくつかご参考までに)

現在、Pythonで画像のグレースケールに変換する処理を並列で実行しようとしています。

画像処理を並列化する場合、質問文中のようにPixelLine単位で各プロセッサ/コアに振り分けるよりも、可能な限り画面領域単位とするほうが好ましいです。例えば高さ1000 Pixelの画像を4コアで処理する場合、250 PixelLineづつ4個コアに振り分ける方式の方がベターです。

他のサイトを見てみても5コア以上はそこまで高速化できてないようですが、それがなぜかまでは説明されておりませんでした、、、

どのような並列処理手法でも、必ず並列化によるオーバーヘッド(=追加の管理コスト)が発生します。並列化タスクの 粒度(grain) を適切にコントロールすることが重要です。マルチプロセスやマルチスレッドは比較的オーバーヘッドが大きい並列化技法のため、出来るかぎり粗粒度(coarse grain)なタスク分割としておいたほうが無難です。

また計測データを見る限り、8コアまでは僅かですが高速化を達成できているようです。(期待するデータではないかもしれませんが、)論理8個コア環境下では正しく並列処理を実現できていると思います。おそらく、並列度を9以上に増やすといずれ処理速度が低下していくと思います。

並列処理による処理の高速化は、必ず「アムダールの法則(Amdahl's law)」に従います。並列処理をどの程度までがんばるべきか、性能限界を大まかに見積もる際に参考にされてください。


あくまでも物理的には4コアですから、4(ハードウェア)スレッド以上使っても(処理内容にもよりますが)あまり高速化されないのが普通です。

個人的な経験則ですが、近年のIntel Coreアーキテクチャのハイパースレッディングは、昔のソレに比べてかなり実効性能向上が改善されている印象です。一時期はハイパースレッディングを無効化した方が総合性能が出たこともありましたが、近年では素直にハイパースレッディング有効で論理コア数まで並列化したほうが良いケースが大半と思います。(最終的にはケース・バイ・ケースですから、今回のように実測するべきですね)

マルチスレッドで高速化出来ない件に関しては、GILで調べるとわかります。
結論だけ書くと、基本的に、そのマルチスレッドは演算を高速化する目的には使えません。

hayataka2049 さんと同意見で、残念ながらPython言語はこの手の並列処理・演算高速化に不向きです。Pythonに限らずですが、大抵のLL言語では GIL(GVL) がボトルネックになっています。真に並列化・処理高速化が必要な場合、最終的にはC言語などのネイティブ・コンパイル方式のプログラミング言語を利用する必要があると思います。

投稿2018/07/23 07:47

編集2018/07/23 07:54
yohhoy

総合スコア6191

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hayataka2049

2018/07/23 09:10 編集

的確な情報を補って頂き、ありがとうございます >最終的にはC言語などのネイティブ・コンパイル方式のプログラミング言語を利用する必要があると思います pythonはある意味ではそれらコンパイル方式のプログラミング言語で書かれたライブラリのグルー言語ですから、可能な限りライブラリの中で処理させるというのが現実的な落とし所ですね
reishisu

2018/07/24 05:47 編集

hayataka2049さん、yohhoyさん回答&補足ありがとうございます! 最初は私もあらかじめ配列をコア数分で区切ってやろうとしていたのですが、Python始めたばかりでNumpyの配列を処理した後の戻り値の配列のマージと並列処理のやり方が解らず困っておりました、、、、 なのでそちらは現在試行錯誤中です。 アムダールの法則は講義の中で聞いていたので知っておりますが、Pythonでは限界があることをは知りませんでした! ある程度、形になったらJavaが好きなのでそちらでも実装してみたいと思います。
reishisu

2018/07/24 12:28

指摘された通りに、プロセスの通信回数を減らしてみたのですが全く高速化されないです、、、 私のイメージはプロセスにある程度まとめて配列を渡しているので「通信時間*高さ」から「通信時間*(高さ/プロセス数)」になってこれで速くなると思ったのですが何処がボトルネックになっているか教えて頂けないでしょうか?(><) def changeToGray2( number: int , length: int ): """ 並列化する処理 @param number (int) : 画像の処理対象範囲の先頭の添字 @param length (int) : 対象範囲の長さ @return number (int) : 画像の処理対象範囲の先頭の添字 @return part_height (int) : 処理後の画像の配列 """ start = time.time() endPioint = number + length if number + length < len(img) else len(img) - 1 part_height = img[ number : endPioint-1 ] for width in part_height: for pixel in width: gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) pixel[0] = gray pixel[1] = gray pixel[2] = gray return number, part_height def mulchProcess2(useCPU: int, step: int): """ マルチコアでプロセスを生成して実行させる処理 @param useCPU (int) : 使用するCPUのコア数 @param step (int) : 画像の高さをコア数で割った数 """ # 配列の区切りポイント index_list = [ i for i in range(0, len(img), step) if i < len(img) ] with concurrent.futures.ProcessPoolExecutor(max_workers=useCPU) as executer: fs = [ executer.submit(changeToGray2, i, step) for i in index_list ] for future in concurrent.futures.as_completed(fs): line_number = future.result()[0] part_height = future.result()[1] for i, height in zip( range(line_number, line_number+len(part_height)), part_height ): img[i] = height
guest

0

ベストアンサー

本質的には、マルチプロセス処理にはかなりオーバーヘッドがあります。特にpythonの実装だと、プロセス間通信にpickleを使っていますし。ですから、まずそこで限界があります。

今回のケースでは更に、CPUのコア数の限界があります。8コアと仰っていますが、恐らく4コア8スレッドのハイパースレッディング機能のあるCPUではないかと思います。

あくまでも物理的には4コアですから、4(ハードウェア)スレッド以上使っても(処理内容にもよりますが)あまり高速化されないのが普通です。

参考:
ハイパースレッディング (Hyper-Threading)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
同時マルチスレッディング - Wikipedia
ASCII.jp:Core iシリーズにも使われる「SMT」の利点と欠点 (1/4)|ロードマップでわかる!当世プロセッサー事情


マルチスレッドで高速化出来ない件に関しては、GILで調べるとわかります。

結論だけ書くと、基本的に、そのマルチスレッドは演算を高速化する目的には使えません。

参考:
karky7のブログ: PythonのGILについて簡単に調べてみました
グローバルインタプリタロック - Wikipedia

投稿2018/07/22 20:33

hayataka2049

総合スコア30933

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

reishisu

2018/07/22 21:53

あ、確かに調べてみたら4コア8スレッドでした! os.cpu_count()では8が取れてたのでてっきり8コアあるのかと勘違いしておりました、、、 つまり、ハイパースレッディングの機能があるおかげでオクタコアのCPUには及ばないけど4コアが頑張って8コア分働こうとしているのでほんのちょっとだけ早くなっていたんですね! なるほどGILというものがあるんですね! ファイルの入出力ではCPUを利用しないのでGILの取得が行われずにスレッド同士が並列で入出力ができ、CPUを利用して演算を行う場合ではGILの取得が行われるので実質逐次処理と変わらないっていうことでしょうか?
hayataka2049

2018/07/22 21:58

コアに関してはそのとおりです。 GILは、「待ち」があるときは並行して処理を進められる(ので、その分高速化できるかもしれない余地はある)と理解すれば良いかと 純粋な演算に対しては、使える計算リソースがシングルスレッドで回すのと同じなので高速になりません(むしろオーバーヘッド分損する)
reishisu

2018/07/22 22:02

教えて頂きありがとうございます! 確かに、今回の演算の場合は待ち状態はないのでスレッドを増やせば増やすほどGILの取得と開放でオーバーヘッドが起きて効率が悪くなってしまうということですね。 待ちというのは、INPUT関数の様な標準入力の待ちでも同様でしょうか?
hayataka2049

2018/07/22 22:05

inputの内容に関係のない何らかの重い処理があって、入力されたタイミングで処理結果を表示する、というようなケースだと、うまくやれば入力待ちの時間を使えます inputされた情報を使ってなにかしたい、とかだと限界がある訳ですが
reishisu

2018/07/23 01:07 編集

なるほど! ・演算処理では、マルチプロセスを利用 ・待ちのある処理では、マルチスレッドを利用 ・ハイパースレッディングやGILについても分かり易くとても腑に落ちました!!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問