How to accelerate Ruby development; Provide real world use case

The history of decentralization of Ruby, and what you can do for the future

The history of Ruby development is also a history of transferring and separating the power of Ruby's decision from Matz.

Many years ago all Ruby development was done by only Matz. But in the year 1999, Matz1 built a CVS server and allow some people to commit their changes into the repository. They fixes trivial bugs and manages some libraries without Matz's decision.

After about a decade, Rails has come and Ruby spreads all over the world, and people started to complain its release engineering.2 The problem is simply that a release includes both new features and bug fixes. To provide them but separated, Ruby introduced "stable branch" and patch releases (maintenance releases).3 And release engineering including releasing a new version (packaging) is also transferred from Matz. Introducing a bug tracker is also at the similar timing. The teeny versioning was transferred to branch maintainers little later (2.1.x).

In CRuby development, Matz still has a privilege over Core component API design and major/minor versioning. We (and maybe you) know Matz's design decision are generally so good. But can't we also replace his role by some other people or committee?

Some people are trying to steal his design principle. For example akr, a Ruby committer, wrote APIデザインケーススタディ, which describes some case studies of Ruby API designs.

Most people of so-called Ruby core team, I think, had considered whether we can replace Matz's role. But they, including me, seem to conclude cannot. It's hard to explain this feeling. But with the words from Smalltalk context,

Personal Mastery: If a system is to serve the creative spirit, it must be entirely comprehensible to a single individual.

Matz seems to use a black box which provides how complex the idea is, whether the name is suitable for the feature or not, the feature has enough merit to add, and so on. We cannot alternate such scoring black box.

Anyway if you check the log of RubyDeveloperMeeting or Matz's replies to Redmine tickets, you will find there's many replies like "naming" and "use cases". In these days people get well understood about naming issues. But people still don't care about use cases so much. Recently some of Ruby core team write Rails applications as their business (including me) and some are working as full time committer in Rails company like Cookpad and gathers use cases in there. Oneshot mode to coverage introduced in Ruby 2.6 is such feature. But we sometimes don't have enough experiences or imagination to discuss about a suggested feature.

The throughput of oracling the black box is limited. But we don't use its maximum performance yet. If you triage issues and providing/discussing a concrete real world use case for a feature, it will boost the Ruby development.

At this time when we discuss about a feature request, we discuss about how the feature is used in the real world. Then we check whether people who reads the code may misunderstand/confuse or not. You may suggest a magical fun feature but be rejected. It's because of such reason. Naming issues are also because of this reason.

And also, if a ticket has so long comments, it's helpful to summarize previous discussions. It's very helpful. Very helpful.

Conclusion

You can speed up Ruby development with showing a snippet from real world application.

char8_tによせて

C++標準化委員会、ついに文字とは何かを理解する: char8_tという記事が話題だってので、つらつらと書いてみました。

「グリフ」について

グリフ(glyph)という言葉の定義をめぐって でも触れられていますが、「グリフ」という言葉が「字体」を指すのか「字形」を指すのかってのは議論がありますね。文字コードの文脈では普通「字形」の意味だとして話を進めることが多いように思います。

CJK統合漢字について

Wikipediaの記事にまとまっていますが、実際に推進していたのは中国みたいですね。うまくやればあんまり問題なかったんでしょうが、あんまりうまく行かなかったんですが、それでも国ごとにその国の過去にあった文字コードとの互換性は取れているので、実際の所CJK統合漢字ってあんまり問題にはなってないと思うんですよね。中国語フォントと日本語フォントを切り替えないといけないって問題はありますけど、それは仕方なくない?だってさぁ、アルファベットは元から統合されてるんだし。

まぁ、トルコ語のアルファベットは分離しておいた方がよかったと思います。Wikipediaのトルコ語の項目にちらっと示唆されていますが、大文字と小文字の対応が違うんですよね…。

MicrosoftUnicodeといえば、もれなくUTF-16を指す。」について

元々はUnicodeは16bit固定長で、それを使ってWindows NTを実装していたら、いつのまにかUnicodeは16bitには納まらなくなっていて、仕方ないのでUTF-16に拡張したっていう悲しい経緯であって、UTF-16そのものの、あるいはワイド文字のいけてなさとは関係ない気がしました。

ワイド文字について

ワイド文字の歴史

ワイド文字については何度か書いたことがあります。

