並列処理・並行処理を設計するときに最初に整理すべきこと

記事内に広告が含まれています。

プログラムのパフォーマンス向上のため、並列処理・並行処理化する際に必要な観点や分割方針について整理

ボトルネックの種類

ボトルネックが CPU バウンド or I/O バウンドのいずれかに該当した場合、並列処理にすることが有効になる。

I/Oバウンド

ファイルのDL/UL や、外部システムからのレスポンス待ちが長いことがボトルネックの場合を指す。レスポンスが届くまで待ち続けるより、待っている間に別処理を進めておくほうがかかる時間短縮に有利。代表的な要因は以下。

  • DBアクセス
  • ファイルのDL/UL, 読み書き
  • ネットワーク通信

上記のアクセス頻度をいかに減らすか? という観点に立つことが有効になる。

例えば、for 文内にDBアクセスのコードがあった場合、頻度を減らすor キャッシュを残すなどが有効になる。

# for文の中でDB検索をする場合、I/Oがファイル数分繰り返されてしまう
for file in file_list:
    # DBアクセスしてデータ取得
    db_select = ...
    # 取得結果に対して何らかの処理
  result = process(file, db_select)

↓改善策案

# DBアクセスしてデータ取得
db_select = ...
# アクセス後にfor文に入る
for file in file_list:
    
    # 取得結果に対して何らかの処理
  result = process(file, db_select)

# もしくは、DBで取得したものを一時的に保管しておき、存在しない場合だけDBアクセス

db_select_cache = {}  # DBの取得結果をキャッシュする変数を設定
for file in file_list:
    db_select = (db_select_resultから取得)
    # db_select が空(取得できなかった)場合はDBアクセス
  if not db_select:
        db_select = (DBアクセスする処理)
    
    # 取得結果に対して何らかの処理
  result = process(file, db_select)

CPU バウンド

ファイルの解析やデータの変換など、CPU不可が重い処理がボトルネックになっている場合を指す。

この場合、プログラムの並列化により処理時間の短縮を図ることができる。もしくは、プログラムコードのアルゴリズム自体の書き換えも検討が必要になる場合も。

  • 複雑な計算
  • プロセス、スレッドの切り替えによるオーバーヘッド

並列処理化するための観点

並列処理化する場合、お互いに依存関係がないレベルまで細分化する必要がある。

細分化する対象として、処理(プログラム)もしくはデータの2種類ある。

  • 処理を分解する細分化する
  • データを分割する細分化

処理を分解する場合

問題をすべて一度に実行できるタスク群に分割するにはどうすればよいか

引用元:なっとく!並行処理プログラミング より

タスク依存関係グラフというものを使うと、視覚的に処理を整理することができて便利。

ノード(四角形)が処理を表し、エッジ(矢印)依存関係を意味する。矢印の方向に依存があることになる。

仮に以下の関係図を描けたとすると、タスク1~3を並列処理・並行処理で進めることが可能となる。(タスク1~3には依存関係がないため)

タスク1~3はタスク4に依存関係にある。

データを分解する場合

同じ演算で済むチャンク(=かたまり)へ細分化し、並列でデータ処理を行うイメージ

タスクのデータを互いに独立した状態で処理できるチャンクに分割するにはどうしたらいいか。

引用元:なっとく!並行処理プログラミング より

コードの繰り返し処理部分(forループ、whileループ)がこの分解に適していることが多い。

1つのデータが巨大な場合、チャンクに分割する方針が有効になることが多い。(fork/joinパターン)

タスクの部分を並列化して同時に処理

小さいデータを大量に裁く場合、複数データをそのままタスクに入れて並列処理する方法もある。(Mapパターン)

並列処理(parallel)と並行処理(concurrent)

  • 並列処理は同じ時間に複数処理が同時に実行されること。マルチコアなど、複数のハードウェア上で実行される。電気回路の並列つなぎのイメージ
  • 並行処理は1度に多くのものを実行すること。処理の種類を短時間で切り替え、あたかも同時にしょりされているように見せる

参考