Slide 1

Slide 1 text

並列対決 Elixir × Go × C# オマケでScala、node.js Created by Enpedasi/twinbee ( [@enpedasi] ) 2018/2/23 powered by Marp

Slide 2

Slide 2 text

自己紹介 @enpedasi (えんぺだーし) qiita : twinbee フリーランスエンジニア 中2の時にZAT SOFT/大名マイコン学院より MZ-700向けゲームレフュージー発売 BGM : Lo hiphopがお気に入り

Slide 3

Slide 3 text

概要 Elixir速いっていうけど、実際どうなの? Go/Scala Akka等 並列に強い処理系の中での優位点 は? オープン系ばかり話題になるけど、Microsoftを見 てみぬふりはダメだよね? 初見でどこまで並行プログラミングができる?

Slide 4

Slide 4 text

ストリーム集計対決 巨大CSVファイルを並列集計 各言語のストリームを使用 ストリームを制するものは、サービスを制する

Slide 5

Slide 5 text

- 登壇者のレベル 言語 経 験 node.js 納品実績あり C# 納品実績あり/非同期開発経験なし Elixir 3年間Watch Scala ハンズオンでWebアプリ作った (Play/Skinny) Go 経験なし ここから作っていくよ! ※ 一応 C / C++ / アセンブラ 10年以上 Delphi 20年経験は あります

Slide 6

Slide 6 text

基礎用語の確認

Slide 7

Slide 7 text

並行・並列 並行処理 (Concurrent) CPU数・コア数の限界を超えて複数の仕事を同時 に行うこと。 シングルスレッドでも該当する。 並列処理 (Pallarel) 複数のプロセッサやコアを使って、仕事を同時に 行うこと。 参考 ASCII倶楽部 Go言語と並列処理

Slide 8

Slide 8 text

ノンブロッキング・非同期 nginxやnode.jsが話題になった時代のテーマ ノンブロッキング にくじゃがをつっつきながら完成を確かめる。 つっつきながら別の料理を捌ける。 非同期 電子レンジに入れてチンと鳴るのを待つ。その間 別の料理に取り掛かる。 厳密に定義を追ってもしょうがないので、並行・ 並列と区別がつけばOK

Slide 9

Slide 9 text

ストリーム 文字・画像・動画などが延々と流れていくイメー ジ。始まりと終わりがない。 ストリームは加工部分のみを読み取り、処理する ので最低限のメモリしか使わない。

Slide 10

Slide 10 text

ストリーム 問題点 サービスをストリームの加工機としてとらえる と、入力をある大きさでぶった切って処理する必 要がある(時間やサイズ)。 ぶった切られたデータは、IOデバイスの都合で切 られるので、加工しやすい型(行・画像)に持っ ていくには手間が必用 流れ待ってくれないので、バッファリングや再送 指示が必用 ⇒ 処理が複雑化

Slide 11

Slide 11 text

マルチタスクモデルとGC Elixir Go C# Scala(JVM) モデ ル 軽量プ ロセス 軽量ス レッド スレッド プール? スレッドプ ール? GC 単位 プロセ ス 共有メ モリ 共有メモ リ 共有メモリ ※スレッドは共有メモリを使用するので、GCすると 全体が止まる

Slide 12

Slide 12 text

非同期ストリームのFramework Elixir Genstage/Flow Scala Akka C# TPL DataFlow Node.js Producer Source (ISoureBlock) reader Produce- Consumer Flow (IPropergatorBlock) (pipe) Consumer Sink (ITargetBlock) writer Scala Akka は ErlangのActor リスペクト GenStage は ScalaAkka Streams リスペクト C# TPLもActorモデル(上記の表は無理やり)

Slide 13

Slide 13 text

Elixirの非同期ストリーム GenStage FlowのベースになっているBehavior GenEventの改良版 Flow GenStageがコールバック主体で実装していくのに 対し、より直観的に書ける。

Slide 14

Slide 14 text

Scala Akka Stream GenStageやFlowがインスパイアされたライブラリ Lightbend社(旧Typesafe社)提供 JVMで動作 Scala or Java対応 豊富なドキュメント Pony langageの資料では Erlangと同等性能 twitterで#Akka what's wrong?って叫んだら中の人 が10分ぐらいでPR送ってくれた(Thak you for ktoso! )

Slide 15

Slide 15 text

Akka Streams

Slide 16

Slide 16 text

Akkaのドキュメントの一部

Slide 17

Slide 17 text

GOの並列実装のバリエーション Channel make時にBackpresure指定可 Select 調停 複数のチャネルからの結果を判断 Goroutine 普通のファンクションの前にGOと書けば、非同期 実行を行う

Slide 18

Slide 18 text

