SEGV探偵が往く

人は誰しも108つの夢を持つと言いますが、読者の皆さんにも探偵を夢見た人は多いことでしょう。実際問題として探偵業をやろうと思ったら地道な聞き込み&張り込みの日々でしょうし、逆に派手な探偵業は陰謀&殺人で恐ろしい。わたしも自分の命は大事ですからどうするか。そう、探偵ごっこをするわけですね。

というわけでSEGV探偵です。死ぬのはプロセスなので安心。Rubyを使っていればSEGVなんて見慣れたものですが、いざ捜査しようと思うと意外とその辺には転がっていないもの。(むかしはつかみ取り出来るくらいいたそうですよ)とはいえ今でもいわゆる「バグ鉱脈」みたいなものはありまして、その一つが「最新のコンパイラでビルドしてみる」です。

今日はGCCの開発版であるGCC8を試してみましょう。FreeBSDだとsudo pkg install lang/gcc8-develで簡単に入れられるのですが、他の環境だとちょっと大変かもしれません。さておきうちでは簡単なので入れてビルドしてmake testしてみます。

・・・はい、早速SEGVしました。

test_thread.rb           ...................................FF.............
#1180 test_thread.rb:330:in `<top (required)>': 
     Fiber.new(&Object.method(:class_eval)).resume("foo")
  #=> killed by SIGIOT (signal 6)
