観察日記 2018-02-14

配列の全ての要素が等しいか否か

mrkn
配列の全ての要素が等しいことはどう確認したら良いんだろう。
`ary.all? {|e| e == ary[0] }` これかな

usa
ary.uniq.size == 1

mrkn
なるほど > uniq

usa
all?でブロック引数より速そうな予感
いやでもaryがでかくてかつ全然要素が等しくなかったらそうでもないか。

mrkn
`ary.all? {|e| e.foo == ary[0].foo }` の場合はどうでしょう。map.uniq.size がいいかな

usa
all?は全て等しい時に遅いが、序盤で違うとわかったら速い

mrkn
確かに > 序盤で違うとわかったら速い

usa
この辺は予想される集合の傾向で判断するしかないですかねえ。
map.uniq.sizeはmapの結果としての一時配列を作らないようにするには、えーと
Enumerator

mrkn
一瞬 `ary.homogeneous? {|e| e.foo }` って書きたくなったけど、そのまま書いても読めるから大丈夫そうだ。
`ary.lazy.map(&:foo).uniq.size`

naruse
MJITがall?もはやくしてくれるさぁ

mrkn
all? がインライン展開されるのか。

sorah
lazy.uniqいけるのか。
.sizeじゃなくて .count?

usa
lazyだとsizeはまずい

mrkn
lazy 使う場合は count でした

sorah
`[1,1,1,1].lazy.uniq.take(2).count == 1` かなぁ

usa
だいぶテクニカルになってきたな

sorah
>> a = Struct.new(:i) { def n; p i; end }; [a.new(1), a.new(2), a.new(3), a.new(4)].lazy.map(&:n).uniq.take(2).count
1
2
=> 2
>> a = Struct.new(:i) { def n; p i; end }; [a.new(1), a.new(2), a.new(3), a.new(4)].lazy.map(&:n).uniq.count
1
2
3
4
=> 4
こういう差が。 usa なるはや殺しっすよね しかしさすがにここまでくると別名をつけたくなったね。 mrkn take(2) 頭いいな この take(2) は結構感動しました。 usa さすがそらはパイセンですよね。 しかし `homogeneous?` ってわかりやすい名前なのかな。 tadd https://ruby-gnome2.osdn.jp/ja/hiki.cgi?%A5%DC%A5%C3%A5%AF%A5%B9 ruby + homogeneous でぐぐったらGtkが引っかかった mrkn 私が最初に思いついた名前なので、一般的ではないと思います。 ary.same_all? とかかな usa ぼくもsame_allかなあと思いました。 mrkn activesupport にも存在しない knu `[1,1,1,1].each_cons(2).all? { |a,b| a == b }` usa なんとなく遅そうなイメージ knu なぜだろう usa 比較が1回減るのはいいのだけど (e0,e1) (e1,e2) (e2,e3) ... という舐め方が有利なのか不利なのかどっちだろう、という。 knu ブロックが遅い気がする。簡略記法欲しいな usa ブロックはどうしても遅い knu `inject(:+)` はチート 計測すると、条件成立時や末尾に違う値がある場合は uniq が2倍以上速い でも、ショートカットはしてくれないので最初の方に違う値が検出される場合は当然each_consが圧勝 usa 2倍以上かあ。 やはり想定されるデータセットにあわせて選ぶしかないですね。 knu きっとone? があれば lazy.uniq.one? が最適解かな (equality が eql? ってのはおいといて) usa あ、そういうのはまあそういうことで knu ショートカットがないは語弊があった。2つの値だけがずっと続く場合は〜、だ。 one? はたまーにほしいんだよな 何回か前のDeveloperMeetingで出てたか と思ったらある?あれ? usa おっ knu 一つだけが真の場合にtrueを返すのか nilやfalseを考えないなら使えるか `[1,1,1,1].lazy.uniq.one?` でよさそう uniqだから 1 と 1.0 は区別される、one?だから nil や false が入る場合はダメ、という制約付き sorah ブロック無し `.any?` を `.size > 0` 的に使ってるのと同じくらいの生理的嫌悪感がある usa わかる sorah あれ使ってる人は nil, false での動作理解して書いてんのかな、というのが分からなくて混乱するのでやめてほしいんだよな knu お、 `one?(&:object_id)` にしても速度落ちない knu Lispだと=がいくつでも引数取れるか `(apply #'= '(1 1 1 1)) ` ずるい usa ずるいよね knu Rubyのオブジェクト指向はレシーバと引数の間に偉さの違い usa Rubyしぐさ knu zipがたまに気持ち悪い 気持ちは a.zip(b,c) でなく [a,b,c].zip と書きたいんだ… Array.zip(…) が配列を返して Enumerator.zip(…) はEnumerableを返す、というのは悪くないと思うんだけど字面がRubyっぽくないんだよな mrkn 会社のブログで出題してみたら `ary.rotate == ary` というここでは出なかった解が出てきた。 usa おー 遅そうw でも柔軟な発想を感じる。すごい。 mrkn 似たようなので `ary[0..-2] == ary[1..-1]` usa rotateしないからこれはまだ。しかしかっこよさに欠けるな。 いや、これ実はむっちゃ速い? mrkn 配列を作らない usa しかも評価もおそらく短絡してくれる。 mrkn 配列の長さが2以上の時の実装はコレでしょうな。 usa 1の時もいけるのでは ko1 rotate 知らなかった sorah 柔軟な発想だ… usa それを考えた人に今日の :100: をあげよう。 文句なし。

