Hatena::ブログ(Diary)

latest log このページをアンテナに追加 RSSフィード

2010-07-22

fn.call(何か) ベンチ

気になったのでとってみたけどあまり意味が無いベンチになりました。fn.call(null) って速いのね。

Browserfn.call(null)fn.call(this)fn.call(window)(function(){fn()}
Fx3.6 1162591136306432
IE8 937101010732338
IE6 2833244725785093
GC6 304 2851452 403
Sa5 288 323 2802002
iSa4 69 67 65 300
Op10.6x 1916197040662287
Total 7509120131314418855

bench.jshttp://pigs.sourceforge.jp/blog/20100722 にあります

<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8" />
<title>bench: call(hoge)</title>
<style>th,td { padding: 5px; text-align: right }</style>
<script src="bench.js"></script><script>

window.onload = function() {

    document.body.innerHTML += "<p>" + navigator.userAgent + "</p>Running...";

    var loop = getLoop();

    // ---------------------------------------------------------
    var _ary = [0, 1, 2, 3, 4];

    function callback(value, index) {
        return index * value;
    }

    function _call_null(idx) {
        var ary = _ary, i = 0, iz = ary.length;

        for (; i < iz; ++i) {
            callback.call(null, ary[i], i);
        }
        return idx;
    }

    function _call_this(idx) {
        var ary = _ary, i = 0, iz = ary.length;

        for (; i < iz; ++i) {
            callback.call(this, ary[i], i);
        }
        return idx;
    }

    function _call_window(idx) {
        var ary = _ary, i = 0, iz = ary.length;

        for (; i < iz; ++i) {
            callback.call(window, ary[i], i);
        }
        return idx;
    }

    function _func(idx) {
        var ary = _ary, i = 0, iz = ary.length;

        for (; i < iz; ++i) {
            (function(value, index) {
                callback(value, index);
            })(ary[i], i);
        }

        return idx;
    }

    job(5, loop, ["callback.call(null)", _call_null,
                  "callback.call(this)", _call_this,
                  "callback.call(window)", _call_window,
                  "(function(){callback()}",  _func]);
}
</script></head><body></body></html>

2010-06-07

MessagePack + WebWorkers

追記

@os0X さんと @edvakf さんからアドバイスをいただいたので、本文とコードを修正しました。

postMessageが文字列に限定されているのは旧仕様で、Firefoxや最近のWebKit(Chrome5とか)はオブジェクトをやり取りできると思います

via @os0X http://twitter.com/edvakf/statuses/15644433920

自分でも試してみました。 http://javascript.g.hatena.ne.jp/edvakf/20100607/1275931930

via @edvakf http://twitter.com/os0x/statuses/15621293442

本文

昨日の日記の続きです。

IE9Opera で動くようになり、ちょっと速くなり、WebWorkers でデコードするモードを追加しました。

http://pigs.sourceforge.jp/blog/20100607/msgpack.htm

コード一式 http://pigs.sourceforge.jp/blog/20100607/msgpack.zip

説明

このテストは、

  1. サーバ上に置いたバイナリデータを application/octet-stream または text/plain で取得する
  2. バイナリデータを JavaScript の ByteArray に変換する
  3. ByteArray を msgpack.unpack() でデコードし、元のデータを復元する

といった一連の流れになっています。

左上でインクリメントし続けるカウンターは、UI スレッドがビジーループに突入すると一時停止します。

WebWorkers が使える環境では、2. と 3. を WebWorkers で処理します。

WebWorkers なのにカウンターが止まる理由

WebWorkers と JavaScript は基本的に文字列データのやり取りしか出来ません。

いまどきの実装(Firefox3.5+, Google Chrome4+)では、WebWorkersとJavaScript は文字列以外のデータもやり取り可能でした。

そのため、msgpack.unpack() したデータを WebWorkers 上で JSON.stringify(mix) し、JavaScript 側で JSON.parse(event.data) しオブジェクトに直しています。

// msgpack.worker.js
onmessage = function(event) {
    var mix = msgpack.unpack(event.data);

//  postMessage(JSON.stringify(mix)); // 本来は不要な処理(UI スレッドは固まらないけど、CPUを食う)
    postMessage(mix);
};

if (xhr.readyState === 4) {
    var status = xhr.status,
        rv = { ok: status >= 200 && status < 300,
               status: status, option: option, data: [] };

    if (rv.ok) {
        if (!_ie && option.worker && win.Worker) {
            var worker = new Worker(_WebWorker);

            worker.onmessage = function(event) {
//              rv.data = JSON.parse(event.data); // 本来は不要な処理(ここでUI スレッドが固まる可能性がある)
                rv.data = event.data;
                callback(rv);
            };
            worker.postMessage(xhr.responseText);
        } else {
            rv.data = msgpackunpack(_ie ? toByteArrayIE(xhr)
                                        : toByteArray(xhr.responseText));
            callback(rv);
        }
    }
}

WebWorkers を使っているにも関わらずカウンターが微妙に止まってしまうことがあるのは、JSON.parse() が挟まっているのが主な理由です。

ちょっとしたこと

WebWorkersモードでは、misc/msgpack.js が misc/msgpack.worker.js を呼び、misc/msgpack.worker.js が misc/msgpack.js を呼んで処理をしています。

misc/msgpack.js などは、ライブラリの一部/単体利用/クライアントサイド/サーバサイド/WebWorker上など多彩な環境で動作するようにデザインしてあるため、条件が整えばこのように呼び戻して使うことも可能になっています。

このおかげで、misc/msgpack.worker.jsソースコードは非常にコンパクトになっています。

importScripts("utf8.js");
utf8 || importScripts("misc/utf8.js");
utf8 || importScripts("src/misc/utf8.js");

importScripts("msgpack.js");
msgpack || importScripts("misc/msgpack.js");
msgpack || importScripts("src/misc/msgpack.js");

onmessage = function(event) {
    var mix = msgpack.unpack(event.data);

//  postMessage(JSON.stringify(mix));
    postMessage(mix);
};

反省会

  • WebWorkers のオーバーヘッドがかなり大きい。
    • ネットワークアクセスが発生するので、importScripts() を出来るだけ使わないほうがいい。
      • つまり、Worker("src.js") で読み込む src.js はあらかじめ concat + minify しておくべき。
  • ベースURLを指定できないので、importScripts() がとても使いづらい。→ WorkerGlobalScope.location から相対パスを取得可能。詳しくは 2010-06-09 の日記を参照
  • IE では WebWorkers が使えないため、Flash にデータを渡して処理する方法も考えたが未着手
    • Flashjs のブリッジ ExternalInterface が重いので、Flash 側で取得しデコードしたデータを JSON で取得する方法だと何とかなるかも? ね

2010-06-06

自作 MessagePack のベンチ取ってみた

http://pigs.sourceforge.jp/blog/20100606/msgpack.htm

IE6 28秒ぐらい
IE8 10秒ぐらい
Chrome6 8.5秒ぐらい
Firefox3.6.3 9秒ぐらい
Opera10.50 不明

約1MBのデータだと、体感的に IE8 と Firefox3.6, Google Chrome 6 で大差ない感じになってる気がします。

# 回線や距離もあるでしょうが

サーバからバイナリデータを受信する処理は、IE6でもうまいことやれてますが、Operaだと、application/octet-stream で先頭数バイトがちゃんと受信できず全滅してます。

ファイル一覧はこちら

http://pigs.sourceforge.jp/blog/20100606/

http://pigs.sourceforge.jp/blog/20100606/msgpack.zip からファイル一式がダウンロードできます。

*.php の中身はこんな感じです

<?php
$fileName = "1byte.bin";
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($fileName));
readfile($fileName);
?>