C#の並列実装のバリエーション Task async / Tasck await TPL DataFlow Actorモデルによる非同期ストリーム Parallel Linq コレクションの操作を並列で行う

Slide 19

Slide 19 text

コード実装

Slide 20

Slide 20 text

Elixirの実装(抜粋) Source Code on Github 上記SourceCodeのElixirFlowOrgはリファクタリン グ前の関数です

Slide 21

Slide 21 text

result = filename |> File.stream! # データクレンジング |> Flow.from_enumerable() |> Flow.map( &( String.replace( &1, ",", "\t" ) ) ) # ①CSV→ |> String.replace( "\r\n", "\n" ) # ②CRL |> String.replace( "\"", "" ) ) ) # ③ダブ # 集計 |> Flow.map( &( &1 |> String.split( "\t" ) ) ) # ④タブで分割 |> Flow.map( &Enum.at( &1, 2 - 1 ) ) # ⑤2番目の項目を抽出 |> Flow.partition |> Flow.reduce( fn -> %{} end, fn( name, acc ) # ⑥同値の出現数を集計 -> Map.update( acc, name, 1, &( &1 + 1 ) ) end ) |> Enum.sort( &( elem( &1, 1 ) > elem( &2, 1 ) ) ) # ⑦多い順で

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

Goの実装(抜粋) Source Code on Github.

Slide 24

Slide 24 text

最初に書いたコードでは1行リード毎にGo routine を起動していた さすがにそれはまずいので、Go Routineをワーカ ーとしてマネージできるライブラリを探したが、 「公式」的なものはなかった。 次の記事を参考に、ワーカーの実装を行った。参 考 ⇒ golang の channel を使って Dispatcher- Worker を作り goroutine 爆発させないようにする

Slide 25

Slide 25 text

Goでの実行イメージ

Slide 26

Slide 26 text