slackのログを上手いことまとめる方法が思いつかなかったので、雑にpreで……。

はじめての投資

投資とは

最近、「投資を始めようかな」と考える人が増えたように思います。この記事を読んでいるあなたもきっとそうでしょう。

アベノミクス、株高、円安、ビットコイン、転職など理由はさまざまでしょうが、リーマンショックを乗り越えた世界の、日本の経済情勢の変化があなたの心にもついに波及したということでしょう。余力のある人にとって、多少のリスクを取って資産を増やそうというのはとてもよい行いです。

そもそも投資とはリソースの最適配分であり、リスクと正面から向き合うことであり、資本主義社会における投票権の行使でもあります。これまであなたは資産の全てを日本円の現預金で保有していたことでしょう。これがどのような意味を持つ行いだったのか改めて考えてみましょう。

2008年のリーマンショックから2012年のアベノミクス開始までは物価や株価は下がり、円高が進んでいましたから、日本円の現預金で資産を持つのは悪くない投資だったと言えるでしょう。この間、あなたの資産は1.5倍くらいになっていたはずです(円建てでは変わっていませんが)

一方で2012年から今までは、消費者物価こそあまり変わっていないものの、株価は上がり円安となりました。この間、あなたの資産は2/3くらいになったともいえます(円建てでは変わっていませんが)

銀行が破綻してかつ預金保護法が守ってくれなかった場合や、極端に物価が上がった場合というような、早々発生しないリスクをおいておいても、上述したような資産価値の変動は日本円で持っていても発生しています。

あなたはこれまで日本円を基準として資産の価値を、リスクを考えてきたと思いますが、それが絶対の基準ではないことが分かりました。株価や物価はもとより、円の価値もドルの価値も日々動いていくとすれば、私達は何をよりどころにして資産価値を把握し、維持・あるいは増やしていく基準としていけばよいのでしょう。健全な精神は健全なKPIの下にこそ宿るというのに、私達は何を信じればいいのでしょう。

国際分散投資

一般論としては、あなたはまず世界経済の長期的な成長を信じる必要があります。3.11があろうと、リーマンショックがあろうと、世界経済はそれを乗り越えて成長していく、世界の資本主義経済の未来に賭ける必要があります。あなたは信じられますか?

これを信じられない方に、私は語る言葉を持ちません。この前提が崩れた世界がどのようなものなのか、年末年始にそれを考えるのは楽しい娯楽かもしれません。とりあえず信じられるものとして先に進みます。

世界経済の成長を信じられるとしても、個々の企業はもちろん、個々の国の経済は信じ切れないわけです。ならばどうするか、色々な企業、色々な国にバラバラに投資する。つまり国際分散投資ということになります。

用語の説明