バイナリデータの受信部分のロジックはこんな感じです

uu.ajax.binary(url) を呼ぶと、非同期にバイナリデータを受信し、ByteArray( [0x00, 0xff, ...] ) を返します。

// === uu.ajax.binary ===
//#include uupaa.js
//#include ajax/ajax.js

uu.ajax.binary || (function(uu) {

uu.ajax.binary = uuajaxbinary; // uu.ajax.binary(url:String, callback:Function, option:Hash = {})

// uu.ajax.binary
function uuajaxbinary(url,      // @param String:
                      callback, // @param Function: callback function
                      option) { // @param Hash(= {}):
    function readyStateChange() {
        if (xhr.readyState === 4) {
            var ok = xhr.status >= 200 && xhr.status < 300;

            callback({ ok: ok, xhr: xhr, data: ok ? toBinary(xhr) : [] });
        }
    }

    var xhr = uu.ajax.create();

    xhr.onreadystatechange = readyStateChange;

    if (!uu.ie) {
        if (xhr.overrideMimeType) {
            xhr.overrideMimeType("text/plain; charset=x-user-defined");
        } else {
            xhr.setRequestHeader("Accept-Charset", "x-user-defined");
        }
    }
    xhr.open("GET", url, true); // ASync
    xhr.send(null);
}

// inner - to binary
function toBinary(xhr) { // @param XMLHttpRequest:
                         // @return ByteArray:
    var rv = [], data,
//{{{!mb
        loop, remain, v0, v1, v2, v3, v4, v5, v6, v7,
//}}}!mb
        i = 0, iz;

//{{{!mb
    if (!uu.ie) {
//}}}!mb
        data = xhr.responseText;
        iz = data.length;
        for (; i < iz; ++i) {
            rv[i] = data.charCodeAt(i) & 0xff;
        }
//{{{!mb
    } else {
        data = vbstr(xhr.responseBody);
        iz = vblen(xhr.responseBody);

        loop = Math.ceil(iz / 2);
        remain = loop % 8;
        i = -1;

        while (remain--) {
            v = data.charCodeAt(++i); // 0x00,0x01 -> 0x0100
            rv.push(v & 255, v >> 8);
        }
        remain = loop >> 3;
        while (remain--) {
            v0 = data.charCodeAt(++i);
            v1 = data.charCodeAt(++i);
            v2 = data.charCodeAt(++i);
            v3 = data.charCodeAt(++i);
            v4 = data.charCodeAt(++i);
            v5 = data.charCodeAt(++i);
            v6 = data.charCodeAt(++i);
            v7 = data.charCodeAt(++i);
            rv.push(v0 & 255, v0 >> 8, v1 & 255, v1 >> 8,
                    v2 & 255, v2 >> 8, v3 & 255, v3 >> 8,
                    v4 & 255, v4 >> 8, v5 & 255, v5 >> 8,
                    v6 & 255, v6 >> 8, v7 & 255, v7 >> 8);
        }
    }
//}}}!mb
    return rv;
}

// --- init ---
//{{{!mb
uu.ie && document.write('<script type="text/vbscript">\
Function vblen(b)vblen=LenB(b)End Function\n\
Function vbstr(b)vbstr=CStr(b)+chr(0)End Function</'+'script>');
//}}}!mb

})(uu);

