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

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

ただいまの
回答率

90.32%

  • Python

    9244questions

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

  • Python 3.x

    7432questions

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

【Python】ProcessPoolExecutor について

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 505

reishisu

score 17

 前提・実現したいこと

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

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

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

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

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

 該当のソースコード

"""
並列で画像をモノクロにする
画像をコマンドライン引数で渡しておく
例)python ParallelMono.py 画像.pngとか
事前に
  pip install opencv-python
  pip install matplotlib
  pip install futures
をしておく"""

# スレッドで並列化を利用する為に必要なモジュール
import concurrent.futures

# その他各ライブラリをインポート
import matplotlib.pyplot as plt
import numpy as np
import cv2
import common
import sys
import os
import time

# コンソールをクリア
os.system('clear')

# コマンドライン引数から画像を読み込む
img = common.getRGBImage( sys.argv[1] )

# 使用数を初期化
useThread = 1
useCPU = 1

def main():
    # スレッドかCPUか選ぶ
    msg = "マルチスレッドかマルチプロセスどちらにしますか?\n"\
          "[1:マルチスレッド 2:マルチプロセス] : "
    multchType = int( input(msg.format(os.cpu_count())) )
    # 使用する数を選択
    if multchType == 1:
        useThread = int( input("使用するスレッドの数を入力してください : ") )
        if useThread > 1:
            mulchThread(useThread= useThread)
            plt.imshow(img)
            plt.show()
        else:
            print("0以下なので終了")
    elif multchType == 2:
        useCPU = int( input("使用するCPUのコアを入力してください[ 1 ~ {0} ] : ".format(os.cpu_count())) )
        if useCPU >= 1 and useCPU <= os.cpu_count():
            mulchProcess(useCPU= useCPU)
            plt.imshow(img)
            plt.show()
        else:
            print("選択の範囲外なので終了")
    else:
        print("どちらでもないので終了")

def changeToGray( number: int, width: np.ndarray ):
    """
    並列化する処理
    @param  number (int)       : このプロセスの番号
    @param  width (np.ndarray) : 横1行の配列[ [R, G, B], ・・・・ ,[R, G, B] ]
    @return number (int)       : このプロセスの番号
    @return width (np.ndarray) : 引数で受け取った配列をグレースケールに変換した配列
    """
    for pixel in width:
        # グレースケールにするする処理
        gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11)
        pixel[0] = gray # Red  
        pixel[1] = gray # Green
        pixel[2] = gray # Blue
    return number, width

def mulchProcess(useCPU: int):
    """
    マルチコアでプロセスを生成して実行させる処理
    @param  useCPU (int)  : 使用するCPUのコア数
    """
    print("")
    start = time.time()
    count = 0
    print("{0}コアで処理を開始します!!".format(useCPU))
    with concurrent.futures.ProcessPoolExecutor(max_workers=useCPU) as executer:
        fs = [ executer.submit(changeToGray, i, width) for width, i in zip( img, range(len(img)) ) ]
        for future in concurrent.futures.as_completed(fs):
            line_number = future.result()[0]
            gray_width  = future.result()[1]
            img[line_number] = gray_width
            count += 1
            common.progressBar(count, len(img))
    print("\n終了しました!!")
    print("かかった時間:{0}秒".format( time.time()-start ))

def mulchThread(useThread: int):
    """
    スレッドを生成して実行させる処理
    @param  useThread (int)  : 使用するスレッドの数
    """
    print("")
    start = time.time()
    count = 0
    print("{0}スレッドで処理を開始します!!".format(useThread))
    with concurrent.futures.ThreadPoolExecutor(max_workers=useThread) as executer:
        fs = [ executer.submit(changeToGray, i, width) for width, i in zip( img, range(len(img)) ) ]
        for future in concurrent.futures.as_completed(fs):
            line_number = future.result()[0]
            gray_width  = future.result()[1]
            img[line_number] = gray_width
            count += 1
            common.progressBar(count, len(img))
    print("\n終了しました!!")
    print("かかった時間:{0}秒".format( time.time()-start ))

if __name__ == '__main__':
    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%

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • umyu

    2018/07/23 03:18

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

    キャンセル

  • reishisu

    2018/07/23 05:48

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

    キャンセル

回答 2

checkベストアンサー

+3

本質的には、マルチプロセス処理にはかなりオーバーヘッドがあります。特に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/23 06:53

    あ、確かに調べてみたら4コア8スレッドでした!
    os.cpu_count()では8が取れてたのでてっきり8コアあるのかと勘違いしておりました、、、

    つまり、ハイパースレッディングの機能があるおかげでオクタコアのCPUには及ばないけど4コアが頑張って8コア分働こうとしているのでほんのちょっとだけ早くなっていたんですね!

    なるほどGILというものがあるんですね!
    ファイルの入出力ではCPUを利用しないのでGILの取得が行われずにスレッド同士が並列で入出力ができ、CPUを利用して演算を行う場合ではGILの取得が行われるので実質逐次処理と変わらないっていうことでしょうか?

    キャンセル

  • 2018/07/23 06:58

    コアに関してはそのとおりです。

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

    キャンセル

  • 2018/07/23 07:02

    教えて頂きありがとうございます!

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

    キャンセル

  • 2018/07/23 07:05

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

    キャンセル

  • 2018/07/23 07:10 編集

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

    キャンセル

+3

(直接的には 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 18:05 編集

    的確な情報を補って頂き、ありがとうございます

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

    キャンセル

  • 2018/07/24 14:46 編集

    hayataka2049さん、yohhoyさん回答&補足ありがとうございます!

    最初は私もあらかじめ配列をコア数分で区切ってやろうとしていたのですが、Python始めたばかりでNumpyの配列を処理した後の戻り値の配列のマージと並列処理のやり方が解らず困っておりました、、、、
    なのでそちらは現在試行錯誤中です。

    アムダールの法則は講義の中で聞いていたので知っておりますが、Pythonでは限界があることをは知りませんでした!
    ある程度、形になったらJavaが好きなのでそちらでも実装してみたいと思います。

    キャンセル

  • 2018/07/24 21: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

    キャンセル

同じタグがついた質問を見る

  • Python

    9244questions

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

  • Python 3.x

    7432questions

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