ラベル ベンチマーク の投稿を表示しています。 すべての投稿を表示
ラベル ベンチマーク の投稿を表示しています。 すべての投稿を表示

2018年11月19日

CPU負荷を抑えて重い処理を軽くするJavaScriptライブラリ「chillout.js」

「chillout.js」とは?


chillout.js は「処理時間を短くする」という物理的な高速化とは違い、CPU負荷を抑えてリソースに余裕を持たせ、重い処理でも軽く感じさせることでユーザーにとって体感的・心理的な高速化につなげる JavaScriptライブラリです。


重い処理から開放されるために


JavaScriptでfor文など繰り返し(ループ)処理をしたとき、重い処理により一瞬でもページが固まってしまうことがあります。
そんなときブラウザ画面は読み込み中のままローディングのくるくるが止まらなかったりゲーム中にマウスが効かなくてカクカクだったり、
Webページだけじゃなくnode.jsなど画面のいらない処理でも高CPU負荷が続くとマシンごと重くなって大変です。

特にスペックの低いPCやタブレットの場合、 CPU使用率100%の状態では加熱してきて冷却ファンが高回転になり、そのまま使い続けると冷却が追いつかずに熱暴走してしまう可能性もあるので、うっかり重いページを開いてファンが「ウイーン!」って言い出すとヒヤヒヤします。

最近のPCは性能がよくなってるのでそんな事態になりにくいかもしれませんが、自分のPCも真夏の気温で動かなくなって修理にだしたのはまだ新しい記憶です😭 (直りました)。

「CPUファンが回りっぱなしかと思ったらフリーズして電源が落ちた」なんてことになると作業途中だったらやり直しになるし、HDDやSSDなどの内部にもダメージが残るかもしれないのでなるべく処理を軽くしたいところです。

JavaScriptでCPU負荷を抑えるには?


重い処理のほとんどはループ処理によって発生します。ループの中でさらにループ、その中でさらにループ…。
ループ間の処理が重くなってくるとマシンのCPUは休む暇なく動き、結果として内部が熱くなるため冷却ファンをたくさん回すことになります。
単純に考えた場合、そうならないようループの途中で一定時間処理を休止させればいいんですが、それができません。
JavaScriptには一定の時間休む sleep のような機能がないからです

そこで、sleepするにはどうするか?というと「非同期」でループ処理します。

CPUを休ませるために非同期でループ処理する


JavaScriptのループは for文や while文、また Array に対して forEachなどがありそれぞれ同期で処理されますが、上述したように sleep して休ませることができないため、これらのループ処理を非同期で実現 します。

同期処理を非同期化するには、
  • setTimeoutを使う
  • process.nextTickを使う(node.js)
  • DOMイベントを使う
  • MessageChannelを使う

などの方法があり、一連の処理を Promise と組み合わせると非同期ループが実現できます

(setInterval や requestAnimationFrame を使うこともできますが、これらは精度を保とうとより正確にループしようとするため今回の用途には不向きです。)

今までも CPU負荷を抑えるためのライブラリをいくつか作ってきたのですが、当時は Promise という便利なものがJavaScriptになかったので自前で Deferred と呼ばれる Promise のようなものを定義していました。そのためにライブラリのサイズが大きくなるしAPIがガラパゴス化してたんですが、ようやくシンプルな実装にできたと思います。

処理時間を短くするんじゃなく、体感速度を向上させる心理的な高速化



処理の高速化というと、とにかく1ミリ秒でも処理時間を短くすることが手法とされますが、Webページやアプリ、ゲームといった、人が画面を見たり操作する場合 心理的に「速い」と感じればユーザーのストレスが減り結果として高速化につながります


Twitterでいい例があったので紹介。
これはエクセルマクロの話ですが 処理時間を短くするんじゃなく待ち時間を退屈させないようにして体感速度を向上させて、心理的な高速化しています。


CPU負荷を抑えてループ処理を軽くするJavaScriptライブラリ「chillout.js」



冒頭が長くなっちゃいましたが、ライブラリの紹介です。

chillout.js は、ループ処理中に適度な休憩(sleep)を入れてあげ、カクカクする重さを感じさせないライブラリ です。
また、処理が重くなるとでる「警告: 応答のないスクリプト」というブラウザ警告なしでJavaScriptを実行できます

ループ処理が重いときにはCPUが休まるくらいの休止時間、処理が速いときには休止時間なしか、わずかな休止時間をいれ本来のループを邪魔しないようにします

ベンチマーク


for文と chillout.repeat を比較します。

function heavyProcess() {
  var v;
  for (var i = 0; i < 5000; i++) {
    for (var j = 0; j < 5000; j++) {
      v = i * j;
    }
  }
  return v;
}

例として上のような重い処理(テスト用に5000*5000回繰り返す処理)に対して、

for文

JavaScriptのfor文。

var time = Date.now();
for (var i = 0; i < 1000; i++) {
  heavyProcess();
}
var processingTime = Date.now() - time;
console.log(processingTime);

CPUグラフ:
  • 処理時間: 107510ms.
  • CPU平均使用率(Nodeプロセス): 97.13%

CPUグラフは上のようになり、CPU均使用率は 100% までいってないけど 97.13% になりました。

chillout.repeat

これを「chillout.js」のメソッド chillout.repeat で実行します。

