« 『JavaScript:The Good Parts』にツッコミ | メイン | 「第6回まっちゃ445勉強会」 に参加してきました »

JavaScriptでconcatはもう使うべきではないのかもしれない

注意!
当エントリーは多くの誤りを含んでいます。参考にされる場合は最後の追記部分まで含めて読まれるようにお願いします。
それなりに慣れているはずのプロのプログラマでも、このような勘違いや大失態をすることがあるという教訓として残すために、エントリーの削除や修正はせずに追記のみに留めておきます。
JavaScriptで、配列に要素を追加するメソッドに、push、unshift、splice、concatがある。このうち、配列の後方に要素を追加するのは、(要素を好きな場所に追加可能なspliceを除くと)pushとconcatの二つである。この二つのメソッドは破壊的/非破壊的の違いがあれ、似たような挙動を示す。
文字列をpush
var ary=["a","b","c"];
var result=ary.push("d");

//破壊的に要素を追加
//  ary     = ["a","b","c","d"]
//  result  = 4
文字列をconcat
var ary=["a","b","c"];
var result=ary.concat("d");

//非破壊的に要素を追加しその配列を返す
//  ary     = ["a", "b", "c"]
//  result  = ["a", "b", "c", "d"]
しかし、配列を追加する際はこのようにはならない。
配列をpush
var ary=["a","b","c"];
var result=ary.push(["d","e","f"]);

//要素を追加する
//  ary     = ["a","b","c",["d","e","f"]]
//  result  = 4
配列をconcat
var ary=["a","b","c"];
var result=ary.concat(["d","e","f"]);

//配列を連結する
//  ary     = ["a","b","c"]
//  result  = ["a","b","c","d","e","f"]
pushは「要素を追加する」メソッドだが、concatは「配列を連結する」メソッドである。concatは引数が配列でなかった場合、引数を配列に内包したと見なし連結するようだ。そして、pushやconcatは複数の引数をとりうる。
複数の引数をpush
var ary=["a","b","c"];
var result=ary.push("d","e","f");

//  ary     = ["a","b","c","d","e","f"]
//  result  = 6
複数の引数をconcat
var ary=["a","b","c"];
var result=ary.concat("d","e","f");

//  ary     = ["a","b","c"]
//  result  = ["a","b","c","d","e","f"]
pushが複数の引数を指定できることにより、pushの換わりにconcat、concatの換わりにpushを使うことができるようになる。
pushとpushに似せたconcat
var a=["a","b","c"];
var b=["d","e","f"];
a.push(b);
//  a       = ["a","b","c",["d","e","f"]]

var a=["a","b","c"];
var b=["d","e","f"];
a=a.concat([b]);
//  a       = ["a","b","c",["d","e","f"]]
concatとconcatに似せたpush
var a=["a","b","c"];
var b=["d","e","f"];
a=a.concat(b);
//  a       = ["a","b","c","d","e","f"]

var a=["a","b","c"];
var b=["d","e","f"];
Array.prototype.push.apply(a,b);
//  a       = ["a","b","c","d","e","f"]
では、このふたつのメソッドの用途の違いは破壊的かどうかだけなのだろうか。実はそうではない。破壊的か非破壊的かの相違は、値の取得方法が違う。concatを用いる場合は値取得のために代入が行なわれており、内部的に新たに変数を生成している。このため実行時間が大幅に違う。Firebug上で3000回実行したときの時間を計測してみた。(これ以上実行回数を増やすとconcatでは測定不能になるため、少々中途半端な回数となっている。)
push
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
a.push(b);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 4(ms)
pushに似せたconcat
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
a=a.concat([b]);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 764(ms)
concat
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
a=a.concat(b);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 751(ms)
concatに似せたpush
var a=["a","b","c"];
var b=["d","e","f"];
var start=new Date();
for(var i=0;i<3000;i++){
Array.prototype.push.apply(a,b);
}
var end=new Date();
end.getTime()-start.getTime(); //=> 8(ms)
圧倒的にpushを用いた方が速い。それだけではない。破壊的な処理を避けるために、concatを用いる場面があると思うかもしれない。しかし、処理を工夫すればpushでも非破壊のような処理が可能である。
concatを用いた処理と、pushで元の配列を破壊させない処理
var a=["a","b","c"];
var b=["d","e","f"];
var c=a.concat(b);
//  a       = ["a","b","c"]
//  b       = ["d","e","f"]
//  c       = ["a","b","c","d","e","f"]

var a=["a","b","c"];
var b=["d","e","f"];
var c=[];
Array.prototype.push.apply(c,a);
Array.prototype.push.apply(c,b);
//  a       = ["a","b","c"]
//  b       = ["d","e","f"]
//  c       = ["a","b","c","d","e","f"]
空の配列を準備し、空配列へ破壊的に連結している部分がこの処理方法の肝だ。もはやconcatの面目は丸潰れだ。実行回数が明らかに少ない場合を除いて、concatは利点よりも欠点のほうが際立ってしまっている。concatを使うべき場面は、私にはもう思いつかない。
2009/04/25 追記
当エントリーについて色々なところから反論を頂きました。まず、エントリーに抜けがあったため、言いたかったことが正確に伝わっていませんでした。そして、根本的に思考から抜け落ちていたことがいくつかありました。上記二点の理由のために、反論も様々な角度から為されました。
そもそもこのエントリーは大きなサイズの配列を組み立てることを前提に書いていました。次のような処理です。
var result=[];
for(var i=0,len=ary.length;i<len;i++){
    result=result.concat(f(ary[i]));
}
関数fは受け取った値から配列を生成する処理をしています。このような処理の場合、当エントリーの説明のようにpushを使用したほうがはやくなります。
var result=[];
for(var i=0,len=ary.length;i<len;i++){
    Array.prototype.push.apply(result,f(ary[i]));
}
抜けていた説明とは、「配列連結の処理を非常に多く行なう場合」ははやくなるという部分です。ただし、これでも誤りがあります。次のコードはconcatを利用していますが、上記のコードとは変数への代入を行なっていない点が異なります。
var result=[];
for(var i=0,len=ary.length;i<len;i++){
    result.concat(f(ary[i]));
}
この処理も高速です。つまり、pushとconcatの実行速度にはさほど違いはありません。私の環境では40%程pushのほうが速いのですが、10万回実行してやっと100ms違う程度です。つまり、速度に関係しているのはpushなのかconcatではなく、変数への代入部分です。単純にこのような変数への代入を伴うような処理にはconcatは向かないというだけです。状況を限定しているにも関わらずこの説明を充分にせず、この結果を持ってconcatがpushよりも劣っているかのような記述をしていました。
思考から抜け落ちていたのはこれだけに留まりません。そもそも実行処理は環境に大きく依存します。ブラウザの違いだけでなく、バージョンの違いによっても、実行速度は違ってきます。これは、今まさに起こっているブラウザの速度競争で、各々のブラウザの得意分野での速度比較を持ち出し、各々のブラウザが最速を宣言しているように、どちらがはやいかは環境に依存する可能性があります。そして、どのようなコンポーネント・機能拡張・プラグインやアドオンを導入しているかによっても違ってくるでしょう。Firefoxのアドオンでも思いと評価されているFirebug上のみで速度の検証を行なって、その結果を一般化して主張していました。
そして最後に、私が思いつくだけでもこれだけの不確定条件があるにも関わらず、断定してしまっていたことです。これが一番の問題でした。
以上

トラックバック

このエントリーのトラックバックURL:
http://www.kanasansoft.com/cgi/mt/mt-tb.cgi/223

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)

Google