反省会

  • VBScript 周りは、最終的なコードだけ見ると大したことやってないように見えるけど、色々と試行錯誤がありました。
    • バイナリデータを JScript だけで受信できない IE は、かなりどうかしてる。
  • http://bit.ly/a0zwGd で紹介されているような VBScript 内でレスポンスデータを AscB() や MidB() でコネて配列化するコードは、200〜300kBのデータを食わせるだけでIEが固まるので要注意です。
    • なので、その辺も自力で何とかしています。

2010-02-24

Canvas on Flashでアニメーション

Flashバックエンドの実装とbugfixもいい感じに進んでます。残すは

  • clip()
  • globalCompositeOperation
  • ShadowAPI
  • TextAPI
  • PixelAPI
    • toDataURL(), getImageData()

な感じです。

アニメーションデモ

今までは派手目なデモの紹介を控えてきましたが、そろそろ頃合なので、幾つかご紹介します。

各デモページの左上(or 左下)にある、[ sl / flash / vml ] ボタンでバックエンドを切り替えられます。

Overlay

f:id:uupaa:20100224050723p:image

http://pigs.sourceforge.jp/blog/20100224/demo/demo5_overlay.htm

Kaleidoscope

f:id:uupaa:20100224111733p:image

http://pigs.sourceforge.jp/blog/20100224/demo/demo6_kaleidoscope.htm


Spread

f:id:uupaa:20100224050724p:image

http://pigs.sourceforge.jp/blog/20100224/demo/demo7_spread.htm

元ネタ

Sphere

f:id:uupaa:20100224112017p:image

http://pigs.sourceforge.jp/blog/20100224/demo/demo8_sphere.htm

# Flashモードで不透明度の処理が変なので、後で直します。

元ネタ



Bugfix

ついったー上でご指摘を受けたバグも修正してます。

http://pigs.sourceforge.jp/blog/20100224/demo/2_4_canvas_arc.htm

http://pigs.sourceforge.jp/blog/20100224/demo/2_7_canvas_combined.htm

@uupaa 既知かもしれませんが demo の 2_7 で matrix effect を二回実行すると以降の描画が崩れますね。

via @mitoma_ryo http://twitter.com/mitoma_ryo/status/9519554155

@mitoma_ryo さん、ご指摘ありがとうございます。

2010-02-15

canvasをより速く(Flashもサポート)-Take4

このエントリは

canvasをより速く(Flashもサポート) - latest log

canvasをより速く(Flashもサポート)-Take3 - latest log の続きです。

今日も、Flashモードのレンダリングをちょっと速くしました。

送信部分の高速化

修正前

