『転』JS最適化の原則
http://www.iteye.com/topic/126859
まず,他の言語と異なり,JSの効率はJS engineの効率に大きく依存する.エンジンの実現の優劣に加えて、エンジン自身もいくつかの特殊なコードモードのためにいくつかの最適化の策略を採用します.例えばFF,Opera,SafariのJSエンジンは,文字列の綴り演算(+)を特に最適化している.明らかに、最大の効率を得るには、エンジンの気性を理解し、できるだけエンジンの味に迎合しなければならない.したがって、異なるエンジンに対して、最適化は逆行する可能性が高い.
ブラウザにまたがるwebプログラミングをすると、最大の問題はIE 6(Jscript 5.6)です!hotfixを打たない場合、Jscriptエンジンのゴミ回収のバグは、実際のアプリケーションでのperformanceと他のブラウザが1桁も存在しないためです.そのため、このような場合に最適化するのは、実際にはJscriptのために最適化することです!
だから
第一原則はIE 6(パッチを適用していないJscript 5.6以降)を最適化するだけです!
もしあなたのプログラムがIE 6で許容できる性能に最適化されているならば、基本的に他のブラウザで性能は全く問題ありません.
そのため、私が以下に述べる多くの問題は他のエンジンでは全く異なる可能性があることに注意してください.例えば、ループで文字列接合を行うには、Array.joinの方法が必要だと一般的に考えられていますが、SpiderMonkeyなどのエンジンが文字列の「+」演算を最適化したため、結果的にArray.joinを使用する効率はむしろ「+」を直接使用するほうがいいです.しかし、IE 6を考えると、他のブラウザでのこのような効率の違いは言うまでもありません.
JS最適化は他言語の最適化と同様である.例えば、いきなり怒鳴って最適化しないでください.それは意味がありません.最適化の鍵は、依然として最も重要な場所、つまりボトルネックに集中することです.一般的には、
ボトルネックは常に大規模な循環の場所に現れている.これは、ループ自体にパフォーマンスの問題があるというわけではありませんが、ループが急速に拡大する可能性のあるパフォーマンスの問題です.
だから
第2の原則は大規模な循環体を最も主要な最適化対象とすることである.
以下の最適化の原則は,大規模なサイクルでのみ意味があり,サイクル体以外でこのような最適化を行うことは基本的に意味がない.
現在のJSエンジンのほとんどは解釈実行されているが、解釈実行の場合、すべての操作において、
関数呼び出しの効率は低い.また、
深すぎるprototype継承チェーンやマルチレベルリファレンスも効率を低下させます.Jscriptでは,10級参照のオーバーヘッドはほぼ1回の空関数呼び出しのオーバーヘッドの1/2である.両方のオーバーヘッドは、単純な操作(四則演算など)よりもはるかに大きい.
だから
第3の原則は、過度な参照レベルと不要な複数のメソッド呼び出しをできるだけ避けることである.
特に注意したいのは、
プロパティ・アクセスのように見えますが、実際にはメソッド・コールです.たとえばすべてのDOMの属性は,実際にはメソッドである.NodeListを巡回する場合、ループ条件はnodes.lengthへのアクセスであり、属性読み出しのように見え、実際には関数呼び出しに等価である.またIE DOMの実装ではchildNodes.lengthは毎回内部遍歴により再カウントされる.(My godですが、これは本当です!childNodes.lengthのアクセス時間がchildNodes.lengthの値に比例すると測定したので!)これは非常に費用がかかります.だからnodes.lengthをjs変数に保存しておけば、もちろん遍歴の性能を高めることができます.
同様に関数呼び出しであり、ユーザー定義関数の効率は言語内構築関数よりもはるかに低い.後者はエンジンローカルメソッドのパッケージであり、エンジンは通常c,c++,javaで書かれているからである.さらに、同じ機能では、言語の組み込み構造のオーバーヘッドは、通常、JSコードのparseフェーズで前者が決定および最適化できるため、組み込み関数呼び出しよりも効率的である.
だから
第四の原則は、言語自体の構造と内蔵関数をできるだけ使用することである.
ここでは、高性能のString.formatメソッドの一例を示します.String.formatの従来の実装方式はString.replace(regex,func)であり、patternがn個のプレースホルダ(重複を含む)を含む場合、カスタム関数funcはn回呼び出される.この高性能実装では、format呼び出しのたびにArray.joinが1回だけ行われ、String.replace(regex,string)の操作が1回行われます.どちらもカスタム関数呼び出しではなくエンジン内蔵方法です.組み込みメソッド呼び出しとn回のカスタムメソッド呼び出しの2回がパフォーマンスの違いです.
同じ内装特性でも性能に差があります.たとえばJscriptではargumentsへのアクセス性能が悪く,関数呼び出しにほぼ間に合った.したがって、可変パラメータの単純な関数がパフォーマンスのボトルネックになった場合、argumentsにアクセスするのではなく、パラメータの明示的な判断で処理する内部を変更することができます.
例:
コード#コード#
このsumは通常呼び出される回数が少なく,パラメータが少ない場合の性能を改善したい.変更後:
コード#コード#
実際にはあまり向上しませんが、次のように変更します.
コード#コード#
かなり向上します(少なくとも1倍速くなります).
最後は
第5の原則は、実際のアプリケーションで最も重要なパフォーマンス障害であり、不要なオブジェクトの作成を最小限に抑えることです.
オブジェクト自体を作成するには一定の代価がありますが、この代価は大きくありません.最も根本的な問題はJscriptの愚かな極みのゴミ回収スケジューリングアルゴリズムによるもので、対象個数の増加に伴い性能が著しく低下している(マイクロソフトの人自身によると複雑度はO(n^2)).
たとえば、よくある文字列の結合の問題ですが、私のテストの検証を経て、単純に文字列オブジェクトを複数回作成するのは、パフォーマンスが悪い理由ではありません.残念なことに、オブジェクトの作成中に無駄なゴミ回収のオーバーヘッドが発生します.Array.joinの方法では、中間文字列オブジェクトは作成されません.そのため、ゴミ回収のオーバーヘッドが減少します.
したがって、大規模なオブジェクト作成を単一の文に変換できれば、パフォーマンスが大幅に向上します.例えば、コードを構築してevalを構築することによって、実際にPIESプロジェクトではこの考えに基づいて専門的な大規模なオブジェクト発生器を作っています......
上は私がまとめたJS最適化の5つの原則です.
これらの原則のほかに、DOMの遍歴など、いくつかの特殊な状況があり、後で議論する時間があります.
まず,他の言語と異なり,JSの効率はJS engineの効率に大きく依存する.エンジンの実現の優劣に加えて、エンジン自身もいくつかの特殊なコードモードのためにいくつかの最適化の策略を採用します.例えばFF,Opera,SafariのJSエンジンは,文字列の綴り演算(+)を特に最適化している.明らかに、最大の効率を得るには、エンジンの気性を理解し、できるだけエンジンの味に迎合しなければならない.したがって、異なるエンジンに対して、最適化は逆行する可能性が高い.
ブラウザにまたがるwebプログラミングをすると、最大の問題はIE 6(Jscript 5.6)です!hotfixを打たない場合、Jscriptエンジンのゴミ回収のバグは、実際のアプリケーションでのperformanceと他のブラウザが1桁も存在しないためです.そのため、このような場合に最適化するのは、実際にはJscriptのために最適化することです!
だから
第一原則はIE 6(パッチを適用していないJscript 5.6以降)を最適化するだけです!
もしあなたのプログラムがIE 6で許容できる性能に最適化されているならば、基本的に他のブラウザで性能は全く問題ありません.
そのため、私が以下に述べる多くの問題は他のエンジンでは全く異なる可能性があることに注意してください.例えば、ループで文字列接合を行うには、Array.joinの方法が必要だと一般的に考えられていますが、SpiderMonkeyなどのエンジンが文字列の「+」演算を最適化したため、結果的にArray.joinを使用する効率はむしろ「+」を直接使用するほうがいいです.しかし、IE 6を考えると、他のブラウザでのこのような効率の違いは言うまでもありません.
JS最適化は他言語の最適化と同様である.例えば、いきなり怒鳴って最適化しないでください.それは意味がありません.最適化の鍵は、依然として最も重要な場所、つまりボトルネックに集中することです.一般的には、
ボトルネックは常に大規模な循環の場所に現れている.これは、ループ自体にパフォーマンスの問題があるというわけではありませんが、ループが急速に拡大する可能性のあるパフォーマンスの問題です.
だから
第2の原則は大規模な循環体を最も主要な最適化対象とすることである.
以下の最適化の原則は,大規模なサイクルでのみ意味があり,サイクル体以外でこのような最適化を行うことは基本的に意味がない.
現在のJSエンジンのほとんどは解釈実行されているが、解釈実行の場合、すべての操作において、
関数呼び出しの効率は低い.また、
深すぎるprototype継承チェーンやマルチレベルリファレンスも効率を低下させます.Jscriptでは,10級参照のオーバーヘッドはほぼ1回の空関数呼び出しのオーバーヘッドの1/2である.両方のオーバーヘッドは、単純な操作(四則演算など)よりもはるかに大きい.
だから
第3の原則は、過度な参照レベルと不要な複数のメソッド呼び出しをできるだけ避けることである.
特に注意したいのは、
プロパティ・アクセスのように見えますが、実際にはメソッド・コールです.たとえばすべてのDOMの属性は,実際にはメソッドである.NodeListを巡回する場合、ループ条件はnodes.lengthへのアクセスであり、属性読み出しのように見え、実際には関数呼び出しに等価である.またIE DOMの実装ではchildNodes.lengthは毎回内部遍歴により再カウントされる.(My godですが、これは本当です!childNodes.lengthのアクセス時間がchildNodes.lengthの値に比例すると測定したので!)これは非常に費用がかかります.だからnodes.lengthをjs変数に保存しておけば、もちろん遍歴の性能を高めることができます.
同様に関数呼び出しであり、ユーザー定義関数の効率は言語内構築関数よりもはるかに低い.後者はエンジンローカルメソッドのパッケージであり、エンジンは通常c,c++,javaで書かれているからである.さらに、同じ機能では、言語の組み込み構造のオーバーヘッドは、通常、JSコードのparseフェーズで前者が決定および最適化できるため、組み込み関数呼び出しよりも効率的である.
だから
第四の原則は、言語自体の構造と内蔵関数をできるだけ使用することである.
ここでは、高性能のString.formatメソッドの一例を示します.String.formatの従来の実装方式はString.replace(regex,func)であり、patternがn個のプレースホルダ(重複を含む)を含む場合、カスタム関数funcはn回呼び出される.この高性能実装では、format呼び出しのたびにArray.joinが1回だけ行われ、String.replace(regex,string)の操作が1回行われます.どちらもカスタム関数呼び出しではなくエンジン内蔵方法です.組み込みメソッド呼び出しとn回のカスタムメソッド呼び出しの2回がパフォーマンスの違いです.
同じ内装特性でも性能に差があります.たとえばJscriptではargumentsへのアクセス性能が悪く,関数呼び出しにほぼ間に合った.したがって、可変パラメータの単純な関数がパフォーマンスのボトルネックになった場合、argumentsにアクセスするのではなく、パラメータの明示的な判断で処理する内部を変更することができます.
例:
コード#コード#
function sum() {
var r = 0;
for (var i = 0; i < arguments.length; i++) {
r += arguments[i];
}
return r;
}
このsumは通常呼び出される回数が少なく,パラメータが少ない場合の性能を改善したい.変更後:
コード#コード#
function sum() {
switch (arguments.length) {
case 1: return arguments[0];
case 2: return arguments[0] + arguments[1];
case 3: return arguments[0] + arguments[1] + arguments[2];
case 4: return arguments[0] + arguments[1] + arguments[2] + arguments[3];
default:
var r = 0;
for (var i = 0; i < arguments.length; i++) {
r += arguments[i];
}
return r;
}
}
実際にはあまり向上しませんが、次のように変更します.
コード#コード#
function sum(a, b, c, d, e, f, g) {
var r = a ? b ? c ? d ? e ? f
? a + b + c + d + e + f : a + b + c + d +
e : a + b + c + d : a + b + c : a + b : a : 0;
if (g === undefined) return r;
for (var i = 6; i < arguments.length; i++) {
r += arguments[i];
}
return r;
}
かなり向上します(少なくとも1倍速くなります).
最後は
第5の原則は、実際のアプリケーションで最も重要なパフォーマンス障害であり、不要なオブジェクトの作成を最小限に抑えることです.
オブジェクト自体を作成するには一定の代価がありますが、この代価は大きくありません.最も根本的な問題はJscriptの愚かな極みのゴミ回収スケジューリングアルゴリズムによるもので、対象個数の増加に伴い性能が著しく低下している(マイクロソフトの人自身によると複雑度はO(n^2)).
たとえば、よくある文字列の結合の問題ですが、私のテストの検証を経て、単純に文字列オブジェクトを複数回作成するのは、パフォーマンスが悪い理由ではありません.残念なことに、オブジェクトの作成中に無駄なゴミ回収のオーバーヘッドが発生します.Array.joinの方法では、中間文字列オブジェクトは作成されません.そのため、ゴミ回収のオーバーヘッドが減少します.
したがって、大規模なオブジェクト作成を単一の文に変換できれば、パフォーマンスが大幅に向上します.例えば、コードを構築してevalを構築することによって、実際にPIESプロジェクトではこの考えに基づいて専門的な大規模なオブジェクト発生器を作っています......
上は私がまとめたJS最適化の5つの原則です.
これらの原則のほかに、DOMの遍歴など、いくつかの特殊な状況があり、後で議論する時間があります.