Posts Tagged ‘並行コンピューティング技法’

並行コンピューティング 第10章 & 第11章

8月 1st, 2010

今回でラスト。

◯第10章 グラフアルゴリズム
深さ優先探索と幅優先探索の並行実装例を紹介している。どちらも逐次処理だと再帰実装がよく用いられるが、再帰と並行処理の相性が悪いためキュー・スタックをつかって探索位置を管理する必要がある。スタックを使えば深さ優先探索に、キューを使えば幅優先探索になる。

次が最短経路探索問題のアルゴリズム。Floyd-Warshall法やDijkstra法があるけど、本ではFloyd-Warshall法だけTBBを使って紹介。特筆事項は無し。

最後が最小スパニングツリーを見つけるアルゴリズム。KruskalとPrimがあるけど、本ではPrimをTBBとOpenMPを組み合わせて並列化する手法を紹介。これまでの総集編といった趣。

◯第11章 スレッド対応ツール
宣伝じゃないと言いながらもIntelの製品をパワープッシュ。
著者はIntelの社員さんなのは割り引いて聞く必要があるかも。

◯全部読んでの感想
会社のメンバで輪講したのですが、後半はアルゴリズムの解説が多く、ソースの実装もいまいちだったように感じました。さらに後半は元の文が悪いのか、訳が悪いのか読んでいてわからないところが頻出しました。並行処理の基礎知識を習得するのに5章までを読むのにはいいかもしれません。後半は並行処理実装のアイデアを色々もらえるけど、いくらか疑ってかかったほうがいいと思います。

並行コンピューティング技法 第8章&第9章

7月 3rd, 2010

間が開いたので2回分まとめて。

◯第8章 ソート
プロセッサ時間の80%以上はソートに費やされるとも言われる。ソートを高速化すれば処理全体の高速化につながりやすい。
この章では5つのアルゴリズムについて並行化を検討している。

バブルソートは左右で要素を比較して、小さい値(または大きい値)が泡のように上がっていくことからそう呼ばれる。スレッドの順番が入れ替わらない限り、頭から複数スレッドで配列を走査してもソートは保証される。このアルゴリズムは波の動きに似ているためwavefront法とも呼ばれる。単に前のスレッドを抜かさないようにするとオーバーヘッドが大きくなるため、配列をいくつかのゾーンに区切って制御すると良い。

奇偶転置ソートは「奇数インデックスの比較→偶数インデックスの比較→奇数インデックスの比較」を繰り返してソートを行う。隣接同士の比較・変更しか起こりえないので、データ分解による並列化が容易。

シェルソートは配列全体を適当なサイズで分割し、分割された範囲内でソートを行う。徐々に分割サイズを小さくすることでソートを行ない、要素が大きく動かせることからソートが早く行える特徴がある。逐次処理では有効だが、並行処理ではキャッシュが効きにくくなり並行処理では短所にもなりうる。

クイックソートは逐次処理では最も一般的なソートである。再帰を使って実装することもあるが、並行処理には不向きなのでキューを使うなりして再帰は避ける必要がある。スレッドをプールし、ソート対象のインデックス範囲をキュー/スタックに入れておくことでスレッド間の競合を抑え、処理を並行化することができる。処理が終わったかどうかを判断し、スレッドに処理を辞めるよう促す実装が若干面倒。

奇数ソートはソート対象を二進数と考え、上位ビット/下位ビットを使ってソートしていく。同じ値の順序が保たれる安定ソートであることが特徴。実装では複数ビットをまとめてソートすることになるが、ソート後の移動先の計算がかなり面倒。本でもトリッキーに思える例が紹介されていた。

◯第9章 サーチ
未ソートデータに対するサーチと、ソート済みデータに対するサーチの例を紹介。

未ソートデータの場合、全要素を見ないといけないので、配列をデータ分解してスレッドに処理させる。探索対象のデータが見つかったときに処理を打ち切る工夫を入れるぐらいでOK。素直な実装になる。

ソート済みデータの場合、逐次処理では2分探索が一般的。並行処理ではこの考え方を発展させてN分探索すると良い。配列をNつに分解し、探索対象がどちら側にあるか(右側、左側、見つかった)を調べる。探索対象のデータは向きが異なるブロックに含まれるため、このブロックを探し出し、内部をさらにNつに分解して再帰的に探索を続ける。

posted with amazlet at 10.07.03
Clay Breshears
オライリージャパン
売り上げランキング: 6200

並行コンピューティング技法 第6章

4月 23rd, 2010