| [BUG] Segmentation fault at 0x0000000000000000
| ruby 2.5.0dev (2017-08-20 trunk 59626) [x86_64-linux]
| 
| -- Control frame information -----------------------------------------------
| c:0003 p:---- s:0009 e:000008 CFUNC  :class_eval
| c:0002 p:---- s:0006 e:000005 IFUNC 
| c:0001 p:---- s:0003 e:000002 (none) [FINISH]
| 
| -- Ruby level backtrace information ----------------------------------------
| bootstraptest.tmp.rb:0:in `class_eval'
| 
| -- Machine register context ------------------------------------------------
|  RIP: 0x000000000060a481 RBP: 0x00000000010a5630 RSP: 0x00007ff54468ea60
|  RAX: 0x00007ff5446af010 RBX: 0x0000000000000001 RCX: 0x00007ff5446af010
|  RDX: 0x0000000000000000 RDI: 0x00000000010e5730 RSI: 0x00000000010a5680
|   R8: 0x0000000000000034  R9: 0x0000000000000001 R10: 0x000000000000101a
|  R11: 0x0000000000000000 R12: 0x0000000000000000 R13: 0x0000000000000034
|  R14: 0x00000000010e5730 R15: 0x00007ff54468ee48 EFL: 0x0000000000010202
| 
| -- C level backtrace information -------------------------------------------
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(rb_vm_bugreport+0x50d) [0x6157ad] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm_dump.c:671
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(rb_bug_context+0xd8) [0x491e28] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/error.c:539
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(sigsegv+0x42) [0x588802] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/signal.c:930
| /lib/x86_64-linux-gnu/libpthread.so.0 [0x7ff544395390]
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(eval_string_with_cref+0xb1) [0x60a481] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm.c:505
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(rb_mod_module_eval+0x10a) [0x60afca] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm_eval.c:1613
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(vm_call0_body.constprop.158+0x270) [0x60b610] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm_eval.c:86
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(rb_vm_call+0x30) [0x60ba80] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm_eval.c:59
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(vm_yield_with_cfunc.isra.128+0x14d) [0x5fe39d] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm_insnhelper.c:2532
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(rb_vm_invoke_proc+0x5c) [0x6094cc] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm.c:1167
| /home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(rb_fiber_start+0x11e) [0x47097e] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/cont.c:1344
| /lib/x86_64-linux-gnu/libc.so.6 [0x7ff54363f5d0]
| 
…

RubyにはSEGVが発生した際にそれをキャッチしてCレベルでのバックトレースを表示する機能がついているので、いきなりCソースにたどり着くことが出来ます。上の4行はCバックトレース表示部分とシグナルトランポリンなので飛ばして、/home/ko1/ruby/build/trunk-test-gcc-trunk-np/miniruby(eval_string_with_cref+0xb1) [0x60a481] /home/ko1/ruby/src/trunk-test-gcc-trunk-np/vm.c:505が本題ですね。見てみましょう。

rb_control_frame_t *
rb_vm_get_ruby_level_next_cfp(const rb_thread_t *th, const rb_control_frame_t *cfp)
{
    while (!RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(th, cfp)) {
        if (VM_FRAME_RUBYFRAME_P(cfp)) {
            return (rb_control_frame_t *)cfp;
        }
        cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
    }
    return 0;
}

505行目はRUBY_VM_PREVIOUS_CONTROL_FRAME(cfp)ですが、マクロが一杯でよくわかりません。こういうときはMakefileを開いて、CFLAGSの行に-save-tempsを追加してビルドし直すとコンパイル中の一時ファイルが残ります。今回の場合vm.iが残っているはずです。見ると以下のような内容が得られます。

rb_control_frame_t *
rb_vm_get_ruby_level_next_cfp(const rb_thread_t *th, const rb_control_frame_t *cfp)
{
    while (!(!((void *)(((rb_control_frame_t *)((th)->ec.vm_stack + (th)->ec.vm_stack_size))) > (void *)((cfp))))) {
        if (VM_FRAME_RUBYFRAME_P(cfp)) {
            return (rb_control_frame_t *)cfp;
        }
        cfp = ((cfp)+1);
    }
    return 0;
}

なお、VM_FRAME_RUBYFRAME_P()はマクロと見せかけて関数で、実質cfp->ep[0] & 0x80という内容です。

古いコンパイラならば動くのに最新のコンパイラでだけこけるという場合は、通常より高度になった最適化が原因です。ですので、コンパイラの最適化で壊れるような要素、具体的にはC標準において未定義な動作をつくコードや、冗長でコンパイラによって消されそうなコードを探すのですが……見当たりませんね……。仮にそのようなコードがあったなら、直してみてうまく動けば大勝利なのですが、今回はそうではなかったようです。

とすると、これはコンパイラ側の問題である可能性が高まってきますね。しかし、どのようにしてコンパイラの問題であることを示せば良いのでしょうか。この問いは私にとって未解決なのですが、とりあえず期待と異なる命令列が出力されていることを示すことにしましょう。これを示すにはいくつか方法があります。

  • gdbなどの命令単位ステップ実行を使って、期待と異なる動きをするタイミングを見つける
  • objdumpなどによる逆アセンブル結果を見て、期待と異なる命令列を見つける

通常、デバッグにおいては「期待する動作とはどんなものか」という非常に難しい問題があるのですが、今回の場合少し古いコンパイラの結果と比較することであるべき姿がどんなものか知ることが出来ます。

また、バグを見つけたときはまず再現出来るようにすることが重要ですが、これは既に達成されました。また、小さなRubyコードで再現出来るようにすることも重要ですが、これも達成出来ています。ここまで小さければgdbで実行してみましょう。

% gdb --args ./miniruby -e'Thread.new("foo", &Object.method(:class_eval)).join'
GNU gdb (GDB) 8.0 [GDB v8.0 for FreeBSD]
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-portbld-freebsd10.3".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./miniruby...done.
(gdb) r
Starting program: /tmp/ruby/miniruby -eThread.new\(\"foo\",\ \&Object.method\(:class_eval\)\).join
[New LWP 101479 of process 94182]
[New LWP 101478 of process 94182]

Thread 2 received signal SIGSEGV, Segmentation fault.
[Switching to LWP 101479 of process 94182]
0x000000000124c591 in rb_vm_get_ruby_level_next_cfp (cfp=0x805506000, th=<optimized out>)
    at vm.c:505
505             cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);

まぁ、gdb上でも再現したのはよいことなのですが、本当はこのちょっと前でbreakしたいのです。これはLinuxだとReverse Debugという魔法で解決出来るのですが、FreeBSDではサポートされていないのでconditional breakpointを使います。よく考えた結果、b rb_vm_get_ruby_level_next_cfp if ruby_current_thread != ruby_current_vm->main_thread&&ruby_current_vm->main_threadでSEGV少し前に行けます。

そこからstepi(命令単位ステップ実行)を使ってとりあえず流し見します。すると、わりと狭い範囲をループしていることが分かります。まぁCのコードを見返してみれば当然なのですが。ではループしている範囲を逆アセンブルして見てみましょう

(gdb) disas 0x000000000124c591-61,+116
Dump of assembler code from 0x124c554 to 0x124c5c8:
   0x000000000124c554 <eval_string_with_cref+116>:      mov    0x60(%rsp),%rdx
   0x000000000124c559 <eval_string_with_cref+121>:      mov    0x28(%rdx),%rcx
   0x000000000124c55d <eval_string_with_cref+125>:      mov    0x20(%rdx),%rdx
   0x000000000124c561 <eval_string_with_cref+129>:      mov    0x30(%rax),%rax
   0x000000000124c565 <eval_string_with_cref+133>:      lea    (%rdx,%rcx,8),%rcx
   0x000000000124c569 <eval_string_with_cref+137>:      cmp    %rcx,%rax
   0x000000000124c56c <eval_string_with_cref+140>:      jae    0x124c59f <eval_string_with_cref+191>
   0x000000000124c56e <eval_string_with_cref+142>:      mov    0x20(%rax),%rdx
   0x000000000124c572 <eval_string_with_cref+146>:      testb  $0x80,(%rdx)
   0x000000000124c575 <eval_string_with_cref+149>:      jne    0x124c589 <eval_string_with_cref+169>
   0x000000000124c577 <eval_string_with_cref+151>:      jmpq   0x124c6d8 <eval_string_with_cref+504>
   0x000000000124c57c <eval_string_with_cref+156>:      nopl   0x0(%rax)
   0x000000000124c580 <eval_string_with_cref+160>:      test   %rdx,%rdx
   0x000000000124c583 <eval_string_with_cref+163>:      je     0x124c6d8 <eval_string_with_cref+504>
   0x000000000124c589 <eval_string_with_cref+169>:      add    $0x30,%rax
   0x000000000124c58d <eval_string_with_cref+173>:      mov    0x20(%rax),%rdx
=> 0x000000000124c591 <eval_string_with_cref+177>:      mov    (%rdx),%rdx
   0x000000000124c594 <eval_string_with_cref+180>:      and    $0x80,%edx
   0x000000000124c59a <eval_string_with_cref+186>:      cmp    %rax,%rcx
   0x000000000124c59d <eval_string_with_cref+189>:      ja     0x124c580 <eval_string_with_cref+160>
   0x000000000124c59f <eval_string_with_cref+191>:      lea    0x306f5a(%rip),%rax        # 0x1553500 <rb_eRuntimeError>
   0x000000000124c5a6 <eval_string_with_cref+198>:      lea    0x4a79b(%rip),%rsi        # 0x1296d48
   0x000000000124c5ad <eval_string_with_cref+205>:      mov    (%rax),%rdi
   0x000000000124c5b0 <eval_string_with_cref+208>:      xor    %eax,%eax
   0x000000000124c5b2 <eval_string_with_cref+210>:      callq  0x10c4af0 <rb_raise>
   0x000000000124c5b7 <eval_string_with_cref+215>:      nopw   0x0(%rax,%rax,1)
   0x000000000124c5c0 <eval_string_with_cref+224>:      lea    0x2f3499(%rip),%rsi        # 0x153fa60 <ruby_binding_data_type>
   0x000000000124c5c7 <eval_string_with_cref+231>:      mov    %rax,%rdi

stepiによると、最初から走ってきた後、しばらく0x124c580と0x124c59dの間をループして、最後に0x124c591でSEGVしています。ここでの登場人物を紹介しておくと以下の通りです。

  • rax: cfp
  • rcx: (th)->ec.vm_stack + (th)->ec.vm_stack_size
  • rdx: cfp->ep or cfp->ep[0] or cfp->ep[0] & 80

add $0x30,%raxcfp+1に対応し、mov; mov; andcfp->ep[0] & 80に対応するのですが、勘の言い方はもう気付かれたのではないでしょうか。cfpの妥当性、つまり確かにcfp-ep[0]にアクセス出来るというのは、(th)->ec.vm_stack + (th)->ec.vm_stack_size > cfpが真の時にだけ保証されています。しかし、この逆アセンブルではその分岐であるcmp %rax,%rcx; jaの前に、mov 0x20(%rax),%rdx; mov (%rdx),%rdxcfp->epにアクセスしてしまっていますね。このような順番の入れ替えは許されていません。ですので、これこそが今回の直接の原因だったことが分かります。

というような話をまとめてバグレポートしたのがBug 81954 - gcc8 too aggressively reorders memory access beyond conditionです。これは結局はBug 81900 - [8 Regression] GCC trunk miscompiles Perl / __sigsetjmp issueのduplicateであり、報告の数日前に直っていたという残念な話でした。実際、GCC8をtrunkからビルドし直したところあっさり解決してしまいました。

今回は残念ながら犯人に自殺されてしまうような残念な幕切れとなってしまいました。しかし、この記事に書いたような、探偵七つ道具を使い、証拠を集め、SEGVを追い詰めていく過程は、SEGV探偵を志す皆さんにとって参考となるのではないでしょうか。なるといいですね。

このケースとは異なり、もしRuby側に問題があって、たった1行のパッチで問題を解決出来る、そんなケースに出会えたならば、あなたはきっとダークソウルやFF14などのとても難しいボスを倒せたときのような気持ちを体験出来ることでしょう。そう「誰だこんな難易度に調整しやがったやつは!ふざけんな!!」という気持ちですね。

この記事を読んだ皆さんが、明日からはSEGVを踏んでも「ククッ、事件のにおいがするぜ」とニヤリと笑えることを祈っています。

なぜマストドンは日本で花開いたのか、あるいはソシャゲが日本で流行るわけ

なぜマストドンは日本で花開いたのか

マストドンが日本で流行ったのはなぜかという話題が盛り上がりましたね。わたしは『その理由がロリコンでないのなら、なぜマストドンは日本で花開いたのか』が当を得ていると思いました。つまり、海外では「一定人数以上のインスタンスを立てられなかったから」ですね。

しかし、この仮説を支持するとなると新たなる疑問が浮かびます。なぜ日本では立てられるのか。賢人ひしめくMIT擁するアメリカでは出来ないことがなぜ日本では出来たのかが解決しません。

逆算経営

ところで、「逆算経営」という言葉があります。まず到達すべきゴールを決め、それに必要なものをブレイクダウンしていき、それらを各個撃破していくというものです。これに類する概念は色々なところで語られていますが、その最も重要な点がどこかについてはあまり言及されていない気がします。

改めて考えてみれば当たり前なのですが、重要なのは何を目標とすべきかであり、かつそれが確かに達成可能なことですね。達成不可能なことを目標にしてしまうと「チャレンジ」がはじまるわけですがその話はまた別の話です。

ここで問いは「マストドンをスケールさせることは可能なのか」です。こんな日本語ブログを読んでいる物好きなみなさんは「まぁ、がんばればできるだろーけどかかわりたくねーな」あたりが正直な感想じゃないでしょうか。やりゃあできると思ってるわけですね。と、すれば後は気合いの入ったCEOがつかつかと歩み寄ってきて、高い給料とストックオプションをちらつかせながら、「ちょっとがんばってくれないかね」とか言ってくれば物語がはじまる日もあるわけですな。

RDBのスケールは可能、日本人はいつからそう信じたか

ちょっと主語を大きくしましたので異議のある方も出てくるかと思いますが、まだ条件付き賛成くらいはして頂けるんじゃないかと思います。で、いつからですか?

2006年頃からじゃないでしょうか。

そう、mixiが1年で20万人、2年で200万人というユーザー数の激増を捌ききった頃からではないでしょうか。彼らがその努力の詳細を公開し、またその後の不幸によって智恵を身につけた技術者が様々な会社に散らばってからではないでしょうか。

あの頃から人はスケールアウトが可能なものだと信じるようになったのでしょう。言い換えれば、スケールアウトは技術的な問題ではなく、スケールアウトのための投資が取り戻せるかという経営の問題にすぎないことだと理解されるようになったわけです。(そのせいでなめた設計をしてサービスイン当日に爆死する風景が絶えないのはまた別の話)

ソシャゲが日本で流行るわけ

ここまでの理屈に同意して頂ければ、この問いの答えも導けますね。そう、日本でだけ可能だったからです。

ソシャゲは一般に、自慢される側である大量の無課金者と、自慢する側である小数の課金者に分かれ、この二者は共に必要不可欠なものです。このモデルの問題は大量のユーザー数を捌けないと小数の課金者を維持出来ない点ですね。

これが可能な会社は海外でももちろん例外的にあるようですが、その例外に当てはまらないような会社はユーザー数が数万を超えたあたりで限界に達し、さらなる拡大を信じ切れずに投資を渋って消えていったのだろう、そう推測出来るわけです。ユーザー数に応じて柔軟に技術的な落としどころがあると広く信じられている日本が恵まれていることが分かりますね。

まとめ

mixiは死んでもスケールアウトは死せず、これをもって結びの言葉とします。

2017年のruby-mswin事情

WindowsRubyを使うのは難しいようで簡単なようで難しいことです。

ただインストールするだけならばインストーラがあるので簡単です。公式サイトのダウンロードページから探しましょう。

……ここでいきなり迷うであろうのが、"mswin"と"mingw"という語です。Unix系のOSではABIは通常一環境に一つです。しかし、Windowsではたくさんあります。64bit版Windowsでは32bitのバイナリも動かすことが出来るのと、最近までVisual Studioはバージョンアップの度にABIを変更していたため、2017年現在では以下のようなABIが存在します。

  • i386-mswin32
  • i386-mswin32_70
  • i386-mswin32_71
  • i386-mswin32_80
  • i386-mswin32_90
  • i386-mswin32_100
  • i386-mswin32_110
  • i386-mswin32_120
  • i386-mswin32_140
  • x64-mswin64_80
  • x64-mswin64_90
  • x64-mswin64_100
  • x64-mswin64_110
  • x64_mswin64_120
  • x64-mswin64_140
  • i386-mingw32
  • x64-mingw32

あとcygwinとかinterix、WSLですか。さて、挙げたうちの上の15個、mswinとあるのがVisual C++版で、下の2個がmingw版 (gcc版) です。i386-mswin32とi386-mingw32だけ互換性があることを除けば、あとはABI互換性がありません。一応付け加えると、ついに近年Microsoftも心を入れ替えたらしく 1、Visual C++ 2015と2017はABI互換性があり、ともに末尾140のものを使います。よかったですね。

さて、どの版を使うのが安心かですが、WindowsRubyを使う際の難しさはたいていが拡張ライブラリ絡みであることはそろそろ知られていると思います。つまり使いたいgemがバイナリgemで提供されているものを選ぶべきですね。たとえばNokogiriを見てみましょう。i386-mingw32版とx64-mingw32版が提供されていることがわかります。

mingw版とバイナリgem

他にもmingw版のバイナリgemが提供されているgemはmsgpackやffiなどいくつかあります。これはrake-compiler-dockを使うとdockerを使ってクロスコンパイルをしてくれる便利ツールがあるからです、みなさんもC拡張gemを作る場合は導入をぜひご検討ください。

mswin版を選ぶと言うこと

という世界の現実に抗いたい、そういう人もいると思います。Visual C++が好き、そういう人もいるでしょう。現代ではMicrosoftVisual StudioIDEを含む全てではなく、コンパイラだけを配布してくれるようになったので、VCのインストールも随分楽になりました。まだVisual C++ 2015または2017が入っていない人はBuild Tools for Visual Studio 2017をダウンロードして実行、個別のダウンロードで以下にチェックを入れてインストールします。

  • Windows ユニバーサル CRT
  • デスクトップ C++ x86 および x64 用 Windows 10 SDK (10.0.15063.0)
  • スタティック分析ツール
  • VC++ 2017 v141 ツールセット (x86,x64)
  • Visual C++ Buid Tools のコア機能

あとはスタートメニューからx64 Native Tools Command Prompt for VS 2017を選べばビルド環境が立ち上がります。

なお、blogによると自動インストールも可能なようですが、その際の最適なオプションの探求は読者の宿題とします。

Rubyの依存ツール

リリース版のtarballからビルドする場合はともかく、リポジトリからRubyをビルドするには色々なものが必要です。以前git for Windows SDKを用いると楽って話を書きましたが、現在ではRubyInstaller2でもいいかもしれませんね。pacmanからx64-mingw64版のautoconf, bison,rubyなどを入れて(例えばmsys版だとパス変換があるのでうまくいかない)、インストール先をcmd.exe側のPATHに足せば良いです。ここでの注意点はこれでインストール出来るのは実行ファイルだけなこと。ライブラリは前述の通りABIが異なるためMSYSではインストール出来ません。

Rubyの依存ライブラリ

依存ライブラリは従来は一つ一つ自分で調達する必要がありました。(たいへんだった)けれども最近vcpkgというMS謹製パッケージマネージャが登場しました。これを使うとopensslやzlib、libxml、libxsltなどを簡単にインストールすることが出来ます。

vcpkgのリポジトリをcloneしてcdし、

bootstrap-vcpkg.bat
.\vcpkg --triplet x64-windows install libxml2 libxslt openssl

あとはPATHに/vcpkg/installed/x64-windowsをいれ、win32\configure.bat --with-opt-dir=/vcpkg/installed/x64-windowsなどでRubyのビルドが出来るはずです。

また、gem install nokogiri -- --use-system-libraries --with-xml2-dir=/vcpkg/installed/x64-windows --with-xslt-dir=/vcpkg/installed/x64-windows --with-exslt=/vcpkg/installed/x64-windowsなどと指定すればnokogiriだってビルド出来ます。

Nokogiriが入れられたと言うことはつまり、最新のRailsが動くということですね。ぱちぱちぱち

残る問題

上記以外でよく必要とされる拡張ライブラリというと、readlineとtherubyracerあたりでしょうか。therubyracerはまぁexecjsとnode.exe使って回避すれば良いとして、readlineはどうすればいいんでしょうね。rb-readlineは日本語対応してなかった気がするし。Clang/C2でビルドって出来るんでしょうか。readlineは今でも勇者の登場を待っています。

Go言語感想文

最近、敵情視察を兼ねた仕事ととしてGoでアプリケーションを書いていた。このアプリケーションがどんなものかはそのうちid:tagomorisさんがどこかで話すと思うけれど、このコンポーネントOSS化される予定はいまのところないので、そこで得た知見をここにまとめておくことにする。

GoroutineとChannel

さて、GoといえばGoroutineとChannelですね。

Goroutineはようするにスレッドなんですが、文法と実装の支援でより気軽に使えるのが他の言語との違いでしょうか。なので、Goroutineをどれだけほいほい使うべきかというコスト感覚を身につけることがとても大事な気がします。Rubyなどとは気持ちを切り替えていく必要があるでしょう。ぼくはまだ切り替えきれていません。

もう一つがChannelですね。これは端的にはメッセージキューです。 Goは前述の通り同時に動くマルチスレッド (Simultaneous Multithreading; SMT) を気軽に扱えるわけですが、七つの人類悪の一つであるマルチスレッドに丸腰で立ち向かっても無残な死が見えています。 そのための道具の一つがキューで、適切にロックを用いて作られた通信路を経由してデータをコピーすることで、わかりやすくかつ比較的効率よくスレッドをまたいでデータをやりとりすることができるわけです。(ちなみにRubyにもThread::Queueってのがあったりしますが、CRubyだとGVLがあるのでわざわざ使うことは少ないかも)

とは言ってもそれなりにコストかかるんでしょう?って人はchansendの実装を眺めるとチャンネルごとのロックを取ってmemcpyしてるだけってわかるので、頑張って並列ハッシュとか使わなくていいんだって気分になれます。送受信にかかる時間は10~100ナノ秒くらいかな? とすると、話はいかに書きたいアプリケーションをGoroutineに分割し、間をChannelでつなぐかという話になります。そして、様々なデータは基本的にそれぞれのGoroutine内に閉じ込め、他のGoroutineはChannel経由でアクセスする。

この構図、どこかで見たことがありますね?そうマイクロサービスの話と同じです。 Goでアプリケーション書くのは、このようなマイクロサービス的なコンポーネント分割というマクロな楽しみと、Cのコードを触っている時のような細かなデータの取り回しというミクロな楽しみが隣り合わせにあって、独特の感覚を覚えます。

テストについて

Goの標準パッケージにはtestingってのがあります。このパッケージの思想はGo の Test に対する考え方などで解説されています。「まぁ、一理はあるけど……」という感想の方が多いのではないでしょうか。「いやダメでしょきつすぎ」って反射的に反応したくなりますが、敬意を表して実際にtestingでやってみました。

いや、ダメでしょこれ。結局テストのメッセージなんて”A is expected but B”が9割で、これのメッセージをいちいち考えるとか決断力の無駄遣いですよ。日本人が英語でメッセージを綴るリソースが有限だという悲しい現実を考慮していない。ぼくは5個くらいassertionを書いたところで嫌になってあとはひたすら”A is expected but B”をコピペしまくりました。

まぁでも、それで致命的につらいかというと真面目にやるのを放棄してコピペすれば良いし、Goのfmt.Printfは%vと%#vが便利なので、なんとかなりはします。assert_equalの類以外に、タイムアウトをつけてチャンネルを読んだりするassertionメソッドが欲しくなって作りたくなるかもしれませんが、そういうときはruntime.Callerで呼び出し元の行数が取れるので、うまいことやるとよいです。 まとめると、正直testingはダメだと思うけど、とはいえなんとかはなるのでtestingで依存先を減らすのはありかも。

その他

  • log.Fatalは使わない
    • カバレッジ上げづらくなる
    • fatalに追い込まれるようなエラーはもっと上流で捌くべきなんだと思う
  • テストでlogが邪魔なときはlog.SetOutput(ioutil.Discard)で黙らせる
    • 並列実行でうまくいかないのでloggerを持たせて個別にやった方がたぶんよい
  • 困ったときはgoto ← おまえほんとうにそれでいいのか
  • switchべんり ← おまえほんとうにそれでいいのか
    • selectやswitchの中でbreakすると外側のforまで届かないのでbreak <label>すればよいけど、結局goto使う
  • Printf(“%#v”, v)べんり
  • error伝播させたいときは、pkg/errorsのcause使う
  • とすると、エラー用構造体は作りまくった方がよい ← 本当に?
  • sync/atomicとかあるけど、new(expvar.Int)とかでmutex付き変数が簡単に作れる、便利
  • Goはnull安全ではない←構造体のポインタを扱い始めると気になってくる

まとめ

えーっと、この記事の趣旨は「Goは21世紀のRubyistが触っておくべき言語なのか?」でしたっけ。Elixirとかをやっていないのなら、Guildの予習として一度Goで何か作ってみるとよいと思いました。作ると言ってもこういう悪い例じゃなくてchannelを使ったアプリを作ってみると、従来のスレッドがうぇいうぇい暴れながら大事な共有リソースをロックで守るみたいなのとは違う設計が体に染みいります。みなさんもGoをいじってRuby 3.0を準備万端で迎えられるようにしましょう!

Re: Re: Go言語感想文

mattnさんのRe: Go言語感想文について。

テストについて

アサーションをコピーしなければならない理由は一つのテストケースの中で異なるテストが混在しているか、単に同様のテストがコピーして作られているのが原因ではないでしょうか。

あー、ちょっと言葉が悪かったかな。わたしはおそらくmattnさんより10倍くらいDRYに関して短気で、mattnさんの挙げた例で言えば、以下の3行ですら許せないのです。

        if !test.err && err != nil {
            t.Fatalf("should not be error for %v but:", test.input, err)
        }
        if test.err && err == nil {
            t.Fatalf("should be error for %v but not:", test.input)
        }
        if got != test.want {
            t.Fatalf("want %q, but %q:", test.want, got)
        }

あとは、Table Driven Testsって基本的には関数的というか入力と出力が対応するものでは便利なんだけど、REST APIを叩くコードのリトライ部分のテストとか、状態を持って他のコンポーネントとchannelで通信しながらループするコンポーネントのテストにはいまいち。

その他

Golang に限らずですが、最近の言語では Labeled Break という物があります。

あ、ゴメン「selectやswitchの中でbreakすると外側のforまで届かないのでbreak <label>すればよいけど、結局goto使う」の<label>がタグ扱いになって消えてた……。それはそれとして、上述のリトライみたいなケースだと結局goto使いました。

null安全について

これは世間一般での意味での「null安全」です。ポインタではなく直接構造体を扱えばうまいこと行くかなーと思いきや、なんやかんやでポインタ使う羽目になったりして悲しい。

RubyのTypo Checkerについての考察

RubyKaigi 2014の基調講演でまつもとさんが静的型の野望を明かしてから2年半が経った。 その間の進捗は芳しいものとは言えないけれど、それでもまじめな研究として例えば多相型、推論、Ruby が行われている。普通の人は私のこの記事を読むよりもこちらを読んだ方がよいと思う。 じゃあなぜこの記事を書いたかというと、それでも一部の人には得るところがあると思っているからである。この記事の読者の中にはRubyKaigi 2014中に書かれたakrさんの日記で「非常に簡単化した静的解析」の話を読んだ人もいるのではないかと思う。この話をそのまま発展させた場合にどういう迷路に迷い込むのかという点についていくつかの知見を得たものの、これまでそれを書いていなかったのでちゃんと書くことにしたのだ。

さて、nurse/static-check.rbである。 これは大きく分けて3つの部分からなっている。定義されているメソッドのリストを得る部分、呼ばれているメソッドのリストを得る部分、あるメソッドから呼ばれているメソッドをよりfalse positiveを減らしつつ得る部分だ。

定義されているメソッドのリストを得る部分はより漏れがないようにしている。具体的には特異メソッドも取れるようになったのだが、デメリットとして全てのオブジェクトに特異クラスを作ってしまう。(最近のRubyではいわゆる即値にあたるオブジェクトでは特異クラスがつくれなくなっているので、それ以外)これを避けるにはRuby側に新たな機能が必要なんだけど、これ、ぼくいがいにだれかほしい?

2つ目の呼ばれているメソッドのリストを得る部分はakrさんのものから本質的な違いは無い。RubyVM::InstructionSequence.disasm(meth)の戻り値はStringだが、RubyVM::InstructionSequence.of(meth).to_a[13]の戻り値はオブジェクトなのでより構造的に扱えるというくらいだ。

このスクリプトで本質的なのは3つ目のfalse positiveを減らす部分だ。false positive、つまり存在しないものとして扱われて欲しいのに、メソッドを呼び出しているとされてしまう例は例えばGem.load_yamlにある以下のようなコードだ。

        if defined?(YAML::ENGINE) && YAML::ENGINE.yamler != "psych"
          YAML::ENGINE.yamler = "psych"
        end

これらのYAML::ENGINE、YAML::ENGINE.yamler、YAML::ENGINE.yamler=はRuby 1.9でのSyckからPsychへのYAMLライブラリ移行に際して存在した移行用の定数とメソッドで、2.2以降ではもはや存在しない。こういうコードをそのまま読んでしまうと、typo checkに引っかかってしまう。これを避けるため、静的実行のように、スタックを管理しながら絶対に実行されない部分を除外しようとしている。

まぁここまで書くと、もっと効率的に数学的に解析出来ないものかと思い始めるわけですね。わたしはSSA形式に変換した方がいいんじゃないかなと思いました。で、中断して今に至るわけです。ですから、RubyバイトコードSSA形式に変換して変数への代入をうまく処理しながらデッドコードを削除してtypo checkをしたりするのは読者の宿題とします。

macOS上のAPFSはUnicode Normalizationを行うのか?

iOS 10.3がリリースされましたが、APFSへの移行が含まれていて話題です。特に文字コード界隈ではHFS+で搭載されていた暗黙のUnicode Normalizationがなくなっている点が指摘されています1

ではmacOSではどうなのでしょうか。SierraならばすでにAPFSを扱うことが出来るので試してみましょう。

% hdiutil create -fs APFS -size 1GB foo.sparseimage
WARNING: You are using a pre-release version of the Apple File System called
APFS which is meant for evaluation and development purposes only.  Files
stored on this volume may not be accessible in future releases of OS X.

You should back up all of your data before using APFS and regularly back up
data while using APFS, including before upgrading to future releases of OS X.

Continue? [y/N] y
created: /Users/naruse/foo.sparseimage
% hdiutil mount /Users/naruse/foo.sparseimage
/dev/disk2              GUID_partition_scheme
/dev/disk2s1            Apple_APFS
/dev/disk2s1s1          41504653-0000-11AA-AA11-0030654 /Volumes/untitled
% touch ガ.test
% cp ガ.test /Volumes/untitled
% ls -1|grep .test|od -tx1
0000000    e3  82  ab  e3  82  99  2e  74  65  73  74  0a
0000014
%  ls -1 /Volumes/untitled|grep .test|od -tx1
0000000    e3  82  ac  2e  74  65  73  74  0a
0000011

カレントディレクトリ (HFS+) では e3 82 ab e3 82 99 と2コードポイントにNFD2されていましたが、APFSでは e3 82 ac と1コードポイントになっていますね。

というわけで、macOS上のAPFSもUnicode Normalizationを行わないという結論でした。AppleもやっとファイルシステムのレイヤーでNormalizationを行うのが愚かな行いだと気付いたようでなによりです。(Foundation APIが正規化を行い続けていることがわかったので、この発言は撤回します。)


  1. http://mjtsai.com/blog/2017/03/24/apfss-bag-of-bytes-filenames/ など

  2. 正確にはNFDとは多少違うのだが

そして最近の分岐予測について2

先日の日記で最近のIntel CPUでは間接分岐の分岐予測がほとんどミスしなくなっているという話を紹介しましたが、Branch Prediction and the Performance of Interpreters - Don't Trust Folkloreという論文にまさに同じことが書かれているのを見つけました。ていうか、この論文わたし見た形跡がある……。

去年にこの論文を見かけたときは「Direct threaded codeとかオワコン」って話までしか見てなかったんですが、今改めて見ると分岐予測が世代ごとに進化していてすごいって話に加えて、ITTAGEという分岐予測手法を使うと同じくらい当たるって書いてありますね。

ITTAGEはTAGE (TAgged GEometric length predictor)の間接分岐版で、TAGEは原論文がA case for (partially) tagged Geometric History Length Branch Predictionにあります。また、Why TAGE is the bestという解説がありました。

なお、少なくともSandy Bridge世代では最適化マニュアルに"This unit predicts the target address not only based on the EIP of the branch but also based on the execution path through which execution reached this EIP. "とあり、これはTAGEの動きと異なるように見えます。Haswellでは再設計と共にこの記述は消え、"Next generation branch prediction"に変わりましたが、Intelからの発表は"Improved performance and saves wasted work"にとどまっています。Agner氏のまとめでは"These observations may indicate that there are two branch prediction methods: a fast method tied to the µop cache and the instruction cache, and a slower method using a branch target buffer."とあるので、TAGEとはまた違う手法なのかもしれません。

魔法が現実なのはわかったが、ブラックボックスなのは相変わらずなのであった。

それだけだとアレなので、optcarrotでのperf statの結果を載せておきます。optcarrotとは何かという話はRuby で高速なプログラムを書くを。なお、mameさんのマシンはBroadwellだと聞きました。

Sandy Bridge

$ perf stat ./miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 13.28153785633246
checksum: 59662

 Performance counter stats for './miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

      17860.390687 task-clock (msec)         #    0.998 CPUs utilized
               214 context-switches          #    0.012 K/sec
                 0 cpu-migrations            #    0.000 K/sec
            15,470 page-faults               #    0.866 K/sec
    40,509,030,133 cycles                    #    2.268 GHz                     [83.34%]
    18,724,702,913 stalled-cycles-frontend   #   46.22% frontend cycles idle    [83.33%]
    11,614,302,978 stalled-cycles-backend    #   28.67% backend  cycles idle    [66.67%]
    52,111,018,708 instructions              #    1.29  insns per cycle
                                             #    0.36  stalled cycles per insn [83.34%]
     9,668,028,188 branches                  #  541.311 M/sec                   [83.32%]
       505,219,551 branch-misses             #    5.23% of all branches         [83.33%]

      17.897844545 seconds time elapsed

$ perf stat ./miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 13.027601466360375
checksum: 59662

 Performance counter stats for './miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

      14796.375448 task-clock (msec)         #    0.998 CPUs utilized
               185 context-switches          #    0.013 K/sec
                 0 cpu-migrations            #    0.000 K/sec
            14,404 page-faults               #    0.973 K/sec
    33,417,895,565 cycles                    #    2.259 GHz                     [83.29%]
    11,061,511,903 stalled-cycles-frontend   #   33.10% frontend cycles idle    [83.34%]
     7,225,689,220 stalled-cycles-backend    #   21.62% backend  cycles idle    [66.70%]
    51,166,479,085 instructions              #    1.53  insns per cycle
                                             #    0.22  stalled cycles per insn [83.38%]
     9,698,878,651 branches                  #  655.490 M/sec                   [83.35%]
       507,996,700 branch-misses             #    5.24% of all branches         [83.34%]

      14.820478694 seconds time elapsed

Ivy Bridge

%  perf stat ./miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 17.73951805406009
checksum: 59662

 Performance counter stats for './miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

      11017.367463      task-clock (msec)         #    0.997 CPUs utilized
               273      context-switches          #    0.025 K/sec
                 3      cpu-migrations            #    0.000 K/sec
             14877      page-faults               #    0.001 M/sec
       31898032583      cycles                    #    2.895 GHz
        9862135088      stalled-cycles-frontend   #   30.92% frontend cycles idle
   <not supported>      stalled-cycles-backend
       52153024400      instructions              #    1.63  insns per cycle
                                                  #    0.19  stalled cycles per insn
        9663177149      branches                  #  877.086 M/sec
         492985311      branch-misses             #    5.10% of all branches

      11.046473500 seconds time elapsed

# 1472224481 0:14:41 naruse@tk2-243-31075:~
%  perf stat ./miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 16.002392702765267
checksum: 59662

 Performance counter stats for './miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

      11865.744103      task-clock (msec)         #    0.999 CPUs utilized
                41      context-switches          #    0.003 K/sec
                 1      cpu-migrations            #    0.000 K/sec
             14884      page-faults               #    0.001 M/sec
       34584790642      cycles                    #    2.915 GHz
       11820020205      stalled-cycles-frontend   #   34.18% frontend cycles idle
   <not supported>      stalled-cycles-backend
       51054848410      instructions              #    1.48  insns per cycle
                                                  #    0.23  stalled cycles per insn
        9677225367      branches                  #  815.560 M/sec
         567975115      branch-misses             #    5.87% of all branches

      11.871797668 seconds time elapsed

Haswell

%  perf stat ./miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 28.363176942909437
checksum: 59662

 Performance counter stats for './miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

       7021.035983      task-clock:u (msec)       #    0.999 CPUs utilized
                 0      context-switches:u        #    0.000 K/sec
                 0      cpu-migrations:u          #    0.000 K/sec
             14846      page-faults:u             #    0.002 M/sec
       18991845254      cycles:u                  #    2.705 GHz
       52072400309      instructions:u            #    2.74  insn per cycle
        9648278758      branches:u                # 1374.196 M/sec
          61237531      branch-misses:u           #    0.63% of all branches

       7.026493931 seconds time elapsed

%  perf stat ./miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 27.269785989915373
checksum: 59662

 Performance counter stats for './miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

       7214.329827      task-clock:u (msec)       #    0.999 CPUs utilized
                 0      context-switches:u        #    0.000 K/sec
                 0      cpu-migrations:u          #    0.000 K/sec
             14849      page-faults:u             #    0.002 M/sec
       19775865899      cycles:u                  #    2.741 GHz
       50975216372      instructions:u            #    2.58  insn per cycle
        9662536352      branches:u                # 1339.353 M/sec
          67884021      branch-misses:u           #    0.70% of all branches

       7.218712639 seconds time elapsed

Skylake

%  perf stat ./miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 45.18173733234584
checksum: 59662

 Performance counter stats for './miniruby-gcc48 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

       4496.992449      task-clock (msec)         #    1.000 CPUs utilized
                 6      context-switches          #    0.001 K/sec
                 1      cpu-migrations            #    0.000 K/sec
             14857      page-faults               #    0.003 M/sec
       17488535114      cycles                    #    3.889 GHz
       52123910662      instructions              #    2.98  insn per cycle
        9657149412      branches                  # 2147.468 M/sec
          54148235      branch-misses             #    0.56% of all branches

       4.498014828 seconds time elapsed

%  perf stat ./miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes
ruby 2.4.0dev (2016-08-26 trunk 56013) [x86_64-linux]
fps: 42.543931298125635
checksum: 59662

 Performance counter stats for './miniruby-gcc611 -v -Ioptcarrot/lib -r./optcarrot/tools/shim optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes':

       4662.130752      task-clock (msec)         #    1.000 CPUs utilized
                 4      context-switches          #    0.001 K/sec
                 1      cpu-migrations            #    0.000 K/sec
             13765      page-faults               #    0.003 M/sec
       18093681660      cycles                    #    3.881 GHz
       51137911290      instructions              #    2.83  insn per cycle
        9698686892      branches                  # 2080.312 M/sec
          74335218      branch-misses             #    0.77% of all branches

       4.662798673 seconds time elapsed