var time = Date.now();
chillout.repeat(1000, function(i) {
  heavyProcess();
}).then(function() {
  var processingTime = Date.now() - time;
  console.log(processingTime);
});

CPUグラフ:
  • 処理時間: 138432ms.
  • CPU平均使用率(Nodeプロセス): 73.88%

ベンチマーク結果


  ForStatement (for文) chillout.repeat
処理時間 107,510ms. 138,432ms.
CPU平均使用率(Nodeプロセス) 97.13% 73.88%


グラフでは少しわかりにくいかもしれませんが、CPU平均使用率は for文 97.13% に対し、 chillout.repeat は 73.88% となり、for文よりもCPU使用率が抑えられてます

そのかわりループ中にCPUを休ませてるため処理時間は 107,510ms から 138,432ms となり少しかかっています。
もっとCPU使用率を抑えるには単純に休止時間を増やせばいいんですが、そうすると処理時間が長くなるため適度なバランスにしています。

画面上ではわかりませんが、for文を実行してるときはCPUファンが「ウイーン!」と激しく回っていたのに対し、chillout.repeat のときはファンが静かめでした(環境によってそこまで変わらないかもですが)。

chillout.js は、処理速度を少し遅くするかわりに低いCPU使用率で安定してJavaScriptを実行できる特徴があります。

特にブラウザやゲームなど、人が画面を見て操作するJavaScriptのパフォーマンスにおいて最も重要なことの一つは、 数値的な速度だけでなく、安定したレスポンスによってユーザーにストレスなく動かすことと考えています。

(ベンチマークスペック: Windows8.1 タブレット / Intel(R) Atom(TM) CPU Z3740 1.33GHz)

API



for文やwhile文、Array.forEachに対応するメソッド4つがあります。そのうち2つ紹介。

chillout.repeat

for文のような動きをします。

// 5回繰り返す
chillout.repeat(5, function(i) {
  console.log(i);
}).then(function() {
  console.log('done');
});
// 0
// 1
// 2
// 3
// 4
// 'done'

chillout.forEach

Array.forEachと同じように動く。

// 配列を回す
var values = ['a', 'b', 'c'];
chillout.forEach(values, function(value) {
  console.log(value);
}).then(function() {
  console.log('done');
});

// 'a'
// 'b'
// 'c'
// 'done'

非同期処理で Promise が返されるため then で繋ぎます。
(他のAPIの詳細は chillout.jsのGitHub を参考ください。)

比較表


既存のJavaScriptループを chillout.js のAPIに置き換えると、大抵の場合 CPU使用率を抑えて実行できます。

変換例:

JavaScript chillout.jsの場合
[1, 2, 3].forEach(function(v, i) {}) chillout.forEach([1, 2, 3], function(v, i) {})
for (i = 0; i < 5; i++) {} chillout.repeat(5, function(i) {})
for (i = 10; i < 20; i += 2) {} chillout.repeat({ start: 10, step: 2, done: 20 }, function(i) {})
while (true) {} chillout.till(function() {})
while (cond()) {} chillout.till(function() {
  if (!cond()) return chillout.StopIteration;
})
for (value of [1, 2, 3]) {} chillout.forOf([1, 2, 3], function(value) {})


GitHub / chillout.js



GitHub / chillout.js

※このブログの内容は常に更新してるわけじゃないので古くなってる可能性があります。特にAPIの最新の情報は GitHub / chillout.jsを参考ください。


2012年9月4日

重い処理を軽くできるJavaScriptライブラリ作ってみた

追記: この記事のものは古くメンテナンスされていません。
CPU負荷をかけずにループ処理するJavaScriptライブラリは「CPU負荷を抑えて重い処理を軽くするJavaScriptライブラリ「chillout.js」 | 圧縮電子どうのこうの」を参照ください。

lazyIter.js

lazyIter.js は CPU 負荷をかけずにループ処理が可能な JavaScript ライブラリです。

重いループ処理などを軽くすることができます。
ループ内で処理の負荷に応じて遅延させ、結果的に重い処理は軽くなり
元々軽い処理は、ほぼ従来の速度のまま実行できます。

処理の負荷だけでなく、時間のかかるループ処理は
「応答のないスクリプト」警告がでてしまいます。
そのような警告なしで実行できるのも特徴の一つです。

どうに違いがあるのかは CPU 負荷テストページ で実際に試してみてください

Pot.js が実装しているイテレータから派生しています。
パフォーマンスなどは Pot.js のイテレータとほとんど同じです

ループ処理は非同期で実行されます。

使い方は Pot.js のイテレータ と同じですが、Deferred を使っていないので
引数にコールバックを指定する点が違います。

動作環境

主な Web ブラウザで動作します
  • Mozilla Firefox *
  • Internet Explorer 6+
  • Safari *
  • Opera *
  • Google Chrome *
また、以下の環境でも動作します
  • Greasemonkey (userscript)
  • Mozilla Firefox Add-On (on XUL)
  • Node.js
  • Other non-browser environment

lazyIter.js は、CommonJS などに対応しています

使い方

lazyiter.js を読み込みます (Webページの場合)

<script src="lazyiter.js"></script>

読み込むと、lazyIter というオブジェクトが定義されます (定義されるのは lazyIter だけです)。

lazyIter.forEach :

オブジェクトや配列に対してループ実行します

// forEach:
//
// lazyIter.forEach(object, func, callback [, speed [, context]])

