280 likes | 425 Views
卒業研究 進捗発表 Lightweight Implementation of Profile-Driven Implicit Parallelization for Haskell. H22-12-22 理学部情報科学科 4 年 新井淳也. 概要 : Haskell で自動並列化. 2 段階かけて効率的な自動並列化を行う プロファイリングによって並列化による性能向上が見込める箇所を分析する 実際にそこを並列化する 自動並列化コンパイラを GHC ベースで実装中 GHC に既存の 機能を活用し簡易な実装を行 う. 背景 : 「 で、どこを 並列化すればいいの? 」.
E N D
卒業研究進捗発表Lightweight Implementation ofProfile-DrivenImplicit Parallelizationfor Haskell H22-12-22 理学部情報科学科4年 新井淳也
概要:Haskellで自動並列化 • 2段階かけて効率的な自動並列化を行う • プロファイリングによって並列化による性能向上が見込める箇所を分析する • 実際にそこを並列化する • 自動並列化コンパイラをGHCベースで実装中 • GHCに既存の機能を活用し簡易な実装を行う
背景:「で、どこを並列化すればいいの?」 • 「どうやって並列化するか」はよく研究されてきている • しかし並列化には「並列化すべき箇所の発見」と「そこを並列化したプログラムの実装」の2段階がある • 後者は楽になりつつあるが前者は手作業任せの研究が多かった • 自力で探してね型 • Cilk, StackThreads, Concurrent ML, Eden, Concurrent Haskell, Glasgow Parallel Haskell, … • 自動で手当たり次第並列化型(特殊なアーキテクチャを対象とする) • Id/pH, Guarded Horn Clauses, …
プロファイリングで「どこ」を発見できるはずプロファイリングで「どこ」を発見できるはず • 並列計算にはオーバーヘッドがある • 闇雲な並列化はプロセッサ間通信を増加させ逆に低速化を招く • ある程度時間のかかる処理の塊を並列化するならオーバーヘッドがあっても高速化に貢献し得る • それをプロファイリングによって発見する • データの依存関係やI/Oのため並列化できない箇所もあるが、それは静的なコード解析で対処できるはず • なのでこの自動並列化はプロファイリングと再コンパイルの2段階を要する
この発想に基づくツールをGHCを基に簡潔に実装する手法を提案この発想に基づくツールをGHCを基に簡潔に実装する手法を提案 • Haskellは純粋関数型言語であり並列化が容易 • Glasgow Haskell Compilerは既にプロファイリングと並列化のための機能を備えている • それらを活用し再発明のない簡潔な実装を与える • 過去の研究は既存のツールや実行時ライブラリに大幅な変更を加えるものが多かった • 我々が独自手法の実装に専念できるということが主な利点
AbstractとIntroduction Method
Haskellは遅延評価 • Haskellは遅延評価を行う言語 • 値は必要になるまで計算されない • 「サンク(thunk)」に包み値が要求されるまで評価を遅らせる • 引数を取らないクロージャのようなもの • サンクの評価が一度行われると結果の値を記録し、次回以降はそれを返す • 基本的に評価は1回だけ
GHCでのプロファイリングはSCCプラグマの挿入で行うGHCでのプロファイリングはSCCプラグマの挿入で行う • {-# SCC “name” #-} <expr>をソースに挿入してビルドすることで<expr>の評価回数や処理時間などを計測する実行ファイルが生成される • 実行時オプションを与えることで結果をファイルに出力させることができる • SCC = Set Cost Centre • 例 let a = {-# SCC “fib-30” #-} fib 30 b = {-# SCC “fib-31” #-} fib 31 in ...
GHCでの並列化はpar関数の挿入で行う • par :: a -> b -> bはGlasgow Parallel Haskell (Trinder, et al. 1998)で実装された関数 現在はGHCに取り込まれている • 第1引数をSparkさせ第2引数はそのまま返す • Spark:(ここでは)サンクの評価を並行に行わせること • Sparkしたサンクはプールに入れられ手の空いたプロセッサにより評価される
今回実装する機能の中核:時間のかかる処理を自動的にSparkさせる今回実装する機能の中核:時間のかかる処理を自動的にSparkさせる • 手作業で並列化するときと考えることは同じ • SCCを挿入して、時間はかかるが結果をすぐには使わない処理をプロファイルの分析で探し出し、par関数を挿入してSpark • これが自動化されればめでたく自動並列化! • 「時間がかかる」とはSparkのオーバーヘッドを相殺できそうな程度の時間をいう • 具体的な数値は実地でベンチマーク結果から決定
時間計測とSparkはサンクごとに • 普通にHaskellのソースをコンパイルすればサンクを生成するようなコードを生成する • このサンクごとに処理を行うのが最も自然で簡易 • Sparkはサンクを対象として行うもの • SCCによる時間計測も内部的にはサンク単位 • 積極的にサンクの生成箇所を制御する手もあるが、基本的にサンクの生成はコストが大きいので少ないほうが良い • ではサンクになる箇所を知るにはどうするか?
Method Implementation
GHCの中間言語CoreSystem Fの一種・Haskellのサブセット Rec { fib_rdj :: GHC.Types.Int -> GHC.Types.Int GblId [Arity 1] fib_rdj = \ (ds_siE :: GHC.Types.Int) -> case ds_siE of wild_siJ { GHC.Types.I# ds1_siH -> case ds1_siH of _ { __DEFAULT -> let { sat_siU :: GHC.Types.Int LclId [] sat_siU = • コンパイルオプションにより処理過程のコードをダンプ可能 • Coreを読み込んでコンパイルすることは(今のところ)できない
SCCやparの表現はCoreでもほぼ同じ GHC.Num.+ @ GHC.Integer.Type.Integer GHC.Num.$fNumInteger (__scc {fibzm30 main:Main} fib1_rlb (GHC.Integer.smallInteger 30)) (__scc {fibzm31 main:Main} fib2_rld (GHC.Integer.smallInteger 31)) • par#はparがインライン化されたもの • par# :: a -> Int#第1引数をSparkし1を返す Main.main2 = case GHC.Prim.par# @ GHC.Integer.Type.IntegerMain.main_a of _ { __DEFAULT -> ...
サンク生成箇所はCorePrepで判明 • この時点のCoreでletにより束縛されているものが将来的にサンクになる • 次の中間言語、STGにおいて引数0のクロージャがサンクとなる(The GHC Commentaryより) • “Convert to STG”パスのソースを読みCoreとSTGのクロージャ生成の関係を調査し確認した • 1つ例外があるが単純な条件なので判別可能
Coreでのコード変換でSCCやparを挿入 let <binder> = __scc <body> in <expr> • GHCに既存の機能のお陰でこんなにも簡単 • 現時点ではプロファイルの分析は実装できていないので、サンクを全てSparkさせてしまう • この変換処理を“CorePrep”の直後に挟んだ let <binder> = <body> in <expr> let <binder> = <body> in case GHC.Conc.par# <binder> of _ { __DEFAULT -> <body>
let sat_siR = let sat_siS = let sat_siT = GHC.Types.I# 2 in GHC.Num.- @ GHC.Types.Int GHC.Num.$fNumInt wild_siGsat_siT in fib_rdfsat_siS in ... 変換 let sat_siR = let sat_siS = let sat_siT = GHC.Types.I# 2 in case GHC.Prim.par# @ GHC.Types.Intsat_siT of sat_sj8 { __DEFAULT -> GHC.Num.- @ GHC.Types.Int GHC.Num.$fNumInt wild_siGsat_siT } in case GHC.Prim.par# @ GHC.Types.Intsat_siS of sat_sj9 { __DEFAULT -> fib_rdfsat_siS } in case GHC.Prim.par# @ GHC.Types.Intsat_siR of sat_sja { __DEFAULT -> ...
最適化でサンクが減る=並列性減少のジレンマ最適化でサンクが減る=並列性減少のジレンマ • 並列化は当然高速化のためにやるのであって、コンパイラの最適化は有効にしたい • しかしサンク生成は本来コスト源なので最適化でサンクの数は削られる • サンクが作られなければSparkする機会が減る • 例えば(効率は良くないが) fib n = fib (n–1) + fib (n–2)と書いた時、最適化無しなら fib (n-1)と fib (n-2)でサンクが作られるが、最適化有りだと作られない
解決法1:サンク数を減らす最適化だけ無効にする解決法1:サンク数を減らす最適化だけ無効にする • 最適化処理の中身をよく調べれば分かる…? • 正格性解析が内容的にそれらしい雰囲気しかしコンパイル時に -O -fno-strictnessを指定してもサンク数は減った • プロファイル解析の結果Sparkすることにしたサンク以外はなくなってくれたほうが良いので、そもそもこの方法は好ましくない。却下
解決法2:変換を施した後もう一度最適化をかける解決法2:変換を施した後もう一度最適化をかける • SCCやparがあるときGHCはそこをサンクにする • なのでそれらが挿入された状態のCoreをSimplifyさせれば並列性を維持しつつ適用可能な最適化が行われる? • しかしCorePrepまで進まないとどこがサンクになるかは分からない
Simplifyの前にCorePrepまでをゴッソリ挿入してみたが…Simplifyの前にCorePrepまでをゴッソリ挿入してみたが… • 最適化オプションを無効にした状態でSimplify~CorePrepを走らせられればサンクが多いままのCoreを入手できる • ならばSimplifyの前に全部入れてしまおう • コンパイラの内部パラメタを弄ってこの間だけ最適化を無効にする
問題点1:最適化オプションの影響を除けなかった問題点1:最適化オプションの影響を除けなかった • コンパイルオプション –O無しでも有りでもParallelizeパスへの入力は同じ…にできなかった • 並列化には-Oを指定しなかった時のCorePrepの出力が欲しい • ここで得られた出力は既にサンクが減少したものだった • 最適化の設定は予想より複雑で、Simplifier’直前でちょっと弄っても駄目 • コンパイルオプションを格納するレコードがあり、最適化レベルは整数でそこに記録されている • 内部では最適化レベルに応じて最適化フラグを入/切している
問題点1(続き):…なので何を調べるか • 後にDesugarでも最適化オプションが影響すると判明 • パイプライン突入直前で強制的に最適化レベルを0に書き換えておくとParallelへの入力は同じになった • もしかするとParse前ではなくDesugar前などでも大丈夫かも? • つまり最適化オプションが影響するより前の時点で介入して書き換えてしまえばよい • どこで書き換えるのがよいかについて調査中
問題点2:コンパイル時にpanic • -Oと-spark-thunksを同時に指定するとコンパイル時にpanic!-spark-thunks: parの挿入を指示する(自作) • 直接の原因:Simplifier内の関数名置換処理に漏れが発生存在しない関数を参照するコードが発生 • Main.fibをMain.$wfibに途中で置換するが、Main.fibの呼び出し箇所がMain.$wfibに変わらない
問題点2(続き):考えられる原因 • “問題点1”派生 • 最適化の有効無効を切り替える中で必要な情報が欠落したのではないか • Parse…Desugar : この範囲は –O • Simplify’…Parallelize : この範囲は –O無し • Simplify…Code generation : この範囲は –O • その他 • 最終的にはその処理を行うコードを読むしか