いよいよアルゴリズムの並行化に着手する。今回は並列和、プリフィックススキャン、セレクションの3種類。

◯並列和
配列の和を全部求めるよくある処理。楽なのはOpenMPやTBBを使ってリダクション処理をそのまま利用すること。
自前で実装するなら配列をスレッド数で分割して個別に小計を計算。全スレッドが計算終わったら小計を全部足して合計をもとめるというやり方がおすすめ。

◯プリフィックススキャン
[3, 5, 2, 5, 7, 9, 4, 6]という配列を与えたときに[3, 8, 10 15, 22, 31, 35, 41]という配列を求める。配列のN番目に元の配列の0〜N番目までの和を入れておく処理に当たる。包含的プリフィックススキャンとも言うらしい。
先頭を覗いて[0, 3, 8, 10, 15, 22, 31, 35]の配列を求める排他的プリフィックススキャンの方法もある。

並列化の手法として、スレッド数で配列を分割し、個別にプリフィックススキャンを行う。次に末尾の要素を使って前後のチャンクに伝搬させる値を計算する。計算が終わったそれぞれのスレッドが担当するチャンクに値を加算してプリフィックススキャンを完了する。

さっきの例を3スレッドでやるとこんな感じになる。2と4のステップが並列化可能になる。
2から3、3から4に移るときに毎回スレッドを生成/破棄するとオーバーヘッドになるので、スレッドのイベント通知機能を上手く使って使いまわすのが吉。

1. 入力をスレッド数で分割
[3, 5, 2, 5, 7, 9, 4, 6] -> [3, 5, 2], [5, 7, 9], [4, 6]

2. 個別にプリフィックススキャン
[3, 5, 2], [5, 7, 9], [4, 6] -> [3, 8, 10], [5, 12, 21], [4, 10]

3. 配列の末尾の値で排他的プリフィックススキャンを行い、伝搬させる値を計算する
[3, 8, 10], [5, 12, 21], [4, 10] -> [10, 21, 10] -> [0, 10, 31]

4. 3で求めた値を2の処理結果にそれぞれ加算
[3, 8, 10]+0, [5, 12, 21]+10, [4, 10]+31 -> [3, 8, 10, 15, 22, 31, 35, 41]

◯セレクション
配列をソートしてN番目の値を取ってくるという処理。
ソートしてもいいけど、1回しか呼ばれないとか、適宜順番が変わるような配列だとセレクションがおすすめ。

公知のセレクションアルゴリズムがあって、その中でいくつかを並列化する。

1. 配列数がQ(本では5を勧めている)より小さければデータをソートして、k番目の要素を返す。Qより多ければ次のステップへ
2. チャンクから中央値を選ぶ(本では配列を分割して中央値を求め、その中からさらに中央値を求めるやり方をしているけど、配列の中身が適度にバラバラなら適当に選んでもいいかも)
3. データを A.中央値より小さい値を持つ B.中央値と同じ値である C.中央値より大きい値を持つ の3種類に分け、A,B,Cの数を数える。
4. 求めるk番目の要素がどこに入るかを調べる。Bに入るなら値を返して終了、AかCなら再帰的に1から処理を続ける。

本で高速化しているのはこのうちの2の処理、3の処理、4の処理の一部である。ポイントは4の処理で、再帰的に呼び出す際の配列の部分集合を使うのにプリフィックススキャンを使って詰め直し先のインデックス値を求めている。逐次処理に慣れきっていると重そうだけど、実際実行してみたら早いのかな? 要検証。

並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング
Clay Breshears
オライリージャパン
売り上げランキング: 8422

並行コンピューティング技法 第3章

3月 28th, 2010

少しずつ難しい話も増えてきたかな? 第3章です。

「第3章 正当性の検証と性能測定」

◯第3章で学ぶ内容

  1. 誤りの無い並行アルゴリズムをきちんと設計できたかをどう確認するか
  2. 並行ソースコードが”十分に”並列動作しているかをどう確認するか

◯Ben-Ariの4つの並行実行一般化

  • プログラムとは連続したアトミックな実行文である。
  • 並行プログラムは複数のスレッド内のアトミックな実行文のインタリーブである。
  • アトミックな実行文の全ての組み合わせは、検証する並行アルゴリズムの全ての性質を満たさなければならない。
  • どのインタリーブでもスレッドの実行文が不公平に除外されることはない。

どんなプログラムも最小の実行単位に分けられる。並行処理は”最小の実行単位”が様々な組み合わせで実行される。実装されるアルゴリズム/プログラムはいかなる組み合わせでも正しく実行されなければならない。正しく実行するとは特定の処理が意味もなく除外されないことも含む…ということだと理解しました。