// Array.
lazyIter.forEach(['a', 'b', 'c'], function(val, i) {
  console.log(i + ':' + val);
}, function() {
  console.log('End loop');
});
// result:
//   '0:a'
//   '1:b'
//   '2:c'
//   'End loop'

// Object.
lazyIter.forEach({a: 1, b: 2, c: 3}, function(val, key) {
  console.log(key + ':' + val);
}, function() {
  console.log('End loop');
});
// result:
//   'a:1'
//   'b:2'
//   'c:3'
//   'End loop'

lazyIter.repeat :

指定の数だけループ実行します

// repeat:
//
// lazyIter.repeat(max, func, callback [, speed [, context]])

// Specify as number.
lazyIter.repeat(10, function(i) {
  console.log(i);
}, function() {
  console.log('End loop');
});
// result:
//   0
//   1
//   2
//   3
//   4
//   5
//   6
//   7
//   8
//   9
//   'End loop'

// for文のように 初め(begin)、ステップ(step)、終わり(end) を指定
lazyIter.repeat({begin: 0, step: 5, end: 30}, function(i) {
  console.log(i);
}, function() {
  console.log('End loop');
});
// result:
//   0
//   5
//   10
//   15
//   20
//   25
//   'End loop'

lazyIter.forEver :

lazyIter.StopIteration が throw されるまでループし続けます

// forEver:
//
// lazyIter.forEver(func, callback [, speed [, context]])

var end = 10;
lazyIter.forEver(function(i) {
  if (i === end) {
    throw lazyIter.StopIteration;
  }
  console.log(i);
}, function() {
  console.log('End loop');
});
// result:
//   0
//   1
//   2
//   3
//   4
//   5
//   6
//   7
//   8
//   9
//   'End loop'

throw lazyIter.StopIteration; により各イテレートを止めることができます。
引数 'context' は、コールバック関数の 'this' として使われます (省略可)。
引数 'speed' を指定することで速度の調節ができます (省略可)。
  • 'ninja' : もっと速い
  • 'rapid' : 速い
  • 'fast' : 速め
  • 'normal' : 通常 (default)
  • 'slow' : 遅め
  • 'doze' : 遅い
  • 'limp' : もっと遅い

速度の指定 (rapid, ninja など) は、Pot.js と同じです

Download

ダウンロード

動作テスト (サンプル)

レポジトリ



その他、なにか問題・バグ・感想・指摘などあれば、
コメントやメールまたは @polygon_planet まで送っていただけるとうれしいです。



2012年3月12日

jsFiddleだらけ-JavaScriptライブラリPot.js+PotLite.jsリリースノート



Pot.js 1.15 1.16PotLite.js 1.32 1.33 リリースしました。

2012-03-13 追記:
Pot.js 1.15 と PotLite.js 1.33 はバギーなためアップデートしました。詳細
2012-03-13 時点の最新は Pot.js 1.16PotLite.js 1.34 です。最新にアップデートお願いします。。


このバージョンでは、
  • 文字列処理の高速化
  • 重くなりそうな処理を非同期化
などを適応しました。

文字列処理の高速化

先日、ふと思って String.fromCharCode を呼ばずに
U+0000 - U+FFFF の配列をあらかじめ作成して インデックスに対応させたらどうなのかなって
ベンチマークとってみました。


結果として、String.fromCharCode(c) のような
apply で配列を使わない場合、かなり高速化できました。
メモリ消費も文字列などは気にするほどじゃなかった。
String.fromCharCode.apply(null, [...]); のような場合は逆に遅くなるので
従来通り String.fromCharCode を使用しています。

(この件はあほなミスをしてて@gochoさんにつっこまれて助かりました)

重くなりそうな処理を非同期化

重くなりそうな処理とは、巨大な文字列が渡される可能性のある関数や、
ループ回数が未知の処理などです。

途中で「応答のないスクリプト」警告なんぞ でてしまったらめんどうです。
そういった対処や負荷軽減も含めて 関数オブジェクトに deferred というメソッドを持たせました。

例えば Pot.md5(string); が同期実行に対して
Pot.md5.deferred(string); は、非同期で実行します。


圧縮・解凍

文字列を LZ77 アルゴリズムをベースに圧縮・解凍する
Pot.Archive.AlphamericString も同様に非同期化しています。

その他のサンプル

他にもいくつかテスト用にサンプルがあったので紹介します。



Pot.js / PotLite.js

Pot.js は CPU に負荷をかけることなく JavaScript の実行を可能とするユーティリティライブラリです。

PotLite.js は Pot.js の非同期な部分だけを抽出したライトバージョンです。

ダウンロード

マニュアル

その他の情報についてはマニュアル/マニュアルからのリンク から参照ください。

レポジトリ




その他、なにか問題・バグ・感想・指摘などあれば、
コメントやメールまたは @polygon_planet まで送っていただけるとうれしいです。


2012年2月11日

JavaScript whileループとPot.Deferred.forEverイテレータでCPU使用率を比較-Pot.js+PotLite.jsリリースノート

Pot.js 1.13PotLite.js 1.30 リリースしました。

Pot.js 1.13 と PotLite.js 1.30 では、主に内部ループ処理を大幅に高速化しました。
(そろそろ ChangeLog 作らないとまずいかも…)

あとは、変数宣言とか 例の (function(){}()) とか (結局直してる)
細かい修正などです。