var _stack = []; 
var _lockState = 0; 
var _readyState = 0; // js側初期化済で1, Flash側初期化済で2 

function send(fg) { // @param String: fragment, "{COMMAND}\t{ARG1}\t..."
    if (fg) {
        this._stock.push(fg);
    }
    if (!this._lockState && this._readyState) {
        if (this._readyState === 2) {
            var ctx = this;

// ▼▼▼▼▼▼▼ ここから修正 ▼▼▼▼▼▼▼
            // <param name="flashVars" param="t={time}&c={cmd}" />
            setTimeout(function() {
                if (ctx._stock.length) {
                    ctx._view.flashVars = "t=" + (+new Date) +
                                         "&c=" + ctx._stock.join("\t");
                    ctx._stock = []; // clear
                }
            }, 0);
// ▲▲▲▲▲▲▲ ここまで修正 ▲▲▲▲▲▲▲
        }
    }
}

修正後

  • ctx._tmid に setTimeoutの戻り値を保存
    • ctx._tmid が真なら送信待ちのキューが存在することになる
      • send()が呼ばれるたびにキューを作るのではなく、既に送信待ちのキューがあれば、それに便乗できるので、新たにsetTimeoutでキューを作成しないように変更
  • +new Date で時刻を取得し送信していたのを止めた
    • 適当にユニークな値をFlash側に送れればいいので、IEでコスト高な +new Date ではなく、1〜9 でグルグルする値(ctx._msgid)に切り替え
            // <param name="flashVars" param="t={time}&c={cmd}" />
            if (!ctx._tmid) {
                ctx._tmid = setTimeout(function() { // タイマーIDを保存
                    if (ctx._tmid && ctx._stock.length) {
                        ctx._view.flashVars = "i=" + ++ctx._msgid + // タイムスタンプではなくIDを送信
                                             "&c=" + ctx._stock.join("\t");
                        ctx._stock = []; // clear
                        ctx._msgid > 9 && (ctx._msgid = 0); // IDの値は 1〜9 まで
                        ctx._tmid = clearTimeout(ctx._tmid);
                    }
                }, 0);
            }

送信データの圧縮

ctx.moveTo(x,y) や ctx.lineTo(x,y) で指定する座標(x, y)は、ほとんどのケースで 0.1234567891234 のような半端な数値だったりします。

今までは小数点以下も含め全て送信していましたが、ちっさい数値は丸々無駄なデータなので、小数点以下は適当な桁(2〜3桁)でちょんぎって送る事に。

普通に考えると、Number.toFixed(3) なのですが、今は速度が必要なので、関数は極力呼ばずに何とかする必要があります。

100倍して切り捨てて文字列を送って、1/100する

toFixed のコストを測定した結果、moveTo, lineTo の値を加工(100倍 + 切り捨て + 文字列化)し、Flash側で数値に戻す際に1/100にすると toFixed を使った場合に比べ、1.5倍速くなることがわかりました。

JavaScript 側のコマンドエンコーダ

// CanvasRenderingContext2D.prototype.lineTo
function lineTo(x, y) {
    this.send("lT\t" + ((x * 100) | 0) + "\t" + ((y * 100) | 0)); // 100倍して端数を切り落とす
}
// CanvasRenderingContext2D.prototype.moveTo
function moveTo(x, y) {
    this.send("mT\t" + ((x * 100) | 0) + "\t" + ((y * 100) | 0)); // 0.12345... を "12" にエンコード
}

Flash 側のコマンドデコーダ

private function recv(msg:String):void {
  var ary:Array = msg.split("\t");
  var i:int = -1;
  var iz:int = ary.length;

  while (++i < iz) {
    switch (ary[i]) { // {COMMAND}
    case "mT": moveTo(ary[++i] * 0.01, ary[++i] * 0.01); break; // "12" を 0.12 にデコード
    case "lT": lineTo(ary[++i] * 0.01, ary[++i] * 0.01); break;
    }
  }

精度が必要になったら有効桁数を3〜4桁に増やすこともそんなに手間では無いので、割と良いアイデアじゃないかと思ってます。


デモ - 2/15版

Flash モードが速くなり、幾つかのページにストレスボタンを追加しています。

なんだかんだで、毎日ちょっとずつ速くなってます。

Flashモードは4日前まで 20fps がやっとだったけど、今は元気に 60fps で動いてます。


何で纏めて記事にしないの? とか

こういった、ジリジリと高速化していくネタは「がんばった。速くなった。これこれやった。はい終わり」のような過去形ではなく、現在進行形で書いたほうが良いかなぁ〜 とか思ってやってますよ。