分散投資をする際に一社一社売買していたら手間がかかって仕方ありません。まずはそういう際の道具を説明しておきます。

  • 証券化: 色々なものを売買しやすくするための手法の一つです
  • 上場: 色々なものを売買しやすくするための手法の一つです
  • 投資信託: 投資家から集めたお金を一定の基準で運用し、手数料を取って利益を配分する金融商品です。株式や債券、不動産など色々なものを扱う投資信託があります。
  • ETF(上場投資信託): 投資信託を上場してより売買しやすくしたものです
  • 債権: 国債社債などです、つまり、国や会社に金を貸して金利を取るというものです
  • REIT(リート): 不動産投資信託 (real estate investment trust)です。その名の通り不動産を扱う投資信託です。
  • コモディティ: 一般には商品、日用品といった意味ですが、この文脈では原油天然ガス、金、プラチナなどをETFにしたものを扱います
  • ノーロード: 売買手数料がかからないという意味です。運用手数料がその分高いわけですが、投資家の利害と運用者の利害が一致しやすいのでよいとされています。(従来型証券会社が売買手数料目当てに無為な売買を客に押し込んでいたという話を聞いたことのある人も多いでしょう)

金融商品での儲け方

金融証券から利益を得る際にはインカムゲインキャピタルゲインという二通りの方法があります。

インカムゲインは端的には利息のことです。具体的には株式なら配当、債権なら受取利息、不動産なら家賃収入です。

キャピタルゲインは値上がり益のことです。株式はもちろん、金融商品はものによって程度の差はあれ価格が変動します。この変動から利益を得ることも出来ます。

金融商品の種類

投資の対象となる金融商品は、種類に応じて以下のような特徴があります

  • 株式: 値動きが大きく(=大きなキャピタルゲインを狙える)、利回りはぼちぼち(=配当収入はぼちぼち)
  • 債権: 値動きが小さく、利回りが高い
  • 不動産: 株式や債券と異なる値動きをし(=リスクを分散出来る)、利回りが比較的高い
  • コモディティ: 物価に連動した動きをする

になります。

地域の分散

次に地域的な分散についてですがおおざっぱには、

  • 国内
  • 先進国: ローリスクローリターン
  • 新興国: ハイリスクハイリターン

に分けられます。

国際分散投資をしよう

さまざまな種類の金融商品と地域分散を理解出来ましたね。

では、投資信託一覧を見て、さまざまな金融商品を組合せ、君だけの最強のポートフォリオを作ってみましょう!

・ ・ ・

できましたか?

ちょっと難しいですよね。そういう方のために最近ではロボアドバイザーというものが作られています。 ロボアドバイザーは何をしてくれるかというと、ユーザーの目指すべきリターン、受け入れられるリスクを加味しながら、適切に分散投資をしてくれます。

適切でない分散投資というのは、一見独立しているかのように見えて実は価格の上下に相関がある複数の商品に投資してしまう場合です。具体例としては一見違う業種、違う国の企業に見えるが、Appleに売り上げを大きく依存している、とかですね。

というわけで証券会社を選んで実際にやってみましょう!どこがいいのって聞かれそうなのでとりあえず二社挙げておくと、

注:二社とも私とは2017年現在と特別な利害関係はありません。

私は松井証券を使っていますが、「世界経済の長期的な成長」以上の何かを信じている場合にはよいですね。例えば当面世界経済は安定するだろうと信じている場合、その分偏った投資(分散出来ていない投資)を行い、的中すればリターンは大きくなります。(外れたら損します)今年はよい年でしたが来年再来年は雲行きがあやしくなるかもしれませんから、先行き分からないときはきちんと分散投資を徹底した方がいいでしょう。とはいえ、自分で考えてギャンブルするのも楽しいので、多少の額で遊んでみるのもよいかもしれません。

明日暴落したらどうするか

株式は元本割れすることがあります。また、それは急速に起きることがあります。 例えば東日本大震災の際にはその後の数日で2割程度下落しています。(その後若干戻りましたが)また、もし急に安倍首相が退陣したら日本の株価は短期間で大きく下落するでしょう。

これについてはすでに答えが出ており、積み立てが正解とされています。これまでに説明した分散がこの三次元世界における分散だとしたら、積み立ては時間方向への分散投資だと言うことができるでしょう。

毎月分配型について

毎月分配型の投資信託というものがあり、それについての議論があります。それを読んで悩む人もいるでしょう。

結論だけ言うと、毎月分配型はリタイア層向けの商品で、この記事の読者向けではありません。毎月分配金を払う事務コストや利益を再投資に回せないのは資産を増やすという観点では悪手です。資産を消費しながら生きていくという観点では悪くないとは思いますが。

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事情

おしらせ

以下に最新版があります。 qiita.com

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をしたりするのは読者の宿題とします。