forEach色々とベンチマーク

cho45さんがCodeReposにあげていたJSEnumeratorに付随していたベンチマークでちょっと遊んでみました。
肝心なことを書き忘れていた。はっきりと差が出ているのは、10回のループをさらに1000回ループさせているからで、大抵の場合はどのメソッドを使っても体感できるほどの差は出ないと思います。
jQueryprototype.jsMochiKit、fLDR、JSEnumeratorなどで使われているforEach関数の速度比較です。recursive eachは一応オリジナルです。
以下、実験ページ
http://ss-o.net/jsenumerator/benchmark/10.html
http://ss-o.net/jsenumerator/benchmark/100.html

結果

とりあえずWindowsのみmacは後で面倒だか、じゃなくて、windowsと大差ないみたいなので省略
まだ集計途中なので、ほぼそのまま載せます。簡単に集計

100*1000 loop WebKit-r30881(Win) Firefox3 beta4 Opera 9.50 beta(9807) IE7 いい加減な偏差値
for 17.099672 9.6295617 21.898841 51.4961 64.54258137
fLDR(mala) each 34.399672 42.999289 49.998841 374.99052 57.7347206
recursive each (ベタ書き) 60.899672 57.299289 46.798841 343.99052 55.62598365
MochiKit forEach 53.099672 71.399289 114.098841 405.99052 53.31218988
prototype.js Array#each 62.499672 108.996562 99.998841 749.99052 47.54804145
recursive each 109.999161 114.997037 81.199344 718.98898 44.78568234
jQuery each 60.899672 150.996562 342.990381 593.99052 41.78387405
JSEnumerator each 139.999161 161.996996 296.990381 811.99058 34.66692666
ブラウザごとの平均 67.36204425 89.78932321 131.7467889 506.4285325

集計前のデータ:http://ss-o.net/jsenumerator/benchmark/jseach_data.tsv

ブラウザ対決では、WebKit Nightlyが期待通りの最速。Firefox2がFirefox3より速くて、ちょっと微妙。体感的にはFirefox3のほうが速いので、どこかおかしいのかも。拡張はどちらもGreasemonkey+Firebugくらいしか入ってない(Fx3のほうが少ない)んだけどなぁ。(Opera9.26はばらつきが大きくてあまり当てにならない、9.5は安定してやっぱり9.5もあんまり安定してないけど高速だった)

関数対決は、malaさんのforeach関数(コメント欄でid:malaさんに元ネタを教えていただきました。元記事は消えてますが、id:brazilさんのブクマコメに概要がありました)が最速(上に載せた一覧は短いループなのでrecursiveのほうが良さそうですが、100のほうだと明らかにmalaさんのが高速です)。これはレベルが違います。オープンソースfastladderから、
http://fastladder.googlecode.com/svn/trunk/fastladder/public/js/common.js の 240行目より、

function foreach(a,f){
	var c = 0;
	var len = a.length;
	var i = len % 8;
	if (i>0) do {
		f(a[c],c++,a);
	} while (--i);
	i = parseInt(len >> 3);
	if (i>0) do {
		f(a[c],c++,a);f(a[c],c++,a);
		f(a[c],c++,a);f(a[c],c++,a);
		f(a[c],c++,a);f(a[c],c++,a);
		f(a[c],c++,a);f(a[c],c++,a);
	} while (--i);
};

これ、始めて見たときは笑いました。まず、8で割った余りの端数をwhileループで片付け、残った値を8個ずつ一気に処理しています。(len >> 3はlenを8(2^3)で割ったときの整数を返しています)

ちなみに、オリジナルの再帰処理ループもなかなか健闘してくれました。再帰なので巨大な配列には使えないですが、なんといってもライブラリなど一切不要、数行で書ける手軽さが強みです。再帰呼び出しにsetTimeoutを挟んであげれば一瞬でサクサクなアニメーションを書くこともできます。

,"recursive each" : function () {
	var list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
	for (var i = 0; i < LOOP; i++) {
		var j=0,l=list.length;
		var f = function(a,j) {
			a;
			j;
			if (++j < l) f(list[j],j);
		};
		f(list[j],j);
	}
}

べた書きなのはチートくさいかなと思い、関数を用意したパターンも試してみました。

function recursive_each(a,f){
	var l=a.length;
	var _f = function(v,j) {
		f(v,j,a);
		if (++j < l) _f(a[j],j);
	};
	_f(a[0],0);
};

関数呼び出しが倍増したので、速度もほぼ2倍に!! 普通のループですね。

この機会に各ライブラリの関数を比べてみたわけですが、各each関数が似ているようでかなり違うことに気が付きます。
jQueryは第一引数に配列のインデックスが入ってきます。これはjQueryだけの特徴で、知らないと嵌ります(すぐ気が付くけど)。MochiKit(とJSEnumerator)は配列のインデックスを渡してくれません。なので、カウントアップしたいときは外側にインデックス用の変数を用意する必要があるみたいです(これは私が知らないだけで、別の解決策があるのかも)。

id:cho45

  • "for"だけループが少ない(他のは2重ループだが、forだけ1重でちーとしてる)
  • jQueryのデータ配列がE()になっていて、配列ではなくJSEnumeratorオブジェクトになっていた

のが気になりました。
細かいところでは、jQueryのeach関数の第一引数は配列のインデックスで、他のeach関数は配列の中身なので微妙に条件が違う点も少し気になったかも。