JavaScriptの高速化3 - Hashによる検索を活用することで、ドラッグをもっとスムーズに

IE6のベンチスコアを追加
補足を追加

アイコンやウインドウをつかんでずずっと移動する。
この動作をもっともっとスムーズにするために、何ができるか考えてました。

以下はサンプルコードです。

uuClass.MyDrag = uuClass.Generic();
uuClass.MyDrag .prototype = {
  construct: function() {
    // ウインドウを包括するオブジェクト
    this._frame = uu.id("WindowFrame");
    // ウインドウのタイトル部分でmousedownされたらhandleEventを呼び出す。
    uu.event.set(this, uu.id("WindowTitle"), "mousedown");
  },
  handleEvent: function(evt) {
    uu.event.stop(evt); // イベントバブルの停止
    switch (evt.type) {
      case "mousedown":
        uu.event.set(this, uu.ua.ie ? uu.id("WindowTitle") : document,
                     "mousemove,mouseup", true);
        uu.event.drag(evt, this._frame, 1); // ドラッグ移動開始
        break;
      case "mouseup":
      case "losecapture":
        uu.event.unset(this, uu.ua.ie ? uu.id("WindowTitle") : document,
                     "mousemove,mouseup", true);
        uu.event.drag(evt, this._frame, 2); // ドラッグ移動終了
        break;
      case "mousemove":
        uu.event.drag(evt, this._frame, 3); // ドラッグ移動
        break;
    }
  }

switch〜caseの展開

switch〜caseで "mousemove" 等の文字列ではなく、数値化してから比較すればもっと速くなるのだろうか?
switch〜case自体を不要にできないか?

// これ追加
var Hash = {
    unknown:          0x0000,
    mousedown:        0x0001,
    mouseup:          0x0002,
    mousemove:        0x0003,
    losecapture:      0x0102  // mouseup for IE
};
// handleEventを差し替え
  handleEvent: function(evt) {
    uu.event.stop(evt); // イベントバブルの停止
    
    var _Hash = Hash; // scope solve (スコープの壁を溶かす)
    var bits = 0xff && (_Hash[evt.type] || 0x0);
    if (!bits) {
      if (bits <= 0x2) { // mousedown(0x01) and mouseup(0x02)
        uu.event.toggle(this, uu.ua.ie ? uu.id("WindowTitle") : document,
                        "mousemove,mouseup", true);
      }
      uu.event.drag(evt, this._frame, bits);
    }
  }

文字列を数値化して数値を比較するよりも、文字列を文字列のまま比較するほうが速いと考える人もいるかもしれません。「それは二度手間だ」とね。

しかし、Hashを使う方法に変更することで、体感できるぐらい高速化できましたし、ブラウザの差異(event.typeの違い)もうまく吸収できています。さらにヘビーループ部分のコードも若干短くなっているので、一石三鳥っぽいです。

なぜ速くなるの?

Hashとswitch〜caseでは、文字列検索に関わるコストが大きく異なります。

※ IE6 は別の PC でベンチ取ってます。 単位はms

Browser Method1
switch〜case "文字列": で検索
Method2
Hashで検索
Firefox3.03 6.9 2.3
Firefox2.0.07 5.9 2.2
Opera9.52 17.5 2.6
Opera9.27 36.6 4.6
Safari3.1.1 43.0 2.0
Chrome 28.0 3.3
IE8β2 163.7 3.9
IE6 67.2 4.6
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Hash BENCH</title>
<script id="uupaa.js" type="text/javascript" src="../../uupaa.js?module=dev"></script>
</head>
<body>
<div>
<input type="button" value="Method1" onclick="Method1()" />
<input type="button" value="Method2" onclick="Method2()" />
</div>

<script>
var HOGE = {
  EVENT_BITS: {
    unknown: 0x0,
    mousea: 0x1,      mouseaa: 0x1,
    mouseb: 0x2,      mouseab: 0x2,
    mousec: 0x3,      mouseac: 0x3,
    moused: 0x4,      mousead: 0x4,
    mousee: 0x5,      mouseae: 0x5,
    mousef: 0x6,      mouseaf: 0x6,
    mouseg: 0x7,      mouseag: 0x7,
    mouseh: 0x8,      mouseah: 0x8,
    mousei: 0x9,      mouseai: 0x9,
    mousej: 0xa,      mouseaj: 0xa,
    mousek: 0xb,      mouseak: 0xb,
    mousel: 0xc,      mouseal: 0xc,
    mousem: 0xd,      mouseam: 0xd,
    mousen: 0xe,      mousean: 0xe,
    mouseo: 0xf,      mouseao: 0xf,
    mousep: 0x10,     mouseap: 0x10,
    mouseq: 0x11,     mouseaq: 0x11,
    mouser: 0x12,     mousear: 0x12,
    mouses: 0x13,     mouseas: 0x13,
    mouset: 0x14,     mouseat: 0x14,
    mouseu: 0x15,     mouseau: 0x15,
    mousev: 0x16,     mouseav: 0x16,
    mousew: 0x17,     mouseaw: 0x17,
    mousex: 0x18,     mouseax: 0x18,
    mousey: 0x19,     mouseay: 0x19,
    mousez: 0x1a,     mouseaz: 0x1a
  }
};
var needle =
 ("unknown,"+
  "mousea,mouseb,mousec,moused,mousee,mousef,mouseg,mouseh,mousei,mousej,"+
  "mousek,mousel,mousem,mousen,mouseo,mousep,mouseq,mouser,mouses,mouset,mouseu,"+
  "mousev,mousew,mousex,mousey,mousez"+
  "mouseaa,mouseab,mouseac,mousead,mouseae,mouseaf,mouseag,mouseah,mouseai,mouseaj,"+
  "mouseak,mouseal,mouseam,mousean,mouseao,mouseap,mouseaq,mousear,mouseas,mouseat,mouseau,"+
  "mouseav,mouseaw,mouseax,mouseay,mouseaz").split(",");

function Method1() {
  var perf = new uuClass.Perf();

  perf.run(test, 1, 10);
  uu.log("Method1: " + perf.toString());

  function test() {
    var _needle = needle; // scope solve
    var rv = 0, i = 0, j = 0, iz = 100, jz = _needle.length;
    for (; i < iz; ++i) {
      for (j = 0; j < jz; ++j) {
        switch (_needle[j]) {
        case "mousea": rv = 0x1; break;  case "mouseaa": rv = 0x1; break;
        case "mouseb": rv = 0x2; break;  case "mouseab": rv = 0x2; break;
        case "mousec": rv = 0x3; break;  case "mouseac": rv = 0x3; break;
        case "moused": rv = 0x4; break;  case "mousead": rv = 0x4; break;
        case "mousee": rv = 0x5; break;  case "mouseae": rv = 0x5; break;
        case "mousef": rv = 0x6; break;  case "mouseaf": rv = 0x6; break;
        case "mouseg": rv = 0x7; break;  case "mouseag": rv = 0x7; break;
        case "mouseh": rv = 0x8; break;  case "mouseah": rv = 0x8; break;
        case "mousei": rv = 0x9; break;  case "mouseai": rv = 0x9; break;
        case "mousej": rv = 0xa; break;  case "mouseaj": rv = 0xa; break;
        case "mousek": rv = 0xb; break;  case "mouseak": rv = 0xb; break;
        case "mousel": rv = 0xc; break;  case "mouseal": rv = 0xc; break;
        case "mousem": rv = 0xd; break;  case "mouseam": rv = 0xd; break;
        case "mousen": rv = 0xe; break;  case "mousean": rv = 0xe; break;
        case "mouseo": rv = 0xf; break;  case "mouseao": rv = 0xf; break;
        case "mousep": rv = 0x10; break; case "mouseap": rv = 0x10; break;
        case "mouseq": rv = 0x11; break; case "mouseaq": rv = 0x11; break;
        case "mouser": rv = 0x12; break; case "mousear": rv = 0x12; break;
        case "mouses": rv = 0x13; break; case "mouseas": rv = 0x13; break;
        case "mouset": rv = 0x14; break; case "mouseat": rv = 0x14; break;
        case "mouseu": rv = 0x15; break; case "mouseau": rv = 0x15; break;
        case "mousev": rv = 0x16; break; case "mouseav": rv = 0x16; break;
        case "mousew": rv = 0x17; break; case "mouseaw": rv = 0x17; break;
        case "mousex": rv = 0x18; break; case "mouseax": rv = 0x18; break;
        case "mousey": rv = 0x19; break; case "mouseay": rv = 0x19; break;
        case "mousez": rv = 0x1a; break; case "mouseaz": rv = 0x1a; break;
        case "unknown":
        default:
          rv = 0x0; break;
        }
      }
    }
  }
}
function Method2() {
  var perf = new uuClass.Perf();

  perf.run(test, 1, 10);
  uu.log("Method2: " + perf.toString());

  function test() {
    var _needle = needle; // scope solve
    var eb = HOGE.EVENT_BITS;
    var rv = 0, i = 0, j = 0, iz = 100, jz = _needle.length;
    for (; i < iz; ++i) {
      for (j = 0; j < jz; ++j) {
        rv = 0xff && (eb[_needle[j]] || 0);
      }
    }
  }
}
</script>
</body></html>

結論的な何か

本当に速度が必要ならば、このようなコードではなく

function eventHandler(evt) {
  switch (evt.type) {
  case "mousedown": ...
  case "mouseup": ...
  case "mousemove": ...
  case "mousewheel": ...
  case "click": ...
  case "DOMMouseScroll": ... // mousewheel for Firefox2+
  case "losecapture": ... // mouseup for IE
  ...
  }
}

こうしましょう

var Hash = {
  mousedown:      0x0001,
  mouseup:        0x0002,
  mousemove:      0x0003,
  mousewheel:     0x0004,
  click:          0x0005,
  DOMMouseScroll: 0x0104,
  losecapture:    0x0102,
  ...
}
function eventHandler(evt) {
  var _hash = Hash; // scope solve
  var bits = 0xff && (eb[_hash[evt.type]] || 0x0); // 下位1byte
  if (!bits) { return; }
// ▼▼▼▼
  switch (bits) {
  case 0x03:  // ドラッグ移動 // Hash.mousemove // 念のため mousemove を一番上にもってくる
  case 0x01:  // ドラッグの移動開始 // Hash.mousedown
  case 0x02:  // ドラッグの終了 // Hash.mouseup
  }
// ▲▲▲▲
// ▼▼▼▼ または最初の例のように、こんな感じでもOK
  if (bits <= 0x02) { // mousedown(0x01) and mouseup(0x02)
    ; // イベントのアタッチ or デタッチ
  }
  ; // ドラッグ処理
// ▲▲▲▲
}

Hashによる検索は事前にコンパイラによって最適化されます(binary treeにする過程で最適化される)。
JavaScriptの case には数値定数以外の値(文字列や式)を記述可能なので、そこそこの最適化しか期待できません。

switch (a) {
case a * 3 > 1: alert("これは正しいcase文"); break;
}

case文が文字列だと、strcmp相当の関数の呼び出しが1回 + 比較用のレジスタが3〜4個(ポインタ2個+whileループ用1〜2個), スタック用で5〜個確保する必要がありますが、数値比較は、アキュムレータへのフェッチ + 比較命令だけで完結するため、文字列比較よりも1〜2桁少ないコストで済みます。
# caseに可変式書かれたら素直にテーブルジャンプ式にはできないわなぁ…

case文で定数(Hash.xxx)ではなくマジックナンバー(0x01とか0x02)を使っているのにもそれなりの理由があります。コメントでしっかりと補足できるなら定数をcase文に記述するよりもマジックナンバーを使うほうがより高速です(そこ、気持ち悪いとか言わない)。

反省会

  • もうね、ドラッグ移動がのろいだけで、Webアプリとしてのファーストインプレッションは最悪ですよ。
    • UIはしっかりと高速化すべき。もっさりな実装は、5秒でブラウザ閉じられちゃいます。
      • キャッチの前にリリースしっぱなしですよ。
  • JavaScriptで速度が必要なら、マジックナンバーであっても消極的に活用すべきだと思うんです。ハイ。

補足: IE以外のブラウザでは、switch 〜 case も、ちゃんと早い。

// 例:
switch (a) {
case ">": ..
case "+": ..
case "~": ..
}

IE 以外のブラウザでは、この程度のswitch〜caseは、Hashよりも若干早いです。
文字列を、charAt() や charCodeAt() で数値化してから比較しようとすると関数呼び出しコストがかかる為、予想に反して遅くなります。気をつけましょう。