先日、Hacker NewsEcho JS で Pot.js が紹介されました (ありがとうございます)。
でもアクセスが今までの 1000 倍くらいになって、すごいことになってちょっとビビった。
(API サーバのほうは適当に調節しておいたので大丈夫だった。よかった。。)


それで Pot.js の本来の存在意義ですが、

実行環境の CPU に負荷をかけることなく JavaScript が実行できる。

といったことが本来の目的であり、常に追求している目標でもあります。
Deferred が重点にとらわれがちですが、Pot.Deferred はそれの足がかりであり、
Pot.Deferred だけがライブラリの中核ではないので、いろいろ使ってみてください。

新しいデザインのリファレンス

現在、新しいデザインでリファレンスを書き直しています。


前は 全部で 1 ファイルになっちゃってて、どんどん重くなるし更新もやり難くて
どうしようもなかったのですが、今回は 各ページを非同期読み込みにして
英語版と日本語版で見れるようにして、戻るボタンとかも再現したり、がんばってます。

でもまだ作成途中です (2012-02-11 現在)。

ある程度完成したら 本来の /index.html に移動させようと思ってるんですが、
先ほどの Echo JS などが /test-for-new-design/ にリンクしちゃってるもんだから
どうしようかと考え中。(たぶん リダイレクトか、もうこのままでいいか…)。

CPU 比較を実行

jsFiddle のほうで
JavaScript の while ループと、Pot.js の非同期イテレータとで CPU 使用率の比較を書いてみました。
前に書いた Pot.js イテレータと jQuery.each と for ループの CPU 使用率をグラフで比較
と同じ事ですが、実際に実行できます (ただし負荷テストなのでブラウザクラッシュに気をつけてください)。
もっとも最近のブラウザはループでクラッシュしないと思いますが。。

CPU 使用率は、Web ブラウザからの JavaScript では取得できないと思うので、
CPU モニタリングするアプリとか、
なければ Win ならタスクマネージャから「パフォーマンス」→ 「CPU 使用率」
を見ながら実行してみるとわかりやすいと思います。


今回は while 文と Pot.Deferred.forEver の比較ですが、
Pot.js 非同期イテレータは CPU 負荷が一定量に抑えられたループが可能になっていると思います。
ただし、この例のような処理だと実行時間はある程度伸びてしまいます。

実行時間に関しては 速度調整 が可能なので、処理に応じたスピードを選ぶこともできます。
(このへんは今後改善したいです)。

サンプルのソースコード

jsFiddle でも確認できますが、今回使用したサンプルのソースコードです。
ループ処理の部分だけ抽出しています。

JavaScript while ループを使ったソースコード:
// while で同期ループして圧縮
compressSync : function(s) {
    var a = 53300, b, c, d, e, f, g = -1,
        h, i, r = [], x = String.fromCharCode;

    s = new Array(a--).join(' ') + s;
    while ((b = s.substr(a, 256))) {
        for (c = 2, i = b.length; c <= i; ++c) {
            d = s.substring(
                a - 52275,
                a + c - 1
            ).lastIndexOf(b.substring(0, c));
            if (!~d) {
                break;
            }
            e = d;
        }
        if (c === 2 || c === 3 && f === g) {
            f = g;
            h = s.charCodeAt(a++);
            r.push(
                x(h >> 8 & 255),
                x(h & 255)
            );
        } else {
            r.push(
                x((e >> 8 & 255) | 65280),
                x(e & 255),
                x(c - 3)
            );
            a += c - 1;
        }
    }
    return r.join('');
}

Pot.js 非同期イテレータ (今回は Pot.Deferred.forEver) を使ったソースコード:
// Pot.js 非同期イテレータで圧縮
compressAsync : function(s) {
    var a = 53300, b, c, d, e, f, g = -1,
        h, i, r = [], x = String.fromCharCode;

    var deferred = new Pot.Deferred();

    s = new Array(a--).join(' ') + s;

    // whileループを forEver に置き換え
    Pot.Deferred.forEver[SPEED](function() {

        b = s.substr(a, 256);
        if (!b) {
            throw Pot.StopIteration;
        }

        for (c = 2, i = b.length; c <= i; ++c) {
            d = s.substring(
                a - 52275,
                a + c - 1
            ).lastIndexOf(b.substring(0, c));
            if (!~d) {
                break;
            }
            e = d;
        }
        if (c === 2 || c === 3 && f === g) {
            f = g;
            h = s.charCodeAt(a++);
            r.push(
                x(h >> 8 & 255),
                x(h & 255)
            );
        } else {
            r.push(
                x((e >> 8 & 255) | 65280),
                x(e & 255),
                x(c - 3)
            );
            a += c - 1;
        }
    }).then(function() {
        deferred.begin(r.join(''));
    }, function(err) {
        deferred.raise(err);
    });
    return deferred;
}

同期か非同期かの違いがありますが、
単に、メインの while 文を Pot.Deferred.forEver に変えてるだけです。
forEver は、StopIteration が throw されるまで、永久にループする関数です。

あと、この関数は LZ77アルゴリズムによる圧縮関数をJavaScript最短コードで | 圧縮電子精神音楽浮遊構造体 (見れないかも)
で作ったソースコードをちょっとだけ手直ししたものです。
文字列を圧縮解凍します。

Pot.js / PotLite.js

Pot.js は CPU に負荷をかけることなく JavaScript の実行を可能とするユーティリティライブラリです。

