エンジニア的なネタを毎週書くブログ

東京でWebサービスの開発をしています 【英語版やってみました】http://taichiw-e.hatenablog.com/

「2番ペゲーロをみんなでやってみる」ために計算量と戦った話 #bpstudy

ありがたいことに、前回に引き続き今回も、Baseball Play Studyで発表させて頂く機会をいただきました。

私の発表

Twitter, 私の発表近辺
twitter.com

Twitter, 全体のまとめ
togetter.com

計算量との戦い

今回、「シミュレータ」をグルグルまわして最適な打順を探す ということを行ったのですが、計算量がネックになりました。
f:id:taichiw:20180421070604p:plain
今回のシミュレータでは、何種類かのループを回す必要がありました。

1 1シーズン、143試合分の試合を実行
2 1を、確率を収束させて精度を上げるために一定回数以上実行
3 2を、想定される打順の組み合わせ分だけ実行

さらに、同様のシミュレーションを、各球団の選手、かつ各球団ごとの打順分布に当ててみる となると、

4 3を、12球団分の選手に対して実行
5 4を、12球団それぞれの打点分布にたいして実行

が、必要となります。

ちょうど「本業」でも似たような課題を抱えており、勉強も兼ねて挑戦してみることにしました。

ループ回数を減らす

結果の精度を落とすことと引き換えに、繰り返し回数を減らすアプローチです。発表中に「ズル」と言っていたものです。

確率の収束を諦める

今回のシミュレータは確率を使っているため、施行のたびに結果が変わる、ブレが大きいものでした。
そのため、ある程度の回数実行して、結果を収束させる必要がありました。
何度か試行錯誤した結果、1000シーズン(= 143 ✕ 1000 試合)だと、年間総得点の誤差が1ー2点に収まるということがわかりました。

しかしながら、これでは数時間かかってしまう状況だったため、今回は速度優先で、100シーズンのループとしました。

施行のパターンを諦める

元々やりたかったことは、9人を並び替えた9!通りの打順をすべて試すこと。(もっというと、支配下選手70人から作られる打順をすべて試すこと)
しかし、1打順1秒としても、9!=362,880秒=丸4日間係る事になってしまってとても終わらない。
そこで、「ありえなそう」なパターンを(強めに)削ることで、55通りの組み合わせまで落としました。
f:id:taichiw:20180421070650p:plain
f:id:taichiw:20180421070729p:plain

並列化する

今回はJavaで実装していたので、Parallel Streamがある!ということで試してみました。
とても簡単に、かつ確実に速くなりました。
ただ、悩んだのが
f:id:taichiw:20180421061723p:plain:h300
このように、外のループと中のループとある場合にどこを並列化するのが良いのか。

例えば4コアCPUの場合、
中のループにParallel Streamを使うとそこだけマルチコアで処理されるため、このように断続的にCPUが100%使われるようになり
f:id:taichiw:20180421062140p:plain:w300

外のループに使うと、ほぼ常時CPUが使われるようになる(そのかわり、1巡の処理は先の場合より遅くなる)
f:id:taichiw:20180421062247p:plain:w300

そして、外と中と両方に使ってしまうと、既に外のParallel StreamでCPUが使われているため、結局外に使った場合とほぼ同じ
f:id:taichiw:20180421062247p:plain:w300

となると思います。(たぶん)

今回のようなバッチ処理の場合は、なるべく外側から並列化したほうがCPUを効率的に使え、処理時間を少しでも短くできそうだなぁ…
と思ったのですが、実際のトコロどうなんだろう。

外に投げる

最後の手段として、世の中にあるもっと高性能なコンピュータを使ってやろうじゃないかということで。
AWS LambdaがJavaも使えるということで試してみました。
qiita.com
こちらを参考にさせてもらいました。

はじめは、全処理をまとめてぽーんと投げてみたのですが

…遅い。とても遅い。

メモリ割り当てをMAXにしても、手元のPCで動かしている方がよっぽど速い。
Parallel Streamもまともに動いていないように見えました。(おそらく1プロセスに割り当てられている、仮想的なコアが1コアなんだと思います)

そこで、方針を転換。
AWS Lambdaは1000並列まで可能とのことで、それならば、多少処理が遅くても、4並列が限界な手元のPCよりはずっとマシだろう!と判断。
とにかく細かい単位で並列に投げまくることにしました。

普通のParallel Streamは可動しているマシンのコア数分しかスレッドが作られないため、この辺を参照。
qiita.com

※ただし、本当に1000並列でタスクを呼び出すためには1000スレッド作るか、Non Brockingな実装にする必要があります。今回はそこまで手を出せなかったので、12並列で終了。

プログラムをImmutableに書いておくの、めっちゃ大事

今回のように「プログラムの途中を外に投げる」ようなことをするにあたって、その箇所のコードが独立していないとなかなか厄介なことになります。
引数をメソッド内でガチャガチャ書き換えるような書き方、ダメ、絶対。

AWSを始めるなら…

この辺読んでから始めたほうがいいです。いろいろ面倒ですが…。
AWS 乗っ取り - Google 検索
従量課金コワイ。


続き書きました。
taichiw.hatenablog.com