◯クリティカルセクション問題を例にBen-Ariの性質を学ぶ
共有変数を参照/更新するソースコード部分”クリティカルセクション”の実現の方法を通して、Ben-Ariの性質を学ぶ。本では5つの段階を追って解説していますが、ここでは割愛。問題にしたポイントは以下のとおりです。

  • 第1段階:1スレッドがクリティカルリージョンを実行していなくても、もう一方のスレッドがクリティカルリージョンに入ることを禁止されていまう
  • 第2段階:排他制御が保証されないインタリーブの組み合わせがある。
  • 第3段階:デッドロックが発生してしまう
  • 第4段階:一方のスレッドがクリティカルリージョンから除外されてしまう(スターベーション)

ということで、スレッドプログラミングでよく陥りそうな問題が満載です。最終的に第5段階として紹介されているのがデッカーのアルゴリズム – Wikipediaです。でもこのアルゴリズムだと2つのスレッドしか排他制御出来ないので、ランポートのパン屋のアルゴリズム – Wikipediaピーターソンのアルゴリズム – Wikipediaが実用にはいいそうです。でもこの節で学ぶべきは使うアルゴリズムの選択じゃなくて、どういう所に着目して問題を見つけるのかというやり方なので、きちんと学んだことがなければ本を当たった方がいいと思います。

◯デッドロックを発生させる4つの条件
4つの条件はANDで発生する。どれか1つでも成立しなければデッドロックには成り得ない。

  • 相互排除条件:リソースを特定のスレッドがロックしようとするからデッドロックが発生する
  • 獲得後のウェイト:複数のリソースをロックしようとするとデッドロックが起きやすくなる。
  • プリエンプト無し:スレッドが自発的にロックを開放する仕組みが無いからデッドロックが起きると復帰出来ない。
  • 循環待ち:ロックの取得関係が循環になってしまう。

デッドロックを避けるにはどれか1つでも起きないようにすればいいらしいけど、複数リソースのロックとか危ういものは出来るだけ避けた方がいいと思う。

◯並行化によってどれぐらい性能が上がったか?
高速化率はどう伝えるべきか? 筆者いわく倍数がいいとのこと。 105%の高速化率と言っても元より5%速くなっただけかもしれないし、205%の高速かもしれない。2倍といったら受けてが迷うこと無いよね とのこと。確かにその通りかも。

逐次処理を並行処理に置き換えた際にどれぐらい高速化出来るかの見積はアムダールの法則 – Wikipediaグスタフソンの法則 – Wikipediaで出来る。グラフを書いてみると一目瞭然だけど、並行化出来ない処理がたとえ25%でも含まれていると、どんなにコア数を増やしても3倍までしか速くならない。

並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング
Clay Breshears
オライリージャパン
売り上げランキング: 3109

並行コンピューティング技法 第2章

3月 11th, 2010

勉強会の2回目があったのでメモ、1回目は下のリンクからどうぞ。

「第2章 並行か非並行か?それが問題だ」

◯大前提
全ての処理を並列化することは出来ない。世の中には並列化が不可能な処理があり、何がそうなのか知っておく必要がある。並行化できないアルゴリズムは以下が代表例。

  • 状態を持つアルゴリズム
  • 漸化式
  • 帰納変数(ループの度に値が増加する、ループ変数とは1対1に対応しないインデックス)
  • ループ内の処理依存

◯並行アルゴリズムの設計モデル
順序に依存しない処理を並行に実行する「タスク分解」と、大量のデータの個別の部分を並行して処理する「データ分解」がある。
どちらを選んでも基本のフレームワークは変わらない。タスクを準備し、スレッドへ処理を割り当て、次の処理へ進む前に全てのタスクが完了したことを確認する。

◯タスク分解
スレッドに割り当てるタスクを抽出する難易度はソースコードや処理内容に対する理解に反比例する。良く知っていればタスクの抽出も容易になる。タスクを簡単に見つけ出す裏技はないが、処理時間を多く占めるホットスポットにおいて、ループに着目するとそこがタスク抽出に適していることが多い。数値計算もタスク抽出のポイントとしておすすめ。

抽出したタスクの数にもポイントが有る。まずタスクの数はスレッド数(≒コア数)と同じ数以上にする。これはアイドルスレッドを避けるためである。次にタスクの粒度は出来るだけ大きい方が良い。これは並列化によるオーバーヘッドが生じることが影響している。タスクが大きい方がメモリキャッシュが効くなど性能面で有利な点がある。

