Hatena::Groupjavascript

JavaScriptで遊ぶよ

 | 

2010-06-07

charset=x-user-defined について

02:32

JavaScript でバイナリを扱うときによく使われるハック。

function load_binary_resource(url) {
  var req = new XMLHttpRequest();
  req.open('GET', url, false);
  //XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com]
  req.overrideMimeType('text/plain; charset=x-user-defined');
  req.send(null);
  if (req.status != 200) return '';
  return req.responseText;
}
var filestream = load_binary_resource(url);  
var abyte = filestream.charCodeAt(x) & 0xff; // throw away high-order byte (f7)  
Using XMLHttpRequest - MDC

この charCodeAt(x) & 0xFF というやつをなぜやらないといけないのか、ずっと疑問だったんだけど、Twitter で教えてもらった。

1999年 (!) に Mozilla で、User-Defined という charset をどういうふうに扱うかについて決定があったらしい。

I propose we do the following
1. Create a "user defined" encoding converter which convert 0x80-0xFF to U+F780 - U+F7FF back and forth.

Rational of the mapping range
1. The Private Use Area is defined from U+E000 - U+F8FF
2. Apple use U+F8A1 to U+F8FF for Apple specific chars.
3. Microsoft use U+E000 - U+EDE7 for Chinese/Japananese/Korean EUDC chars
Bug 6588 – implement "User-Defined" charset

US-ASCII と互換にするために、0x00 から 0x7F まで (一番左のビットが立ってないバイト) については Unicode の U+0000 から U+007F に割り当てて、それ以外の 0x80 から 0xFF まで (一番左のビットが立ってるバイト) は U+F780 から U+F7FF に割り当てることにしたらしい。

そういうわけで、charCodeAt して 0xFF との論理積を取ると、U+0000 から U+007F までは影響ないけれど、U+F780 から U+F7FF までは、上位の 0xF7 が消えて、無事に 0 から 255 までの生のバイトが得られるとということ。

Safari と Chrome もおそらく Mozilla に合わせて x-user-defined を実装したと思われる。

Opera は単純に U+0000〜U+00FF に割り当てることになっている。本当の ByteString。 Ruby1.9 で言うところの ASCII-8BIT。& 0xFF やらなくていいため、バイナリとして扱いやすいし、正規表現でバイナリ操作しやすいので嬉しい。(でも速さは & 0xFF する場合としない場合で差はなかった)

デモ。(最後に改行コードが入ってるけど、気にしない方向で)


ところで、

charCodeAt(i) & 0xFF のコストってどれくらいなんだろう。どうせ U+0000〜U+00FF と U+F780〜U+F7FF の 256+128個しか可能性がないんだったら、ハードコーディングしてしまったほうが速いんじゃないだろうか。と思ったので実験してみた。

やってることは、1MB の適当なバイナリファイルを、

  1. raw.charCodeAt(i) & 0xFF でやる方法
  2. {'\x00': 0, '\x01': 1, ...} というハッシュテーブルを使って変換する方法
  3. switch(raw[i]){case '\x00': ...} とやる方法

でバイト配列に変換して、かかった時間を比べている。

うちでは

1 2 3
Firefox 87ms 150ms 584ms
Safari4 64ms 123ms 123ms
Safari5 41ms 82ms 89ms
Chrome 99ms 117ms 917ms
Opera 52ms 127ms 380ms

という結果だった。charCodeAt(i) & 0xFF が速いってちょっと意外。よく考えると、2*i 番目のメモリ位置 (内部エンコーディングは UTF-16 だから) から2バイト読んで1バイト捨てるって操作なので、ちゃんとに最適化して (メソッド呼び出しもキャッシュして) あったらハッシュテーブルを探すより速いのは当然かもしれない。

ブラウザ間の差を比べるものではないです。例えば配列の要素を増やしていくとき、Chrome だとループ中で ary.push としたほうが 15ms ぐらい速くなるのに対し、他のブラウザは ary[i] = ... でやったほうが速かったので、そっちを採用していたりするから。


他の方法を思い付いた人は教えてください。

適切なヘッダー付けてビットマップ画像として canvas に置いて getImageData するとか考えたけど、data: URI からは確か getImageData できないし、画像だったらちゃんと四角形になっていないといけないし、面倒なのでやめた。

調べてみたら、ビットマップは透過がないから元から無理だった。


raw.charCodeAt(i) vs raw[i]

raw[i] が遅いのだという意見があったので比較。

それぞれ 1MB のファイルにつき、

  1. raw.charCodeAt(i)
  2. raw[i]
  3. raw.split('') してから ary[i] (ただし split の時間は測らない)
  4. raw.charAt(i)
1 2 3 4
Firefox 91ms 56ms 40ms 37ms
Safari4 58ms 94ms 36ms
Safari5 44ms 88ms 38ms 38ms
Chrome 95ms 115ms 113ms 112ms
Opera 48ms 49ms 46ms 43ms

バッチリ分かれたようす。

Chrome だけインデックスアクセスより push のほうが速い理由もなんとなくわかる。

テスト4を追加。id:murky-satyr さんの言うとおり、raw[i] より raw.charAt(i) のほうが速いという結果。

でも上のテストを charAt に変えてもあんまり速くならなかった。

 |