はじめに
Qiitaのアカウントは結構前に作ったのですが、これまでずっと放置していました。ところが先週になって急に弊社アドベントカレンダー
オープンストリーム Advent Calendar 2017
に参加することになったので、活用してみることにします。
何をやる?
私は学生時代、スーパーコンピュータをターゲットとした並列ファイルシステムというか並列データアクセス機構の研究をやっとりまして、その時MPIを使って実装していたわけです。
で、就職後はWebの人になってそういう世界からすっかり遠ざかっていたわけですが、Webの世界は間違いなく面白いのですが、学生時代に獲得したことはあまり生かせなかったのでしょんぼりした面もあったわけですね。
でもでも、何やら最近はWeb界隈でもこういう並列処理、並行処理な話題が増えてきました。で、久しぶりに最近の環境でなんかやってみようかしら、と思って調べてみたところ、どうやらGo言語がお手軽で良いらしいですね。というわけでやってみました。
この投稿では、Goでサンプル的な並列処理をやってみて、 MPI
を触っていた頃と比べてどうか?みたいなことを言っていこうかなと思います。多分今の方がきっと気軽にできると、そう確信しながら・・・
と、思ったら
とかやってると、どうやら、同じアドベントカレンダー
オープンストリーム Advent Calendar 2017
用ネタでネタ被りが発生してしまいました。(※12月3日のネタです)
MPIによるプログラムの並列化
https://qiita.com/ojisann/items/cea6031863886a2eef67
おお、社内に使う人がいたんだ・・・という驚き。大学を離れてから、自分の他にMPIを扱う人を一人も見たことがありませんでした。
とはいえ、あちらは自分が昔やってたMPIの方ですので、多分大丈夫。あと、ここまで被ったのならせっかくなので、というより、ネタ探しに困ってたので、題材も頂いちゃいます、ゴメンナサイ
Goってどういうやつよ
まぁありがちですが、お約束なので書きましょう。
golang.jpプログラミング言語Goの情報サイト
http://golang.jp/
によると、Go言語とは
Go言語は、Linux、Mac、Native Clientで動作する開発言語で、Android携帯上でも動作します。
まだ発表されたばかりなのでこれからの動向が注目されています。
特徴はGoogleによると・・
・シンプルな言語である。
・コンパイル・実行速度が早い。
・安全性が高い。
・同期処理が容易に行える。
・なにより楽しい。
・オープンソースである。
とかなんとか。なんか、特徴を見ても一番注目してた部分であるはずの並列処理が強調されてないですが、気にしないでおきます。出来るということは確認済みなので。
比較してみる
シンプル
Goの言語仕様は比較的シンプルみたいですね。シンプルなのは良いことです。速く覚えられます。MPIを覚えるのは割と大変でした。MPIは名前の通りメッセージパッシングでのプロセス間通信を実現するC++のライブラリと、それを組み込むプリコンパイル環境、および、並列処理を行うためのシェルスクリプト(実行したいオブジェクトコードを引数で指定する)で作られた実行環境からなります。各種各ノードにインストールしたりとか、まず使う前に覚えることがたくさんあっていろいろ面倒です。
日本語資料たくさん
Go言語は日本語の資料が多いですね、ありがたいことです。インストールもサンプルソースも、探せばいくらでもありそう。対する MPI
はといえば、まるで砂漠のような環境でした。なにせ当時、日本語はおろか英語でもろくに文献は存在せず、頼りになるのは1冊の洋書とMPI仕様のドラフトだけでした。後にその洋書の日本語訳書が出たのを知るのは随分後になってからです。
お気軽にできるのは間違いないですね、これはGo大勝利ではないでしょうかね。
環境を作ってみる
環境構築はこちらを参考にしました。
はじめての Go 言語 (on Windows)
https://qiita.com/spiegel-im-spiegel/items/dca0df389df1470bdbfa
WindowsのVisual Studio CodeでGo言語の開発環境を作る(2017年7月版)
http://blog.shibata.tech/entry/2017/07/20/211442
全く何もない状態から、各種インストールしてさくさくっとやっていきます。
最初にVSCをダウンロードしてからHelloWorldまでだいたい15分弱です。簡単ですね。
あまりに簡単なので、特に書くことはありませんでした。
VSCのコンソールでGoのバージョンが確認できたら完了です。
PS C:\home\Go> go version
go version go1.9.2 windows/amd64
PS C:\home\Go>
Windowsで開発とか、昔(20年前)なら許されない事でした。そもそも当時 MPI
のWindows実装なぞありません(今はあるらしいです、なんてことだ)。それにしてもWindowsもOSS方面の開発環境整えるの簡単になりましたよね。いい時代です。
お題
さて、先程の先達のネタはモンテカルロ法を使った円周率の計算になっています。数値計算とかから随分と遠ざかっているので名前は知ってるけどアルゴリズムは覚えてない、で、探したら一発で見つかりました。Google先生えらい。お題としても比較的お手軽だからでしょうかね。
- モンテカルロ法と円周率の近似計算 https://mathtrain.jp/montecarlo
円周率の近似値を計算する方法
モンテカルロ法の具体例として有名なのが円周率の近似値を計算するアルゴリズムです。
モンテカルロ法
1. 1×1 の正方形内にランダムに点を打つ
2. 原点(左下の頂点)から距離が 1 以下なら 1 ポイント,1 より大きいなら 0 ポイント追加
3. 以上の操作を N 回繰り返す,総獲得ポイントを X とするとき,4X/N が円周率の近似値になる
ここで、正方形内とは境界線も含むようです。 [0,0]~[1,1]
内ということですね。
まずは素直に逐次処理で書いてみる
参考サイト:
モンテカルロ法による円周率の推定(その1)
http://text.baldanders.info/golang/estimate-of-pi/
題材がまんまのってます。まぁソースをそのままと言うのも芸がないので、若干改変させてもらいました。
上記サイトに有るように、Goの乱数を発生させる標準的な関数が出してくれるのは [0.0,1.0)
という範囲らしく、 1.0
を出してくれない。そこでちょっと細工して
float64(rand.Int63n(10000001)) / float64(10000000))
という形でやるとよいようですね。すると、正方形内の座標と原点の線分の距離は math/cmplxパッケージ
を使用して
p := complex(float64(rand.Int63n(10000001))/float64(10000000), float64(rand.Int63n(10000001))/float64(10000000))
length := cmplx.Abs(p)
という形で表現出来る、と。
あとは、これのポイントを判断して、合計していく、というのをコードに落とせば良いことに。
参考サイトのをベースにして、こんな感じでしょうかね。
package main
import (
"fmt"
"math/cmplx"
"math/rand"
"time"
)
func main() {
n := int64(100000)
rand.Seed(time.Now().UnixNano())
m := int64(0)
for i := int64(0); i < n; i++ {
p := complex(float64(rand.Int63n(10000001))/float64(10000000), float64(rand.Int63n(10000001))/float64(10000000))
if cmplx.Abs(p) <= float64(1) {
m++
}
}
fmt.Printf("n = %v, m = %v, 4m/n = %v\n", n, m, float64(4*m)/float64(n))
}
並列化
どの部分を並列化するか、題材は単純なので、for分の中身の
- ランダムな点を作成
- 作成した点と原点との距離を計算
- 得点を判定
こんなところでしょうか。今回は単純なのですぐに答えが出ますが、ここを考えるのが難しかったりするんですよね・・・ここを考える部分は今も昔も変わらないようです。
参考にしたページでは、
・作成した点と原点との距離を計算
・得点を判定
の部分を並列化していませんでしたので、今回はこっちまで並列化することにします。
並列処理で重要なのは処理するデータをバラまくmapと、結果を集めてくるreduceですが、 MPI
だと、最初のドラフトではそういうのがなかった気が。つまり、マスターになったノードが集める処理を、自前でシコシコ書くしか無い。でも確か使ってる時にバージョン上がって実装された気がします。さすがに無いと不便ですよね。
Go
の場合は、チャンネルを作ってそこにぶち込むことで勝手にかき集めてくれるみたいですね、便利。凝ったことをしようとすると難しさもあるかもしれないですが、大半の用途はこれで済むのではないでしょうか。
というわけで上のソースを並列化したソースはこちら。
package main
import (
"fmt"
"math/cmplx"
"math/rand"
"time"
)
func main() {
my_rand := rand.New(rand.NewSource(1))
my_rand.Seed(time.Now().UnixNano())
n := int64(1000000)
m := int64(0)
c := culc(my_rand, n)
for q := range c {
m = m + q
}
fmt.Printf("n = %v, m = %v, 4m/n = %v\n", n, m, float64(4*m)/float64(n))
}
func culc(s rand.Source, count int64) <-chan int64 {
ch := make(chan int64, int64(10))
r := rand.New(s)
go func(r *rand.Rand, count int64) {
for i := int64(0); i < count; i++ {
p := complex(float64(r.Int63n(10000001))/float64(10000000), float64(r.Int63n(10000001))/float64(10000000))
if cmplx.Abs(p) <= float64(1) {
ch <- int64(1)
} else {
ch <- int64(0)
}
}
close(ch)
}(r, count)
return ch
}
まぁ比較的楽です。サンプルに習って並列化部分は関数に切り出す形にしました。
MPI
に限らないと思うんですが、当時の並列プログラミングは最初から並列処理として処理全体を考えて書かないとだめで、逐次処理のコードを並列処理に変換しようとすると大変でした。
それに比べると今はだいぶ楽になったというところでしょうか。
実行
以下が実行結果です。
逐次処理バージョンの結果
PS C:\home\Go> .\test.exe
n = 1000000, m = 785733, 4m/n = 3.142932
PS C:\home\Go>
並列処理バージョンの結果
PS C:\home\Go> .\test_p.exe
n = 1000000, m = 786196, 4m/n = 3.144784
PS C:\home\Go>
微妙に両者の数値が違うのは試行回数が少ないのもありますかね。しかし100万回でも3.14までしか出ないもんなんですねぇ。
まとめ
サクサクと環境を作り、ソースを書いて(とはいえ、初なので結構手間取りましたが)、比較的楽に並列化できたと思います。
昔に比べるとだいぶお手軽になりましたね、覚えることも少なさそうだし、どんどん使えば良いと思います。
昔自分がしたような苦労は若い人はしなくていいと思います、ええ、ええ。