MessagePack-JS (4)
cuzic です。
MessagePack-JS をさらに更新しました。
- MessagePack.pack() による、MessagePack へのエンコーディング機能の追加。
- エンコーディングに対応したテストコードも追加
- MessagePack.unpack() のテストコードに XMLHTTPRequest を使った例を追加した。
- XHR を使う対抗側のコードの追加。
MessagePack-JS へのエンコーディングも可能にしたことで、一応、基本的なやりたいことについては、実現しました。
MessagePack-JS を作ったときのもともとのモチベーションは JavaScript の勉強だったのですが、実装してみて、MessagePack にはいろいろと利点があることが確認できたので、整理してみたいと思います。
非常にコンパクト
MessagePack の特徴として、同じ内容のデータでも少ない容量しか必要としないという点があります。
これは、非常に容量が限られている Cookie (最大でもわずか 3.8KB)や、Flash の SharedObject(最大でもわずか 1 MB 程度)といった状況では、この容量制限の中で効率的にデータを格納できる MessagePack 形式は有効だと思います。
最近では HTML5 の Web Storage API もありますが、それでも容量は Google Chrome の場合で 最大 2.5MB 程度と制限されています。
コンパクトにデータを格納できる形式への需要は高いように思います。
通信時間が短縮できる。
パース処理だけで比較した速度面では JSON.parse よりも劣るものの、データ量そのものがコンパクトなので、通信時間は短く済みます。
クライアント側はアイドル時間が多いが、サーバ側は負荷が高いケースも多いかと思いますし、XHR で取得したデータをパースするような場合は、通信時間が大半を占めることも多いですので、メッセージサイズが小さくなり通信時間を短縮できるメリットは大きいでしょう。
MessagePack-JS (3)
cuzic です。
前回の投稿は ベンチマークテストにバグがあって、うまく処理できていなかったようです。ベンチマークテストのバグを修正すると、私の実装は eval に比べて 10倍以上遅いということが分かりました。
今日は一日かけて MessagePack のバイナリ文字列を Internet Explorer 環境で高速に処理する方法について、模索していました。
Internet Explorer 環境の特徴としては、以下の点があります。
- 単純に JavaScript エンジンが遅い
- responseText ではバイナリ文字列が処理できない。( FireFox 等のような overrideMimeType によるハックが使えない)
- responseBody でバイト配列を返す
- バイト配列は、 JavaScript では直接扱えない。
- バイト配列の処理には JavaScript 以外の技術の利用が必須(VBScript の利用、XMLDOM の COM オブジェクトの利用など)
バイト配列の処理手法として当初は VBScript の利用による解決を模索したのですが、次の問題点を克服できませんでした。
- メモリ使用量が非常に大きくなり、スタックオーバーフローが発生する場合さえある
- 耐え難く遅い。(上記と同根の問題?)
そこで、現時点で github にコミットしているコードでは次の方式を採用しました。
- Microsoft.XMLDOM の COM オブジェクトを利用して、一旦 base64 エンコードする
- base64 エンコードした文字列を JavaScript で書いたデコード処理で 0〜255 までの数の配列に変換
- 2で得た配列を元に MessagePack の形式として解釈
いろいろ実験した中ではこの方式が IE8 環境でもっとも高速に動作するように思われました。
base64 へのエンコード処理は、Microsoft.XMLDOM を利用することで、
function binary_to_string(binary){ var xmldom = new ActiveXObject("Microsoft.XMLDOM"); var bin = xmldom.createElement("bin"); bin.dataType = "bin.base64"; bin.nodeTypedValue = binary; return bin.text; // base64 文字列 }
と簡単にできます。
base64 デコードについては、http://www.webtoolkit.info/javascript-base64.html を参考にしつつ、独自に実装しました。
あと、Microsoft.XMLDOM ではなく ADODB.Stream を使って、バイナリ文字列を扱う手法についても見当したのですが、私の InternetExplorer のセキュリティ設定では利用できない COM オブェジェクトであったため、採用しませんでした。
MessagePack-JS (2)
投稿時は、MessagePack-JS の結果の方が eval より高速としていましたが、それは ベンチマークテストのバグに由来するものでした。計測しなおしまして、記事を訂正しましたので下記を参照ください。
cuzic です。
MessagePack-JS ライブラリをちょっと更新しましたので、報告します。
主な変更点としては、以下のとおりです。
といったところです。
ベンチマーク結果ですが、以下のとおりとなり、eval の方が速いという結果になりました。 (Firefox なら 20倍以上遅い)
MessagePack | JSON | |
Firefox | 9,140msec | 380msec |
---|---|---|
Chrome | 6,883msec | 213msec |
Internet Explorer 8 でも検証したのですが、あまりに遅くて計測に耐えられる速度ではありませんでした。。。(1万行が長すぎたか・・・。
Internet Explorer 8でも計測しました。
MessagePack | JSON | |
Firefox | 9,140msec | 380msec |
---|---|---|
Chrome | 6,883msec | 213msec |
IE8 | 88,563msec | 2,422msec |
sort benchmarkで使われている gensort コマンドを使って
./gensort -a 10000 gensort.txt
でできた gensort.txt を元に生成した配列を 10回デコードするという処理にしました。
配列は、
[ [string_10_bytes_1 , string_32_bytes_1, string_52_bytes_1],
[string_10_bytes_2 , string_32_bytes_2, string_52_bytes_2],
[string_10_bytes_3 , string_32_bytes_3, string_52_bytes_3],
...
[string_10_bytes_10000 , string_32_bytes_10000, string_52_bytes_10000],
]
というようなかんじの文字列の配列の配列です。
JSON のデコードには単なる eval を利用しています。
MessagePack.load_url 関数を削除した理由は、Firefox 等でバイナリ文字列として処理するために必要な overrideMimeType を利用するには自分で実装するしかないかな、と思っていたのですが、Prototype.JS でも
new Ajax.Request(url, { method: 'get', onCreate: function(request, response){ if(request.transport.overrideMimeType){ request.transport.overrideMimeType("text/plain; charset=x-user-defined"); } }, onSuccess: function(transport) { // 処理したい内容 } });
とすることで利用可能であることが分かったので、MessagePack ライブラリから不要と判断したためです。
今後は、
などを進めていけたらいいな、と思っております。
MessagePack-JS
cuzic です。
MessagePack の JavaScript 実装を作成しましたので、公開しました。
GitHub においております。
サーバ側で生成した MessagePack の文字列をクライアント側の JavaScript で、
var data = MessagePack.unpack(unescape("%a1%61")); alert(a);
と書けば Ruby で 'a'.to_msgpack に相当する MessagePack のシリアリゼーション結果を変数 data に格納します。
現在は以下の状態です。
- MessagePack 形式のデコードが可能。
- 整数、nil 、false 、true、浮動小数点、文字列、ハッシュ、配列 に対応
- FireFox 3.6.3 と IE 8.0.6 で動作検証を実施。正常動作を確認。
作成した理由としては、以下の理由です。
- 最近 JavaScript を勉強しているので、なにかを作って公開してみたかった。
- MessagePack の仕様の美しさについて、某所で話題になっていた。
- JSON が主流な JavaScript とのデータ連携を MessagePack に変更することで、サーバ負荷の低減、高速化が可能になる。
今後の予定としては、次の内容を進めていきたいと考えております。
- MessagePack 形式のデコードだけでなくエンコードを可能に。
- テストコードを追加。クロスブラウザな自動テストを可能に。
- 各種ブラウザで利用可能かの検証の実施。
- JSON フォーマットとの速度比較の実施
MessagePack-JS の作成にあたっては、http://github.com/nayutaya/msgpack-pure での Ruby 実装を参考にさせていただきました。
vim勉強会#6 参加
3月21日の vim 勉強会で学んだことを簡単にメモ。
テキストオブジェクト
vim では ct) とすることで、 ) までの手前の文字を置き換えることができる。その ct) と同じ内容を ci( でできるらしい。cf) は ca( で同じ操作になる。ci( や ca( を使うと、一度、編集したい範囲の最初の文字まで移動する操作が不要になり、その分、ラクに編集できる。
smartchr.vim
smartchr を使うと、たとえば = の連打で
" = " # 両側を スペースではさんだ = " += " # lhs = lhs + rhs の略記法 " == " # 等号のような比較の時に使う演算子 " === " # PHP や Ruby に存在する演算子 "=" # 通常の =
のような一連の入力を簡単にできるみたい。
vim スクリプトの文法
ちょっと学んだけど、正直キモい。あまり深入りする気持ちにはなれなかった。
私自身、vim はかなりの初心者の上、素の vi を触る機会も多いためあまり高度な使い方はむしろしない。
範囲指定とか、置換とかのベーシックな機能を(今でも使い方を知っているのですが)もっと使いこなせるようになった方がいいかな、と思い中である。
今度、vim 勉強会があったらあまり使い慣れていないベーシックな機能を説明して、自分の勉強に役立てるようにしようかな。
私が使いこなせていない vim のベーシックな機能としては、
- キーボードマクロ
- ftFT による移動。; . とか。
- cc と C の違い
- インデント。 == 、 >> 、<<
とか。
ちなみにこの日記はemacs で書いています。
update-alternatives
Ruby Advent Calendar の記事で http://blog.udzura.jp/2009/12/13/switch-your-ruby-on-ubuntu910/というのがあった。
RVM はいいものだが、利用している環境が Ubuntu でもあるので、この方法についても試してみることにした。
ただ、冗長な書きぶりが気になったので、update-alternatives を生成する Ruby スクリプトを作成。
このスクリプトを利用すれば、rails コマンドなど新たにコマンドが増えたときにも簡単に対応できる。
1 RUBY186 = ["/usr/local/ruby/ruby-1.8.6-p383", "", 120] 2 RUBY187 = ["/usr/local/ruby/ruby-1.8.7-p174", "", 150] 3 system1 = ["/usr/", "1.8", 140] 4 system2 = ["/usr/", "1.9", 100] 5 RUBY191 = ["/usr/local/ruby/ruby-1.9.1-p376", "", 110] 6 7 commands = %w(ruby irb gem rake) 8 9 [RUBY186, RUBY187, system1, system2, RUBY191].each do |dir, suffix, priority| 10 args = commands.map do |command| 11 link = "/usr/bin/#{command}" 12 fullpathcmd = "#{File.join(dir, "bin", command)}#{suffix}" 13 if command == "ruby" then 14 "--install #{link} #{command} #{fullpathcmd} #{priority}" 15 else 16 "--slave #{link} ruby-#{command} #{fullpathcmd}" 17 end 18 end.join(" ") 19 altcmd = "sudo update-alternatives #{args}" 20 system altcmd 21 end
update-alternatives は、切り替えるのに管理者権限が必要となるようだ。ユーザ権限で頻繁に利用するRuby を変更したいと思う用途にはなかなか難しいかもしれない。
update-alternatives --list によって、インストールされている Ruby を得ることができるため、その結果を利用して、インストールしているすべての Ruby で実行する all-ruby コマンドの作成なども可能となりそうだ。時間があれば取り組みたい。
each、each_with_object、inject、map
Ruby の each、each_with_object、inject、map は使いどころが微妙に違う。
それぞれ適切な状況で使い分けられれば、コードはより分かりやすくなる。
どんな状況でも each で書くことはできる。だから、each だけ使いこなせればいいという考え方はある点で正しい。そのような考え方の人にとってはeach で書くのがもっとも分かりやすいコードになるだろう。
しかし慣れてみると上記のメソッドを使い分けられる方が簡潔で分かりやすいコードになる。その理由はメッセージ性の違いだ。
each ですべてを書く場合は余計なコードを書く必要があり、その分、どうしても、本質的なコードが埋もれてしまう。余分なコードがないほど、本質的なコードが際立つ。メッセージが伝わりやすくなる。意味があるコードの比率を下げることは、中級プログラマへの道を開く鍵だ。
それでは本題に入ろう。
まずは簡単に [1, 2, 3] をそれぞれ二乗して [1, 4, 9] を得るコードを書く。
なお、each_with_object の利用には Ruby 1.9 系を利用するか、Rails の一部である ActiveSupport を利用する必要かある。
# each
result =
[1, 2, 3].each do |i|
result << i*i
end
result # 値を返すために必要
# each_with_object
[1, 2, 3].each_with_object do |i, result|
result << i*i
end
# inject
[1, 2, 3].inject [] do |result, i|
result << i*i
end
# map
[1, 2, 3].map do |i|
i*i
end
この場合は map がもっともコードが短く簡潔になることが分かる。each の場合は返り値が self であるため、生成した result を明示的に返す必要がある。
この場合は、 each_with_object と inject の間の違いは引数の順序以外に特に分からない。
では、次の例を見てみよう。
[%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)] から {"0xff0000" => "red", "0x00ff00" => "green", "0x0000ff" => "blue"} を得たい。
# each hash = {} [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)].each do |key, value| hash[key] = value end hash # each_with_object [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)].each_with_object({}) do |(key, value), hash| hash[key] = value end # inject [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)].inject({}) do |hash, (key, value)| hash[key] = value hash # <- 余分だが必要な1行 end # map # ハッシュを返り値とできないため、かけない。
each_with_object では必ず引数のオブジェクトが返り値となるため、返り値となるオブジェクトを明示的に最後に書く必要はない。しかしながら、 inject ではブロックで最後に評価した値が次回のブロックの引数となりさらには返り値となるため、明示的に hash を最後に書く必要がある。
map は配列を返り値とする場合にしか利用できないため、ハッシュを得たい場合は利用できない。
この場合は、each_with_object を使う例がもっとも簡潔である。それは、内部の処理で副作用をもたらす処理を行っており、その副作用を与えたい対象が返したい値だからだ。inject の処理では適切な値を返すために 1行余分に必要になってしまう。
次に整数の和を求める例を示す。
# each sum = 0 [ 1, 2, 3].each do |i| sum += i end sum # each_with_object # 破壊的に変更可能なオブジェクトでないと適用不可 sum = [ 1, 2, 3].each_with_object(0) do |i, j| j += i end # p sum #=> 0 # inject その1 sum = [ 1, 2, 3].inject 0 do |subtotal, i| subtotal + i end # inject その2 sum = [ 1, 2, 3].inject 0, :+ # inject その3 sum = [ 1, 2, 3].inject :+ # map は配列を返り値とする場合にしか利用不可
整数の和を得たい場合は、inject が一番簡潔になる。この場合は each_with_object は意図どおり動作しない。
最後に、文字列の連結処理を考える。
# each path = "" %w(usr local ruby ruby1.9.1 bin).each do |dir| path << "/" + dir end path # each_with_object %w(usr local ruby ruby1.9.1 bin).each_with_object "" do |dir, path| path << "/" + dir # path += "/" + dir は不可 end # inject %w(usr local ruby ruby1.9.1 bin).inject "" do |path, dir| path + "/" + dir end # map # 今回の主な趣旨とは異なるが Array#join を使う解を示す。 %w(usr local ruby ruby1.9.1 bin).map do |dir| "/" + dir end.join("")
この場合も inject が簡潔であるように感じられるが、それは主にeach_with_object がスペルが長いからだ。
その部分を差し引いて考えれば、同等だろう。each_with_object はブロック引数のオブジェクトを破壊的に変更し続ける場合に便利なメソッドなので、この場合 path += ではなく path << を利用する必要がある。inject はブロックの最後の評価結果を利用するため、path << や path += などとする必要はなく、単に + として良い。
map と join を組み合わせる例が、案外簡潔で分かりやすい点も見落とすべきでない。この例もありうる。
総じて each を使うと、返したいオブジェクトの初期化と、値を返すために2行長くなる。この2行は本質的な処理を行っている箇所ではないため、each_with_object や inject、map を利用すれば2行短くできる。
逆にいうと、すでに得られているオブジェクトに対する処理で、返り値などを利用する必要がとくにない場合は each を利用する方が分かりやすいコードになる
補足であるが、tap (Ruby 1.8.7 以降)というメソッドや、returning (ActiveSupport が提供)というメソッドがある。これらを利用すると、返り値の1行などをなくせる場合がある。
# tap {}.tap do |hash| [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)].each do |key, value| hash[key] = value end end # returning returning ({}) do |hash| [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)].each do |key, value| hash[key] = value end end
蛇足だが、さらに上記は Hash[] メソッドを使うことできわめて簡潔に書ける。
a = [%w(0xff0000 red), %w(0x00ff00 green), %w(0x0000ff blue)] Hash[*a.flatten]