タスク同士の間には2種類の依存性がある。1つ目は順序の依存で、あるタスクが他のタスクの処理結果に基づいて実行される場合が該当する。2つ目はデータ依存で、変数の使い方で問題が生じることがある。

タスクの割り当ては最初に決めてしまう静的割り当てと、動的にタスクを決める動的割り当てがある。タスクの処理時間が固定の時は静的割り当てが簡単でオーバーヘッドもなくてよい。処理時間にばらつきがある場合は動的にタスクを割り当てる必要がある。

◯データ分解
基本ルールはタスク分解と同じ。分割する数はスレッド数よりも多くする、細かすぎると並列化によるオーバーヘッドが問題になるので大きい粒度で。処理対象となる分割されたデータ領域をチャンクと呼ぶ。チャンク分割はタスクの粒度と境界長の比率が最大になるように分割すると良い。

チャンクの境界において、別チャンクのデータを参照する必要があると工夫がいる。簡単なのは境界部分を予めスレッドごとにコピーしておく方法がある(ゴーストセル)。そうでなければ処理時に必要に応じて参照させる方法があるが、参照対象のデータが未処理の場合、参照処理をまたしておく必要がある。どちらも一長一短がある。

並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング
Clay Breshears
オライリージャパン
売り上げランキング: 39727

並行コンピューティング技法 第1章

3月 3rd, 2010

「業務が組み込みメインでもそろそろ並行処理も避けて通れないよね」という雰囲気を年初から職場で醸し出し、勉強会を開くことにこぎつけました。内容は並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミングの輪講です、The Art of Concurrencyの邦訳ですね。

「職場の1人ができてもしょうがなくて、みんなの基礎がある程度揃ってないと話もかみ合わなくなるだろう」という思いが勉強会を選んだ理由の1つ。もう1つの理由は「勉強会ならサボらず最後まで読まざるをえないよね」という自分へのハッパがあります。今は他にも実用Gitを読んでいて、実は昨年の夏からレガシーコード改善ガイド (Object Oriented SELECTION)を積読にしたままです。3冊平行に読むのが良いとは思えませんが、読んだ記録をそれぞれつけて行こうと思います。

前置きが長くなりましたが、まずは第1章「速くしたい人、手を挙げて!」です。

◯並列と並行の違い
・並行:複数の動作を同時に実行状態に保てる機能を備えていること。
・並列:複数の動作を同時に実行できること。
スレッドライブラリを使ってマルチスレッドプログラミングを実装し、シングルコア(Pentiumとか)でプログラムを実行した→並行処理
スレッドを使ったプログラムをCore i7で実行した→並列処理
ということですね。並列処理は並行処理に完全に包含される関係があります。

◯効率的な並行アルゴリズム
コア数に応じて処理時間が短くなる(2コアで1/2、4コアで1/4) が理想だけど、処理の一部は逐次処理が不可避な場合がほとんどで理想的な性能を得られることは殆どない。
特定のコア数を念頭においたアルゴリズムではなく、今後新しく発売されるCPU(16コアとか、32コアとか…)に応じて望ましい性能がそのまま発揮出来るように実装するのが目標。

◯スレッド化のステップ
通常の開発が1.仕様定義、2.設計、3.実装、4.テスト、5.チューニング、6.保守の流れを取るのに対し、スレッド化のステップは以下をとる。

  1. 分析:どこが並列可能かを見極める。並列化によりオーバーヘッドが生じるので重い処理を選ぶ。
  2. 設計と実装
  3. 正当性の検証:不完全なスレッドの実装により起きる問題の対応
  4. 性能チューニング:同期・競合・メモリキャッシュなどの問題への対応

逐次処理のソースで動く実装を固めるのが第1、いきなり並行処理アルゴリズムを実装すると元々の問題か、平行化に起因する問題か切り分けが必要になる。

◯歴史的な話
古くは複数の機器を組み合わせて処理を平行化する分散メモリプログラミングが主流だった。最近はCPUのマルチコア化が進んでいるので共有メモリプログラミングに移っている(本にはこんな記述無いんだけど理解があってるかな?)。

◯これから学んでいくこと

  • 処理の分割の仕方。処理を並列化するのか、処理対象のデータを並列化するのか。
  • 処理の分担をどう決めるか。最初に決めてしまうのか、ロードバランシングを考慮して動的に決めるのか。
  • ロック、排他制御の定石 など
並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング
Clay Breshears
オライリージャパン
売り上げランキング: 11030