そもそもワイド文字という概念はUnicode以前からあって、元々はDEC漢字のような日本語UNIX環境の開発から生まれ、日本語UNIX環境がAT&Tの本家UNIXに取り込まれることで世界に紹介され、C89にwchar_tが取り込まれ、C++にも採用されるといった順序になっています。

この本家UNIXの国際化の世界観というのがEUCで、複数の文字集合をG0/G1/G2/G4にマップし、それを内部表現32bit、外部表現は1-4バイトで表現していました。

その他のLinuxとかとかではUTF-32だ、という事が多い。これはコードポイントと一致するエンコードだからだろうか。

これは前述の時代の名残でwchar_tが32bitで定義されていることが多かったため、自然とそこにUTF-32を詰めるようになっただけだと思います。元々そこに詰められていたのはEUCの類だったわけです。

という経緯が分かると、なぜC/C++標準で執拗にUnicode決めうち仕様を避けているのかが分かってくるのではないでしょうか。wchar_tにUnicode以外の何かを詰めたコードは世の中に多数存在するのです。

ワイド文字はなぜいけてないか

さて、なぜワイド文字はいけてないかというと、サロゲートペアは決定的な理由ではないでしょう。16bit Unicode固定長という夢の世界が否定されても、Windows NTやmacOSは既に作られてしまっているわけで、それらとデータをやりとりするにはそれを元にしたUTF-16サロゲートペアでがんばるというのは自然な発想でしょう。またBMP外の文字や絵文字などを処理する際のつらさはUTF-8でもUTF-16でも同じくらい辛いわけで、UTF-16を批判する理由にはなりません。決定的な理由が出てこない限りは、互換性と、UTF-16にあるASCIIとその他の文字を平等に扱うという三方一バイト損的な美しい大義を捨てる理由にはなりません。

では、その決定的な理由とは何か。Web世界とやりとりすることが増えたからでしょう。Webの世界はHTTPの上で成り立っており、HTTPはASCIIを基本として全てが組み上げられています。そんなASCIIの世界をUTF-16で扱うには毎回変換を行う必要がありますし、万が一不正なバイトが入っていた場合のエラーハンドリングを行うにはバイト列のまま扱う処理を用意しなければいけません。そんな事を行うならば最初からバイト列でがんばるか…UTF-8を使った方がよいでしょう。

この結果が90%のWebサイトがUTF-8という現実なのであり、これと同じことがついにC++標準にも及んできたと言うことなのでしょう。

なお、この辺の事情はUnixでのファイルシステムでも同様で、パスがバイト列で表されていて/とヌル文字以外は何でも入るのでだいたいはUTF-8なんだろうけれど例外的な状況も扱えないといけないという事情がありますね。(そもそもUTF-8ファイルシステム用に生まれたんだし)

文字列について

https://togetter.com/li/1301253 で提示されている、「文字列」は文字の列としては扱いづらいという話はその通りだと思うのです。そういう中身が不透明な文字列型は例えば(C++よくわかんないんでCで書きますけど)こんな感じ?

struct {
  rsize_t size;
  int8_t encoding; /* UTF-8, UTF-16, or UTF-32 */
  int8_t valid_p;  /* true, false, or unknown */
  union {
    char8_t *u8;
    char16_t *u16;
    char32_t *u32;
  } chars;
}

前述の通り不正なバイト列もコンテナとしては入れられるけど、文字列用関数に渡すとエラーになるみたいな感じになるのだと思います。でも結局文字列を扱う関数は気合で誰かが書かないといけないし、C++ってそういう関数を各言語という面も少なからずあるので、まずchar8_tから入れたんでしょうかね。

そしてchar8_tについて

と、ここまで考えると一周回って char8_t って本当に必要だったの?という疑問がわいてきます。提案であるchar8_t: A type for UTF-8 characters and strings (Revision 5)を読むに、charにはその環境ごとのdefault localeの文字、つまりWindowsでいえばANSIをいれて、それとは別にUTF-8を入れる型を用意したいって話かな。unsigned charuint8_tでいい気もしますけどね。

ところで、バイト列とUTF-8文字列を区別したいという観点だと、バイト列をunsigned charで扱い、UTF-8文字列をchar8_tで扱うとした場合、文字列は暗黙的にバイト列として扱えるけど、その逆は明示的に指定しないとキャストできないって言う使い心地になるので、それはそれでHTTP絡みのコードを書くときは便利になるかもしれませんね。

観察日記 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は今でも勇者の登場を待っています。