PotLite.js は Pot.js の非同期な部分だけを抽出したライトバージョンです。

ダウンロード

マニュアル

マニュアルは上に書いたように 2012-02-11 現在、まだすべてのオブジェクトの解説ができてません。。

その他の情報についてはマニュアル/マニュアルからのリンク から参照ください。

レポジトリ




その他、なにか問題・バグ・感想・指摘などあれば、
コメントやメールまたは @polygon_planet まで送っていただけるとうれしいです。


2011年12月17日

JavaScriptの無名関数の実行 (function(){})() と (function(){}()) の違い


JavaScript Advent Calendar 2011 (オレ標準コース) 17 日目、polygon_planet です。

ずっと Advent Calendar 参加してみたいなぁと思ってたんですが
ネタが思いつかない日々で半分諦めてたんですが、考え過ぎな気がしてきたので
別におもしろい記事でもないし、技術的にも参考になるのか不明ですがとりあえず書きます。

もしかしたら同じようなこと解説してる記事がすでにあるかも…(うまく検索できてない)


JavaScript で無名関数をその場で実行するとき、
(function() {
// 処理
})();
という書き方が主流っぽいですが、
(function() {
// 処理
}());
こう書いたらどう違うのか。

ちなみに、
function() {

}(); // エラー
これは式ではなく文として扱われるためエラーです。
ただし、以下のように実行する場合はエラーではありません。
var arr = [function(){}()];        // [undefined]
var obj = {foo: function(){}()}; // {foo: undefined}

(function(){})() vs (function(){}())

どちらも同じように実行され、結果的には同じなので
とりあえず、無名関数が生成され実行するまでの JavaScript コードの過程を追ってみます。

今回は手元に SpiderMonkey に付いてる JavaScript shell しかなかったので、それを使います。
わりと前にコンパイルしたやつですが JavaScript バージョンは 1.8 なので現行と大差ないと思います。
中間コードは js shell の dis で逆アセンブルして確認します。

まず (function(){})() から。
$ js
js> dis(function() { (function(){})() });
flags: LAMBDA INTERPRETED
main:
00000: anonfunobj (function () {})
00003: group
00004: pushobj
00005: call 0
00008: pop
00009: stop

Source notes:
0: 5 [ 5] pcbase offset 5
anonfunobj というは、無名関数を定義していると思われますが、
確認のため Mozilla のソースコードを見てます。

今回は js shell のバージョンに合わせるため
/mozilla-central/ ではなく /mozilla/ を参照します。

