More Related Content Similar to Ruby で高速なプログラムを書く (20) Ruby で高速なプログラムを書く2. 自己紹介:遠藤侑介
• Ruby コミッタ(2008年~)
– Rubyのテストを増強した
– コードカバレッジ測定機能を
実装した
– キーワード引数を実装した
– Ruby 2.0 リリースマネージャ
だった
– 最近は何もしてない
2
’06下 ’07上 ’07下 ’08上
60
70
80
90
100
coverage(%)
70%
85%
C0カバレッジ遷移
3. と私
• 立ち上げの時に @chezou さんに相談を受けた
• 初期に数回だけ参加した
• Kawasaki.rb #005 (2013-10-23)で発表した
– 以上(すみません)
• ちなみに Kawasaki.rb #005 で発表したものは
3
4. eval$s=%q(eval(%w(B=92.chr;N=10.chr;n=0;e=->(s){Q[Q[s,B],?"].gsub(N,B+?n)};E=->(s){'("'+e[s]+'")'};d=->(s,t=?")
{s.gsub(t){t+t}};Q=->(s,t=?$){s.gsub(t){B+$&}};puts(eval(%q(%(objectXQRXextendsXApp{H("#{e[%((displayX"#{e[%(Hf
X%sX"#{Q[e["TranscriptXshow:X'#{d[%(putsX [regsub X-allX{. }X"#{Q[e[%[intXK(){sJXs=#{E[%(withXAda.Text_Io;pro
cedureXqrXisXbeginXAda.Text_Io.P ut_Li ne(" #{d[ %(BEGINXH("#{d[%(BEGIN{s=#{E[%(forXbXinXSystem.Text
.ASCIIEncoding().GetBytes(#{Q[ E[" # include<stdio.h>`nintXK(){puts#{E["#includ
e<iostream>`nintXK(){s td::c o ut<<#{E[%(classXProgram{publicXstaticXvoidX
Main(){System.Console .Wr ite(#{E[%((defnXf[lXr](if(>(count
Xr)45)(lazy-seq(cons ( str"XXXX^""r"^"&")(fXl"")))(let[c(
firstXl)](ifXc(f(ne xtXl)(if(=XcX^")(strXrXcXc)
(strXrXc)))[(str"X XXX^""r"^".")]))))(doall(map
X#(Hln(str"XXXXXX XX"%1))(lazy-cat["IDENT
IFICATIONXDIVISI ON.""PROGRAM-ID.XQR.""PR
OCEDUREXDIVISI ON."]#{(" console.log"+E[%((wr
ite-line"#{Q [%(X:XAX."XXXXXXXXX"X;X:X BXAX."XWRITE(*,*)'"XA
X;X:XCXBXTY PEX."X'"XCRX;X:XDXS"XprogramXQR"XC XS^"XHX^"(&"XCXS^"X#
{e[%(pack ageXK;import("fmt";"sJs");funcXK(){fmt.Pr int("H^x27"+sJs.Re
place("# {e[e[%(importXData.Char`nK=putStrLn$"procedure XK();write(^"DO,1
<-#"++s how(lengthXs)++fXsX1X0;f(x:t)iXc=letXv=foldl(^aXx- >a*2+(modXxX2))0
$takeX 8$iterate(flipXdivX2)$Data.Char.ordXxXin(ifXmodXiX4<1 then"PLEASE"els
e"")+ +"DO,1SUB#"++showXi++"<-#"++show(mod(c-v)256)++"^^n"++fX t(i+1)v;f[]_X_
="PL EASEREADOUT,1^^nPLEA SEGIVEUP^");end";s=#{E[%(.classXpublic XQR`n.superXja
va/l ang/Object`n.methodX publicXstaticXK([Ljava/lang/SJ;)V`n.lim itXstackX2`ng
ets taticXjava/lang/Syst e m/outXLjava/io/PrintStream;`nldcX"#{e[% (classXQR{pu
bli cXstaticXvoidXK(SJ[] v){ SJXc[]=newXSJ[8000],y="",z=y,s="#{z=t= (0..r=q=126)
.ma p{ |n|[n,[]]};a=[];%(@s =inte rnalXconstant[#{i=(s=%(PRX"#{Q["H"+E[% (all:`n`t@H
fX% sX"#{e[%(.assemblyXt {}.meth odXstaticXvoidXMain(){.entrypointXldst r"#{e["varX
u=re quire('u til');u.H('#import<stdio. h>^n');u.H(
#{E[% (in tXK(){puts #{E["H_ sJ"+E["Hf"+E[ %(say"# {e["programXQR(output);begi nX#{([*%($_
="#{s= %(<?phpXecho"#{Q [e["i ntXK(){write#{E ["qr: -write('#{Q[e[%(forXlXin#{E[ e[d[%(eval$
s=%q(#$s) )]]]}.split("^^n") :H( 'cat("sayX^^"'+l+ '^^ "^^n")'))],?']}'),nl,halt."]} ;returnX0;}
"]]}"?>);(s+N*( -s.size%6)).byt e s.map{|n|"%07b"%n}. j oin.gsub(/.{6}/){|n|n=n.to_i(2 );((n/26*6+
n+19)%83+46).ch r}}";s|.|$n=ord$ &;substrXunpack(B8,ch r$n-($n<58?-6:$n<91?65:71)),2|e g;s/.{7}/0$
&/g;HXpackXB.le ngth,$_).scan(% r (([X.0-9A-Za-z]+)|( . ))).reverse.map{|a,b|(b) ? "s//chrX#{b
.ord}/e":"s//#{ a}/"},"eval"] *"X xX").gsub(/.{1,25 5}/ ){|s|"write( ' # {s}');"}}
end."]}"`nend`n) ]]]};returnX 0;}). trXB,?@]}.repla ce(/@ /g,SJ.fr o m CharCode
(92)))"]}"callXv oidX[mscor lib]Sys tem.Console:: Write(s J)ret})]}")],/ [ X ^`t;"()
{}`[`]]/]}`nBYE)) .size+1}X xXi8]c"#{s.g s u b(/[^`
n"]/){B+"%02`x58" %$&.ord}}^00"declareX i32@put s(i8*)defineXi32@K(){star t:%0=cal l X i32@pu
ts(i8*Xgetelementp trXinbounds([#{i}XxXi 8]*@s ,i32X0,i32X0))retXi32X0}).bytes{|n | r ,z=z[
n]||(a<<r;q<5624&&z [n]=[q+=1,[]];t[n])}; a<< r;t=[*43..123]-[64,*92..96];a.map{|n | t[n/7
5].chr+t[n%75].chr}* ""}";intXi=0,n=0,q=0, t ;for(;++n<126;)c[n]=""+(char)n;for(; i <s.le
ngth();){t=s.charAt(i );q=q*75+t-t/64-t/92 *5-43;if(i++%2>0){y=q<n?c[q]:y;c[n+ + ]=z+
y.charAt(0);System.out .H(z=c[q]);q=0;}}}}) ]}"`ninvokevirtualXjava/io/PrintStr e am/H
ln(Ljava/lang/SJ;)V`nre turn`n.endXmethod)+N]})].trXB,?@]}^x27^n","@","^^",- 1))})]} "XDU
PXFORXS"X&A,&"XCXNE`x58TX S^"X&A)^",&"XCX0XDOXBX."X&char("XCOUNTX.X."X),&' "XCRXLOOP XS^"
X&^"^""XCXS"XendXprogramXQ R"XCXAX."XSTOP"XCRXAX."XEND"XCRXBYEX;XDX), /([^"])/]}"))]).gsu b(/.
+/){%((cons"DISPLAY"(f"#{e[$ &]}""")))}}["STOPXRUN."])))).trXB,?~ ]}.Replace("~","^^")); }})
]};}"]};returnX0;}"]]}):HXjoin( ['+'forXiXinXrange(0,b)],"") +".>").trXB,?!]};gsub(/! /,"
^^",s);HXs})]}")END)]}");endXqr;) ]};intXi,j;H( "moduleXQR;initialXbeginX");for(
i=0;i<s.length;i++){H("$write(^"XXX") ;for(j=6;j>=0;j--)H((s[i]>>j)%2>0?"
^^t":"X");H("^^n^^t^^nXX^`");");}H("$disp lay(^"^^n^^n^");endXendmodule");returnX0
;}].reverse],/[`[`]$]/]}"X^x60.&]k),?']}';cr"]]} ")]}")).gsubXB*8,?|]}".replaceAll("^^|","#{B*32
}"))})).gsub(/[HJK^`X]/){[:print,0,:tring,:main,B*2,0,B,?¥s][$&.ord%9]})))*"")#_buffer_for_future_bug_fixes_#_b
################### Quine Relay -- Copyright (c) 2013 Yusuke Endoh (@mametter), @hirekoke ##################)
5. • とある Ruby プログラム
• 実行すると Scala プログラムが出力される
• 実行すると Scheme プログラムが出力される
• …
• 実行すると REXX プログラムが出力される
• 実行すると元の Ruby プログラムが出力される
• ... というプログラム(50 段階の Multi-quine)
• (多分)世界初、50言語を使用するプロジェクト
• https://github.com/mame/quine-relay/tree/50
10. • Ruby開発チームの最近のスローガン
– “Ruby3 will be 3 times faster than Ruby2”
• 条件[Matz の RubyKaigi 2015 の発表より]
– 2020年ごろ
– 比較対象は Ruby 2.0
– ベンチマークは開発チームが選ぶ
• 小さいが人工的でないもの
– 省メモリよりスピード重視
10
チート?
11. 開発者の悩み
• あらゆるプログラムを 3 倍速くするのは困難
– 今の Ruby は(スクリプト言語にしては)すでに相当高速
– 高速化すべきプログラムが高速になるようにしたい
• 『手軽』に試せるベンチマークプログラムがない
– Ruby コア開発者 = C 言語マスター ≠ Ruby ヘビーユーザ
– Rails のセットアップとか知らない
11
15. 開発動機
• Ruby3x3を煽るベンチマーク候補
– Ruby 2.0 で 20 fps で動く Ruby 3.0 で 60 fps?
– CPU律速で現実的なベンチマーク
– 最適化ニンジン(Optimization Carrot)
• もう1つの動機:
Rubyで無理そうなことをやってみたかった
– NESの解像度:256 x 240 ピクセル x 60 fps
– ループ以外のタスクを 0.8 秒で?
(256*240*60).times do |i|
ary[0] = 0
end
0.2 秒
15
16. 開発経緯
• 10年前:実装を試みるも断念
– 当時はRubyもCPUも遅すぎ、NES解析情報もいまいち
• 2015/11/08:大江戸 Ruby 会議 04
– Ruby で NES ROM を作るフレームワーク burn を見る
– http://wiki.nesdev.com/ を発見
• 2015/12/11-13:RubyKaigi 2015
– Ruby3x3 を聞き、一通り実装して 3 fps 程度になる
• 2015/12/29-31:冬休み前半に 20 fps 達成
• 2016/01/01-03:冬休み後半に 60 fps 達成
• 2016/04/01:公開
16
17. の関連研究
• Ruby でファミコンプログラミング
(takkaw, 2007)
– NES ROM でプレゼン
• Nario:MRI の段階的 GC のデモ
(authornari, 2008)
– マリオ風ゲームのもたつき現象で
リアルアイム性をデモした
• Burn (remore, 2014)
– Ruby で NES ROM を作れる
フレームワーク
17
18. のアーキテクチャ
CPU GPU
Program ROM Bitmap ROM
Cartridge
NES
RAM
(2 kB)
VRAM
(2 kB)
control
read
read/write
read
render
read/write
※NES 業界ではGPUではなく PPU (Picture Processing Unit) と言うようです
interrupt
18
APU
20. の仕事
• 背景描画(最大のボトルネック、次ページ)
• スクロール
– VRAMは2画面分あり、1画面分を描画する
• スプライト
– 背景にキャラチップを重ねて書ける
– 衝突判定:0番スプライトを描画するとき CPU 割り込みする
• GPUはNES大勝利の立役者
– 同時期のアーケードと遜色ないグラフィック
– 同時期の汎用機(数十万円)より良い(NES:1.5万円)
20
21. 背景描画
• ピクセルごとに以下を実行 (1秒あたり256 x 240 x 60 = 3.7M回)
1. そこにあるビットマップ番号をタイル配置データから同定
2. パレット配置データを読んでパレットを同定
3. ビットマップ番号に対応するビットマップデータを読み込む
4. 組み合わせてビデオ信号にして送る
タイル配置データ
パレット配置データ
VRAM
GPU2
1
3
4
描画対象
正確には8ピクセル(1バイト)単位で処理する
21
画像データ
Cartridge
24. の仕事
• 以下の波形を合成して出力する
– 矩形波 x 2
– 三角波
– ノイズ
– PCM
– 簡単に言えば 3 和音+ドラム
• 実装は面倒だが、ボトルネックにはならない
– 44100 Hz なら、1 秒間に 44100 x O(1) の処理でよい
– GPU は 256x240x60 = 3686400 x O(1) の処理が必要
– ノイズ生成のみ若干重い
24
25. と の通信
• CPUから見てGPUとAPUはメモリに見える
– アドレス0x2000番地に書き込むとGPUへの情報送信
– アドレス0x2000番地から読み込むとGPUからの情報取得
25
アドレス 内容
0x0000..0x07ff 普通のメモリ
0x2000..0x3fff GPU
0x4000..0x5fff APUとパッド
0x6000..0x7fff 拡張メモリ
0x8000..0xffff プログラムROM
char *gpu = (char*)0x2000;
*gpu = 0x01;
32. アルゴリズム最適化を考えよ
• Optcarrot の場合
– ナイーブに書いて 3 fps
– アルゴリズム・データ構造の改善で 20 fps (約 7 倍)
– メソッド展開とか汚いことやって 80 fps (約 4 倍)
• アルゴリズム改善の方が寄与度が高い
– アルゴリズムに工夫の余地がなさそうなエミュレータですら
– メソッド展開などはわかりやすくて印象に残りやすいが、
通常はメンテナンス性を犠牲にするほどの効果はない
32
33. 効果を検証せよ
• ×「実行してみたら速かった」
– 測定のたびにブレがある
– 都合のいい結果を記憶に残して「速くなった」と信じてしまう
• 確証バイアス:仮説や信念を検証する際にそれを支持する情報ばか
りを集め、反証する情報を無視または集めようとしない傾向のこと
• 今回:最適化前と後で 30 回ずつ実行時間を計測、
Welch の t 検定で 5% 有意差があることを確認した
• 有意差が認められなかったら変更を捨てる 重要
– 頑張って実装した最適化が効果なかったとは認めたくない
ものだが、Mottainai の精神は悪
33
35. • a sampling call-stack profiler for ruby 2.1+
– https://github.com/tmm1/stackprof
• 特徴
– オーバーヘッドがほとんどない
– 測定結果は正確ではない
• インタプリタを数ミリ秒おきに監視し、各タイミングでどのメソッドが
実行されているかカウントする(サンプリングプロファイラ)
– 使うためにコードを書き換える必要がある
• 対抗:ruby-prof
– 使いやすい:ruby foo.rb を ruby-prof foo.rb にするだけ
– 測定結果が正確:メソッドごとの呼び出し回数
– オーバーヘッドがヤバい
35
36. の使い方
1. 測りたい部分を以下で囲む
– Optcarrot は初期化後のメインループを囲んでいる
2. 実行する stackprof.dump ファイルができる
3. stackprof stackprof.dump で結果を見る
– render_pixelが実行時間の20%を占めていることがわかる
36
TOTAL (pct) SAMPLES (pct) FRAME
274 (19.3%) 274 (19.3%) Optcarrot::PPU#render_pixel
160 (11.3%) 160 (11.3%) Optcarrot::PPU#wait_one_clock
106 (7.5%) 105 (7.4%) Optcarrot::CPU#fetch
StackProf.run(mode: :cpu, out: "stackprof.dump") {
# 測定したいコード
}
37. メソッド単位のプロファイル結果
37
$ stackprof --method "Optcarrot::PPU#render_pixel"¥
stackprof-cpu.dump
Optcarrot::PPU#render_pixel (/home/mame/work/optcarrot/lib/optcarrot/pp
samples: 309 self (17.8%) / 309 total (17.8%)
callers:
309 ( 100.0%) Optcarrot::PPU#main_loop
code:
| 803 | def render_pixel
| 804 | if @any_show
186 (10.7%) / 186 (10.7%) | 805 | pixel = @bg_enable
17 (1.0%) / 17 (1.0%) | 806 | if @sp_active && (
| 807 | if pixel % 4 ==
| 808 | pixel = sprite
| 809 | else
| 810 | @sp_zero_hit =
| 811 | pixel = sprite
| 812 | end
| 813 | end
| 814 | else
5 (0.3%) / 5 (0.3%) | 815 | pixel = @scroll_ad
39. オブジェクト生成の計測
39
$ stackprof obj.dump
==================================
Mode: object(1)
Samples: 86283 (0.00% miss rate)
GC: 0 (0.00%)
==================================
TOTAL (pct) SAMPLES (pct) FRAME
86278 (100.0%) 86278 (100.0%) Optcarrot::PPU#main_loop
86282 (100.0%) 4 (0.0%) Optcarrot::PPU#run
1 (0.0%) 1 (0.0%) Optcarrot::CPU#run
5 (0.0%) 0 (0.0%) Optcarrot::NES#step
5 (0.0%) 0 (0.0%) Optcarrot::NES#run
5 (0.0%) 0 (0.0%) <main>
5 (0.0%) 0 (0.0%) <main>
StackProf.run(mode: :object, out: “obj.dump") {
# 測定したいコード
}
46. • CPUはGPUやAPUをメモリ越しに制御する
• メモリアクセスのナイーブな実装:
– 毎回条件判定するのは遅い
– メモリマップはカートリッジによって変わるので
ハードコーディングはいけてない
def memory_read(addr)
case addr
when 0x0000..0x07ff then @main_memory[addr]
when 0x2000..0x3fff then @gpu.read(addr)
...
end
end
46
47. • Method#[] と Array#[] が同名なのを利用したコード
– 普通のメモリアクセスは配列参照 2 回でいける
– GPU・APU制御の場合は Method#[] で対応メソッドが
呼び出される
@mem = []
@mem[0x0000] = @main_memory
...
@mem[0x2000] = @gpu.method(:read)
...
def memory_read(addr)
@mem[addr][addr]
end
47
48. その他の最適化
• アルゴリズム最適化でも漸近的計算量だけを見ない
– 例:Ruby でリングバッファを実装するより、長さ次第では
Array#rotate!を使った方が速いことも
• いろいろ事前計算する
• こんな感じで 20 fps を達成した
– Intel® Core™ i7-4500U @ 2.40 GHz / Ubuntu 16.04
0x23C0 |
(addr & 0x0C00) |
(addr >> 4 & 0x0038) |
(addr >> 2 & 0x0007)
ARY[addr]
48
52. • 典型的な実行パスを展開する
™ while catchup?
if can_be_fast?
# fast-path
do_A
do_B
do_C
@clock += 3
else
case @clock
when 1 then do_A
when 2 then do_B
when 3 then do_C
...
end
@clock += 1
end
end
while catchup?
case @clock
when 1 then do_A
when 2 then do_B
when 3 then do_C
...
end
@clock += 1
end
47 fps 63 fps 52
54. 実装上の工夫
• 手動と言っても本当に手動にはしない
– 正規表現を使って自動的に変換する
• コードのインデントを頼りにパースする
– コードのインデントがちゃんとしている前提
– コードチェッカ Rubocop を使って、
コードのインデントが変になっていないことを検証する
src = File.read(__FILE__)
src.gsub!(/.../) { ... } # method inlining
src.gsub!(/.../) { ... } # ivar localization
eval(src)
54
61. 考察
• JRuby 9k が最速: “Deoptimization” が有望
– レアケースを無視して最適化 JIT コンパイルしておく
– レアケースが発生したら、そこだけ JIT コンパイルしなおす
卜部さんが検討中?
https://github.com/shyouhei/ruby/tree/deoptimization_base
• OMR はあまり速くない?
– JIT は望み薄?
• perf によると 50% が評価器だが、これは基本的な組み込みメソッド
の処理を含んでいる
– OMR はできたばかり
• チューニングの余地はあるし、opt_case_dispatch (case 文)の
最適化がまだっぽい
61
62. 不完全な 実装との戦い
• mruby:
– require なし、module_function なし、 fiber なし、
モジュールのインクルードがなんか微妙
• topaz:
– String#tr や #% を使ったら異常終了することがある
• opal:
– パスが動的な require 禁止、binread を自力実装
• 適当な shim を作った
– shim 不要なのは MRI と JRuby 9.1.0.0 だけ
62
63. ベンチマークプログラム
• コード生成含めて 5000 行以下
• 非GUIモードならライブラリ一切不要
– miniruby(MRI開発用の不完全なRuby実装)でも動く
– GUIモードでは ruby-ffi 経由でSDL2 を使う
• 基本的な Ruby 機能しか使っていない
– ruby 1.8 / mruby / topaz / opal でも shim などで動く
• Ruby開発者がRubyの高速化を検証する流れ
– miniruby ビルド→ miniruby 実行
63
64. よく使われるベンチ:
• 30000行超
• セットアップ手順
– PostgreSQL の設定
– Redmine インストールと初期設定と初期データ登録
– Apache のインストールと設定
– Passenger のインストールと設定
• Ruby開発者がRubyの高速化を検証する流れ
– Ruby 全ビルド→インストール→(Apache再起動?)→
ab 実行
64
65. 『手軽』なベンチマーク
• Ruby開発者にとって『手軽』であることが重要
• インストールやセットアップが不要
– git clone だけとコマンド一発で
標準的な設定とデータセットが準備できると理想
• コマンドラインから ruby foo.rb で起動・終了する
– 「このコマンドの実行時間を短縮せよ」だとわかりやすい
• 数秒くらいで終わり、毎回の実行時間が安定しているとより良い
– rake や rack 経由で実行するのはめんどい
• プロファイラやデバッガが使いにくい
• Ruby 開発者の Ruby は /usr/bin/ruby にはない
• 依存関係が少ないこと、そんなに大きくないこと、など
– miniruby で動くと理想(拡張ライブラリなし)
65
66. は適切なベンチマーク?
• 「遅いRubyには向いてないアプリでは?」
– もう Ruby はそこまで遅くない
– 「科学技術計算向き」とされる Python よりも
(コア自体は)速くなってきている
• 「にしても、エミュレータはRubyの目指すところか?」
– 個人的にはこういうのも書ける言語になってほしい
– 他のものを高速化してほしい人は、自分なりの
ベンチマークプログラムを作って公開してください
66
69. つの言語にこだわる つの理由
• 言語が「脳のキャッシュ」に載る
– リファレンスを見ずにプログラムできる
– ストレスなくプログラミングするために重要
– 多数の言語をこの状態にするのは(自分は)難しい
• 言語の「土地勘」が身につく
– その言語「らしい」書き方がわかる
– 「らしい」書き方だと、効率的なプログラムになったり、
バージョンアップ時のハマりを減らせたりする
– 言語機能の熟練度が予想できる(akr プロダクトは安心)
• 言語の「守備範囲」がよくわかる
– その言語でできることは思ったより広い
69
70. 注意
• 他言語を勉強しなくていいわけではない
– 見識は広げるべき
– ただ、「広く浅く」だけでは見えてこないものもある
• 何が何でも 1 つの言語で完結させる必要はない
– 工夫しまくってファミコンエミュレータが書けたが
– 工夫せずに実速が出せる C 言語の方が適切なのは確か
– ただ、ちょっとした工夫で言語を変えせずに済む場合も多い
• エコシステムの存在はでかい
– 科学技術計算やるなら Python に戦ってもしょうがない
– 再発明のコストが新言語習得のコストを明らかに上回る
70