チャネルによるディスパッチ func (w *worker) start() { go func() { for { // register the current worker into the dispatch pool w.dispatcher.pool <- w select { case v := <-w.data: if bulkstr, ok := v.([][]string); ok { wordMap := processData(bulkstr) w.dispatcher.sink <- wordMap } w.dispatcher.wg.Done() case <-w.quit: return } } }() }

Slide 27

Slide 27 text

バルクデータをマップ化 func processData(r [][]string) map[string]int { var m = map[string]int{} for _, rec := range r { m[ rec[aggColNo] ]++ } return m }

Slide 28

Slide 28 text

// チャネルからクラスタ化された集計結果を集める func waitAndSum(sumMap map[string]int, d *Dispatcher, quit for { select { case rec := <-d.sink: for mk, mv := range rec { sumMap[mk] += mv } case <-quit: fmt.Println("quit") return sumMap default: } } }

Slide 29

Slide 29 text

Scala Akka Streams 実装 (抜粋) Source code on Github

Slide 30

Slide 30 text

source .via(CsvParsing.lineScanner()) .via(CsvToMap.withHeaders("firstname", "lastname", "gender" .map(_.map(r => (r._1, r._2.utf8String))) // Map( birthday - .filter(rec => rec.get(grp_col) != None) .groupBy(30, r => r(grp_col)(0)) // 頭一文字でグループ分け .async .fold(acc_empty) { (acc: Map[String, Int], rec: Map[String val word = rec(grp_col) val cnt = acc.getOrElse(word, 0) acc.updated(word, cnt + 1) } .mergeSubstreams .via(Flow[Map[String, Int]].fold(acc_empty) { (acc: Map acc ++ rec.map { case (k, v) => k -> (v + acc.getOrElse(k, }) .runWith(Sink.foreach(e => {resultMap = e})) .onComplete(done ⇒ { val msec = (System.currentTimeMillis - start) println(done) resultMap.toSeq.sortWith(_._2 > _._2).take(10).foreach(pri

Slide 31

Slide 31 text

GroupByの第一引数には、サブストリームの数が 入る。集計キーが多い場合はSQL的に使えない。 ストリームをうまく分割するアイデアが見つから なかったので、集計キーの1文字目でグループ化 効果がありそうな場所に.asyncを配置しても、1.5 倍遅い結果だった

Slide 32

Slide 32 text

Node.js(抜粋) SourceCode on Github const parser = csvParse({ delimiter: ',', relax_column_count: true }); parser.on('data', (data) => { const key = data[1] sumMap.set(key, sumMap.has(key) ? sumMap.get(key) + 1 : 1) }); ストリームをラップしたcsv-parserを使用 CSV parserが行ごとにdataイベントを返すので、 都度集計処理

Slide 33

Slide 33 text

C# dotnet core 2.0 実装 抜粋 SourceCode on Github static private async Task> Rea { using (FileStream sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096 )) { StreamReader sr = new StreamReader(sourceStream) string text; while ((text = await sr.ReadLineAsync()) != { var wd = text.Replace("\"", string.Empty).Spli wordMap.AddOrUpdate(wd, 1, (_key, val) => { } return wordMap; } }

Slide 34

Slide 34 text

・async/awaitによる非同期タスク制御 ・StreamReader.readAsyncというぴったりの機能 ・CocurrencyDictionaryという、これまたぴったり なKVコレクション

Slide 35

Slide 35 text

対 決 結 果

Slide 36

Slide 36 text

Windows 10 Home Edition Corei5 [email protected] Hz (Kabylake) 2コア4スレッド 16GMemory

Slide 37

Slide 37 text

Windows10 Home edition Corei5 [email protected] Hz 2コア4スレッド Elixir Go C# Scala Node.js 30 万 行 0.3 1.2 1.5 3.7 10.5 300万行 19 12 15 27 107 1200万行 86 50 47 101 433 単位(秒) C#とGoが速い ※ScalaはIntelliJ環境下のスコア

Slide 38

Slide 38 text

Windows 2008 Server Intel Xeon(R) CPU E31230 (32n 2011Q2)@ 3.2GHz 4コア8スレッド 8GMemory

Slide 39

Slide 39 text

Windows Server 2008 R2 8GB Intel Xeon(R) CPU E31230 (32n 2011Q2)@ 3.2GHz 4 コア8スレッド (On-premises) Elixir Go C# Scala Node.js 30万行 300万行 1200万行 49 43 35 コア数が増えたことで、85秒⇒49秒へ猛追 C#も速度上がる。Windows上ではC#最速

Slide 40

Slide 40 text

Google Compute Engine Debian 9 vCPUx8 16GByte

Slide 41

Slide 41 text

ここでLinuxでC#に問題発生 Unhandled Exception: System.ArgumantException The index is equal to or greater than the length of the array, or the number of elements in dictionnary is greater than the available space from idex to the end of destination array.

Slide 42

Slide 42 text

GCE Debian 9 vCPU 8 16GByte Elixir Go C# Scala Node.js 30万行 0.09 1.2 Crush - 13 300万行 9.6 11.4 Crush 28 137 1200万行 40.1 44.8 Crush 116 549 ElixirがGoを抜きさる C#は例外クラッシュ Node.jsはWindowsより1割ほど速度低下 ScalaはCPUあんまり使ってない。プログラムが問 題かも。

Slide 43

Slide 43 text

Docker 2GB/2Core Debian Microsoft公式 dotnetcoreコンテナ Elixir Go C# Scala Node.js 30万行 0.3 2.2 Crush 300万行 29 23 Crush 1200万行 118 Killed Crush C#は例外クラッシュ Goはメモリ不足でTask Kill

Slide 44

Slide 44 text

まとめ

Slide 45

Slide 45 text

Elixir/Go/C# ともにNode.jsの5 - 10倍の速度 Elixir Flow ⇒コア数が多いほど真価を発揮 ⇒コード量の少い!学習コスト少ない!性能高い! どの環境でも安定度が抜群 ⇒Shift-JIS対応や商用RDBとの接続に課題 GO ⇒並列入門用としては最適 ⇒GoroutineはGCされないので、メモリ管理が負 担。1200万件では8GBのメモリを消費 ⇒コード量がかさむ。大規模PJでは辛そう ⇒VS CodeのIDE力が強力

Slide 46

Slide 46 text

Scala Akka ⇒InnteliJかしこい。型はあまり気にしなくてOK ⇒なにより実績がある。ドキュメントが手厚い ⇒環境構築が辛い。 ⇒IntelliJやDockerコンテナ使っている間はよい が、そこから外れるとコストが大きい ⇒コンパイル時のメモリ消費大。今回のPGでも 2.5Gほどメモリを消費する ⇒Oracle RDBを重用してる身としてはOracleの企 業姿勢がリスク(JVM) C# ⇒Windowsでは最速。Dotnet core動向に注目 ⇒なんでも揃えます感 ⇒VisualStudio賢い。でも重い

Slide 47

Slide 47 text

Elixir Go C# Scala Node.js パフォーマンス B+ ~A A A B+? D ポーテーション 〇 〇 ? 〇 〇 安定度 S B A A A 環境構築コスト A S B C A 学習コスト B+ A B+ B- A 記述量 S C A S A メンテナンス性 A B A A A ドキュメント B+ A A S A Elixirの安定度がSなのはGCの仕組による(プロセ ス単位)

Slide 48

Slide 48 text

Enjoy Elixir Programming! ご清聴ありがとうございました