jsopcode.tbl
305 /* Auto-clone (if needed due to re-parenting) and push an anonymous function. */
306 OPDEF(JSOP_ANONFUNOBJ, 128, "anonfunobj", NULL, 3, 0, 1, 19, JOF_OBJECT)
このあたりの JSOP_* から参照できます。
JSOP は jsopcode.c あたりで確認できます。
3777               case JSOP_ANONFUNOBJ:
3778 #if JS_HAS_GENERATOR_EXPRS
3779 sn = js_GetSrcNote(jp->script, pc);
3780 if (sn && SN_TYPE(sn) == SRC_GENEXP) {
3781 JSScript *inner, *outer;
3782 void *mark;
3783 SprintStack ss2;
3784
3785 LOAD_FUNCTION(0);
3786 inner = fun->u.i.script;
// ... (長いので省略) ...
ここでは LOAD_FUNCTION が使われています。

JavaScript バイトコードインタプリタの jsinterp.c を参照して無名関数の生成から追ってみます。
5744           BEGIN_CASE(JSOP_ANONFUNOBJ)
5745 /* Load the specified function object literal. */
5746 LOAD_FUNCTION(0);
5747
5748 /* If re-parenting, push a clone of the function object. */
5749 parent = js_GetScopeChain(cx, fp);
5750 if (!parent)
5751 goto error;
5752 obj = FUN_OBJECT(fun);
5753 if (OBJ_GET_PARENT(cx, obj) != parent) {
5754 obj = js_CloneFunctionObject(cx, fun, parent);
5755 if (!obj)
5756 goto error;
5757 }
5758 PUSH_OPND(OBJECT_TO_JSVAL(obj));
5759 END_CASE(JSOP_ANONFUNOBJ)
ここでも LOAD_FUNCTION が使われているので定義を追ってみます。
jsinterp.c
2604 #define LOAD_FUNCTION(PCOFF)                                                  \
2605 JS_GET_SCRIPT_FUNCTION(script, GET_FULL_INDEX(PCOFF), fun)
JS_GET_SCRIPT_FUNCTION を実行するマクロになっています。

じゃあ JS_GET_SCRIPT_FUNCTION はどうなってるの
jsscript.h
143 #define JS_GET_SCRIPT_FUNCTION(script, index, fun)                            \
144 JS_BEGIN_MACRO \
145 JSObject *funobj_; \
146 \
147 JS_GET_SCRIPT_OBJECT(script, index, funobj_); \
148 JS_ASSERT(HAS_FUNCTION_CLASS(funobj_)); \
149 JS_ASSERT(funobj_ == (JSObject *) STOBJ_GET_PRIVATE(funobj_)); \
150 (fun) = (JSFunction *) funobj_; \
151 JS_ASSERT(FUN_INTERPRETED(fun)); \
152 JS_END_MACRO
JSFunction にキャストして JavaScript 関数オブジェクトになることが確認できました。
(これは現行の JavaScript だともっとてっとり早くなってる)

こんな感じで追ってみると、anonfunobj は無名関数の生成ということがわかります。

次の group は、
jsopcode.tbl で同じく定義されています。
317 /* Parenthesization opcode to help the decompiler. */
318 OPDEF(JSOP_GROUP, 131, "group", NULL, 1, 0, 0, 19, JOF_BYTE)
jsopcode.c では以下のあたり。
2066               case JSOP_GROUP:
2067 cs = &js_CodeSpec[lastop];
2068 if ((cs->prec != 0 &&
2069 cs->prec <= js_CodeSpec[NEXT_OP(pc)].prec) ||
2070 pc[JSOP_GROUP_LENGTH] == JSOP_NULL ||
2071 pc[JSOP_GROUP_LENGTH] == JSOP_DUP ||
2072 pc[JSOP_GROUP_LENGTH] == JSOP_IFEQ ||
2073 pc[JSOP_GROUP_LENGTH] == JSOP_IFNE) {
// ...(中略)...
2100 }

無名関数オブジェクトを生成し、それを括弧 () によって式とし、
そしてそれを call というような流れになっています。


続いて (function(){}()) のほうも同じように。
$ js
js> dis(function() { (function(){}()) });
flags: LAMBDA INTERPRETED
main:
00000: anonfunobj (function () {})
00003: pushobj
00004: call 0
00007: pop
00008: stop

Source notes:
0: 4 [ 4] pcbase offset 4

なんと、(function(){})() と比べると
group がないだけで、ほかは同じになってしまいました。

無名関数に対する Parenthesization が発生しないので、
ちょうど、上で書いた [function(){}()] のように実行されています。

この結果から、

(function(){})() よりも、 (function(){}()) のほうが
コストが減るとわかりました。


とは言っても、どちらを使用しても実行時間が変化するとしてもマイクロ秒単位などで
JavaScript でそこまでクリティカルな処理が存在するのかも微妙なところで
あまり気にしないでいい気がします。


でも一応ベンチマークをとってみます。
js> for (var t = +new Date, i = 0, f; i < 10000000; i++) f = (function(){}());
print(+new Date - t);
10437
js> for (var t = +new Date, i = 0, f; i < 10000000; i++) f = (function(){})();
print(+new Date - t);
10485

結果:
  • (function(){}()) : 10437 ms.
  • (function(){})() : 10485 ms.

たしかに (function(){}()) のほうが速かったんですが、
10,000,000 回ループして わずか 48 ミリ秒の違い。
これを懸念してる人って…(いるのでしょうか)


そういえばこの違いについてはちょっと前に Twitter で話題(?) になって気がします。
もしかしてもうまとめられてたのかな…。だとしたら……考えたくない。


とりあえずモヤモヤした感じがなくなったのでよかった。
たまにソースコード確認してみると新鮮デスネ!


2011年11月5日

フリーズやカクカクしない安定した動作のアプリやアドオン、ゲームなどが作れるJavaScriptライブラリ

Web を閲覧していると、突然 CPU 100% になったり
定期的に負荷がかかったりします。

flash などを除くと、それ以外のほとんどは JavaScript による重いループ処理が原因です。
少しずつ改善されてるような気はしますが
おそらくもう何年も前から、この問題がついてまわっていたのではないでしょうか。

そしてこのような負荷は、高スペックなマシンでは気付かないと思います。
開発者やテスターが高スペックな PC で動作確認を行ったことにより、
瞬間的な負荷を見過ごされたまま公開、リリースされてしまうこともあると思います。
それを、低スペックなマシンや iPhone などスマホを使っているユーザーが実行し
負荷に耐え切れずカクカクな描画になってしまったり、
最悪フリーズしてしまう場合もあります。

マシンの性能に限らず、例え高スペックマシンでも
いくつものアプリケーションを常時起動していて
タスクがいっぱいになっているユーザーもこのような事例に該当します。

何らかのアプリケーションを開発する場合、
処理の高速化は常に求められます。
より高速に動作したほうが軽快なのは確かです。

ですがクライアントアプリケーションを作る場合、
CPU 100% 使う処理を 10 秒も 20 秒も続けられてしまったら
それはもう不快でしかありません。
さらにそれが非ブロックでなく実行されてたら
マシンはもうフリーズのような動かない状態になりっぱなしです。

単純に高速を目指すのではなく、UI のことを考え制御を返すことが重要です。
UI を無視してひたすら高速に実行して 5 秒かかる処理だったら、
定期的に UI に制御を返し
例え数秒遅くなってもバックグラウンドで行い CPU 負荷を抑えることが
ユーザーへのストレス軽減として重要と考えています。

さらに非同期で実行することで並列化も可能になり、
逆に本来より速く実行できる可能性もあります。


PotLite.js は、そのような負荷軽減を可能とする JavaScript ライブラリです。
非同期処理をチェインで扱う Deferred オブジェクトを中心に
いろいろなループ処理を非同期で実行できるイテレータを実装しています。

イテレータは PotLite.js バージョン 1.21 現在、
  • forEach
  • repeat
  • forEver
  • iterate
  • items
  • zip
  • map
  • filter
  • reduce
  • every
  • some
が実装されています。
非同期での実行、同期での実行、そして Deferred チェイン上での実行が可能です。

そして、速度指定できるのが特徴の一つです。
Deferred.forEach(obj, function(value, key) {...})
上のコードが forEach (jQuery での jQuery.each のようなもの) の非同期での通常実行になり、
Deferred.forEach.slow(obj, function(value, key) {...})
上のように .slow と速度を明示することができます。
すると各ループをゆっくり目で実行します。

他にも .fast などの指定ができます。
関数内で return wait(1); 等と Pot.Deferred オブジェクトを返すことで
各ループ間でより細かな wait などの調整ができます。
逆に、非同期イテレートをより速く実行したい場合は
Pot.Defered.forEach.ninja(...) と、
最も速く実行するよう定義されている .ninja を指定すると
通常の for 文を単に関数で包んだのと同じくらいの速度で実行できます。

より詳しくは リファレンス を参照してください。


どれくらい変化があるかベンチマークをとってみました。
イテレータ JSON ファイルサイズ CPU 最大使用率 CPU 平均使用率 実行時間 (ms)
for 文 1MB 14.52% 10.61% 1211ms.
jQuery.each 1MB 16.28% 13.53% 1362ms.
Pot.Deferred.forEach 1MB 11.65% 5.53% 1647ms.
for 文 5MB 24.85% 21.71% 4717ms.
jQuery.each 5MB 27.78% 24.29% 5570ms.
Pot.Deferred.forEach 5MB 13.74% 8.61% 8925ms.
for 文 10MB 28.14% 25.92% 10699ms.
jQuery.each 10MB 31.09% 30.77% 15844ms.
Pot.Deferred.forEach 10MB 18.62% 9.77% 38909ms.

同期と非同期の根本的な違いがありますが
この結果は、下記のネストループを Core i3 で 5 回実行した平均結果です。
JSON ファイルは、はてなキーワード一覧 から取得した CSV をキーワードだけの JSON に変換したものです。
10MB はキーワードすべて、5MB は半分で切った JSON ファイル、 1MB はそのまた半分です。

Pot.Deferred.forEach によるイテレータは、処理時間が他と比べ長くなっていますが
CPU 最大使用率、CPU 平均使用率 共に安定しているのがわかります。


CPU グラフも比較してみました。
すべてのキーワードを対象に、Pot.Deferred.forEach (上) と、jQuery.each (下) をそれぞれ実行した結果です。

jQuery.each は、途中で「警告:応答しないスクリプト」のダイアログがでてしまいました。
そのため、本来なら Pot.Deferred.forEach のほうが時間がかかると予想してください。

それでも、Pot.Deferred.forEach では jQuery.each に比べ
瞬間的 (もしくは断続的) な負荷を抑えることができています。
これは、対象のデータがどれほど巨大でも
負荷のかからないイテレートが可能なことを示しています。


なお、上記のベンチマークは
ある程度負荷のかかるループ処理 を対象としています。
PotLite.js のイテレータは、各ループがある程度重いと判断すると CPU 負荷を抑えるよう働きます。
つまり、逆に言うと
瞬時に終わるような小中規模のループでは、他のループと変わらない速度で実行されるということです。

以下に、小規模のループ結果を記します。
イテレータCPU 最大使用率CPU 平均使用率実行時間 (ms)
for 文4.10%3.87%9ms.
jQuery.each5.54%4.72%18ms.
Pot.Deferred.forEach4.71%4.15%19ms.
この結果は、1MB の JSON ファイルを対象に ユニーク処理をなくした実行結果です。
上と同じく 5 回実行した結果の平均になります。

PotLite.js は、jQuery.each とほぼ同じ実行速度で、
ちょうど for 文を関数で包んだものを実行したのと同じくらいの速度で実行できています。

このような毎回の処理が小さなループでは、 for 文などが最も適していますが
適度に各関数を使うことでコストも減少させることができます。

以下は、テストの使用したソースコードです。
// jQuery.each
function benchmark_jQuery() {
  $.getJSON('hatena.keywords.json?callback=?', {
    size : '10mb'
  }, function(data) {
    var results = [];
    var start = +new Date;
    $.each(data.keywords, function(k, word) {
      if (!/[^a-zA-Z0-9_.-]/.test(word)) {
        var uniq = true;
        for (var i = 0; i < results.length; i++) {
          if (word === results[i]) {
            uniq = false;
            break;
          }
        }
        if (uniq) {
          results.push(word);
        }
      }
    });
    $('#result').text((+new Date) - start);
  });
}
巨大なファイルの転送と負荷のテストのため、ソースのみ記します。
// Pot.Deferred.forEach
function benchmark_potlite() {
  begin(function() {
    return jsonp('hatena.keywords.json?callback=?', {
      queryString : {
        size : '10mb'
      }
    }).then(function(res) {
      var results = [];
      var start = now();
      return Deferred.forEach(res.keywords, function(word) {
        if (!/[^a-zA-Z0-9_.-]/.test(word)) {
          var uniq = true;
          for (var i = 0; i < results.length; i++) {
            if (word === results[i]) {
              uniq = false;
              break;
            }
          }
          if (uniq) {
            results.push(word);
          }
        }
      }).then(function() {
        return now() - start;
      });
    }).then(function(time) {
      $('#result').text(time);
    });
  });
}
以下は、小規模のループとしてテストしたソースコードです。
// jQuery.each
function benchmark_jQuery_lite() {
  $.getJSON('hatena.keywords.json?callback=?', {
    size : '1mb'
  }, function(data) {
    var results = [];
    var start = +new Date;
    $.each(data.keywords, function(k, word) {
      if (!/[^a-zA-Z0-9_.-]/.test(word)) {
        results.push(word);
      }
    });
    $('#result').text((+new Date) - start);
  });
}
// Pot.Deferred.forEach
function benchmark_potlite_lite() {
  begin(function() {
    return jsonp('hatena.keywords.json?callback=?', {
      queryString : {
        size : '1mb'
      }
    }).then(function(res) {
      var results = [];
      var start = now();
      return Deferred.forEach(res.keywords, function(word) {
        if (!/[^a-zA-Z0-9_.-]/.test(word)) {
          results.push(word);
        }
      }).then(function() {
        return now() - start;
      });
    }).then(function(time) {
      $('#result').text(time);
    });
  });
}

PotLite.js

PotLite.js は、Pot.js の軽量バージョンです。 非同期処理のオブジェクト/関数だけに絞ったライブラリです。 経緯などは Pot.js に関する以前の記事 を参照ください。

概要

PotLite.js は、非ブロックでの非同期処理を直列的に書けるようにし、 UI や CPU への負担を軽減するループ処理を中心に実装された JavaScript ライブラリです。 MochiKit ライクな Deferred オブジェクトにより 様々なイテレート (forEach, filter, map, repeat, some など) を可能とします。 ※ここでいう MochiKit ライクとは、JSDeferred とは違い 1 つのチェインが 1 つのインスタンスということです。 ※Deferred チェイン は JSDeferred や MochiKit.Async.Deferred と同じ感覚で扱えます。

ダウンロード

最新

レポジトリ

$ git clone git://github.com/polygonplanet/Pot.js
GitHub : polygonplanet/Pot.js

動作環境

以下の Web ブラウザで動作確認済みです。
  • Mozilla Firefox *
  • Internet Explorer 6+
  • Safari *
  • Opera *
  • Google Chrome *
また、以下の環境でも動作するよう設計されています。
  • Greasemonkey (userscript)
  • Mozilla Firefox Add-On (on XUL)
  • Node.js
  • Other non-browser environment

インストール

一般的な方法で動作します。
例:
<script src="potlite.min.js" type="text/javascript">
</script>
Node.js の場合。
// Example to define Pot object on Node.js.
var Pot = require('./potlite.min.js');
Pot.debug(Pot.VERSION);

Pot.Deferred.begin(function() {
  Pot.debug('Hello Deferred!');
}).then(function() {
  // ...
});
// ...
Greasemonkey (userscript) の例。
// ==UserScript==
// ...
// @require  https://github.com/polygonplanet/Pot.js/raw/master/potlite.min.js
// ...
// ==/UserScript==
Pot.Deferred.begin(function() {
  return Pot.request('http://www.example.com/data.json').then(function(res) {
    return Pot.parseFromJSON(res.responseText);
  });
}).then(function(res) {
  Pot.debug(res);
  // do something...
});
//...
PotLite.js をバージョンを限定して Web から直接読み込みたい場合、 上の GitHub リンクでは常に最新になってしまうため 実装の差異による不具合が発生するかもしれません。 そのため、API サーバを用意しました。
これは、1.21 の部分をリリース済みのバージョンに合わせて変更できます。 レポジトリに (例えば 1.22 と) バージョンをタグ付けした時に、 あわせて API サーバに置くようにしています。 例えば Greasemonkey で 1.21 を使いたい場合、
// ==UserScript==
// ...
// @require  http://api.polygonpla.net/js/pot/potlite/1.21/potlite.min.js
// ...
// ==/UserScript==
と記述できます。 Greasemonkey に限らず script タグからでもなんでも自由に使ってください。 ----
jQuery プラグインとしての例:
// jQuery を読み込んだ後に実行。
Pot.deferrizejQueryAjax();

// Ajax 系の関数が Pot.Deferred を返すようになる
$.getJSON('/hoge.json').then(function(data) {
  alert(data.results[0].text);
}).rescue(function(err) {
  alert('Error! ' + err);
}).ensure(function() {
  return someNextProcess();
});

// エフェクトなどを Deferred 化する 'deferred' が追加される
$('div#hoge').deferred('hide', 'slow').then(function() {
  // ( hide() が終了したあとの処理)
});
Pot.deferrizejQueryAjax() は現状、 ライブラリ側で実行しません。 なので、プラグインを使用する場合は コードの最初などで実行する必要があります。

リファレンス・マニュアル

より詳しい情報はすべてリファレンスに載っています。 基本的な導入や、各メソッド・関数についても扱っています。

動作テスト

以下のページで動作テストができます。 ページを開くと実装されている主な関数・メソッドを全てテストします。

自動生成されたドキュメント

Closure Compiler によりソースコードから自動生成されたドキュメントです。
殆どの関数ごとにサンプルコードを載せているので、ある程度は参考になると思います。 生成物をすべて確認しているわけではないので、誤認識してる箇所もあるかもしれません。 より詳細な実装などは直接ソースコードを参照ください。

久しぶりにちょっとがんばって重点をまとめてみました。 これを機に、Pot.js (PotLite.js) で遊んでくれると嬉しいです。 不明な点、要望やバグや感想などありましたら @polygon_planet や 下のレポジトリから、またはメールでなんでもどぞです。

レポジトリ