JavaScriptの機能パフォーマンス測定





パフォーマンスは常にソフトウェアで重要な役割を果たしてきました。 また、Webアプリケーションでは、サイトが遅い場合にユーザーが競合他社に簡単にアクセスできるため、その価値はさらに高くなります。 プロのWeb開発者は、これに留意する必要があります。 現在でも、クエリの数を最小限に抑える、CDNを使用する、レンダリングにブロッキングコードを使用しないなど、パフォーマンスを最適化するための多くの古い手法を正常に適用できます。 ただし、JavaScriptを使用する開発者が増えるほど、コードを最適化するタスクが重要になります。



頻繁に使用する機能のパフォーマンスに関して、おそらくいくつかの疑いがあるでしょう。 状況を改善する方法を見つけたのかもしれません。 しかし、パフォーマンスの向上をどのように測定しますか? JavaScriptの関数のパフォーマンスを正確かつ迅速にテストするにはどうすればよいですか? 理想的なオプションは、組み込み関数performance.now()



を使用して、関数の実行前後の時間を測定することです。 ここでは、これがどのように行われるかを見て、いくつかの落とし穴も分析します。



Performance.now()



高解像度時間APIには、 DOMHighResTimeStampオブジェクトを返すnow()



関数があります。 これは、ミリ秒単位の現在の時刻を反映する浮動小数点数で、1000分の1ミリ秒の精度です。 それ自体では、この数値にはほとんど価値がありませんが、2つの測定値の差は、経過した時間を表します。



このツールは組み込みのDate



オブジェクトよりも正確であるという事実に加えて、「単調」でもあります。 簡単な方法の場合:システム時刻の修正の影響を受けません。 つまり、 Date



2つのコピーを作成し、それらの差を計算すると、どれだけ時間が経過したかを正確に表す代表的なアイデアが得られません。



数学の観点から見ると、 単調関数は増加するか、減少するだけです。 理解を深めるための別の例:国内のすべての時計が1時間または1時間先に切り替わる夏または冬時間への切り替え。 Date



の2つのコピーの値(時計の前後)を比較すると、たとえば、「1時間3秒と123ミリ秒」という差が生じます。 そして、 performance.now()



2つのコピーを使用する場合-「3秒123ミリ秒456 789千分の1ミリ秒」。 ここではこのAPIを詳細に分析することはしません;希望する場合は、記事「 Discovering the High Resolution Time API 」を参照してください。



これで、High Resolution Time APIの概要と使用方法がわかりました。 さて、考えられるいくつかのエラーをmakeHash()



ましょうが、最初にmakeHash()



関数を書きましょう。この関数は本文で後で使用します。



 function makeHash(source) { var hash = 0; if (source.length === 0) return hash; for (var i = 0; i < source.length; i++) { var char = source.charCodeAt(i); hash = ((hash<<5)-hash)+char; hash = hash & hash; // Convert to 32bit integer } return hash; }
      
      





このような機能のパフォーマンスは、次の方法で測定できます。



 var t0 = performance.now(); var result = makeHash('Peter'); var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);
      
      





ブラウザでこのコードを実行すると、結果は次のようになります。



 Took 0.2730 milliseconds to generate: 77005292
      
      





デモ: codepen.io/SitePoint/pen/YXmdNJ



間違い#1:不要なものをランダムに測定する



上記の例では、2つのperformance.now()



間でmakeHash()



関数が使用され、その値がresult



変数に割り当てられているresult



気付くかもしれません。 そのため、この関数を実行するのにかかった時間を計算します。 この方法で測定できます:



 var t0 = performance.now(); console.log(makeHash('Peter')); // Bad idea! var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds');
      
      





デモ: codepen.io/SitePoint/pen/PqMXWv



ただし、この場合、 makeHash('Peter')



関数の呼び出しにかかっ時間と、結果をコンソールに送信して表示する時間を測定します。 これらの各操作にかかる時間はわかりませんが、合計時間のみがわかります。 さらに、データとコンソールへの出力の送信速度は、ブラウザーに大きく依存しており、現時点でのその他の操作にも依存しています。 このconsole.log



は予測できないほど遅いと思われるでしょう。 ただし、いずれの場合でも、各機能がI / O操作を意味しない場合でも、複数の機能を実行するとエラーになります。 例:



 var t0 = performance.now(); var name = 'Peter'; var result = makeHash(name.toLowerCase()).toString(); var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);
      
      





繰り返しますが、どの操作に最も時間がかかったかはわかりません。変数に値を割り当て、 toLowerCase()



またはtoString()



呼び出します



間違い2:単一の測定



多くの場合、1回の測定のみを行い、合計時間を合計して、広範囲にわたる結論を導き出します。 ただし、実行速度は次のような要因に大きく依存するため、状況は毎回変わる可能性があります。





したがって、1つの測定ではなく、複数の測定を実行することをお勧めします。



 var t0 = performance.now(); for (var i = 0; i < 10; i++) { makeHash('Peter'); } var t1 = performance.now(); console.log('Took', ((t1 - t0) / 10).toFixed(4), 'milliseconds to generate');
      
      





デモ: codepen.io/SitePoint/pen/Qbezpj



このアプローチのリスクは、ブラウザーベースのJavaScriptエンジンが準最適化を実行できることです。つまり、将来記憶され使用される同じ入力データで関数が2回呼び出されることです。 これを回避するには、同じ値を何度も繰り返す代わりに、多くの異なる入力行を使用できます。 ただし、入力データが異なると、関数の実行速度が時々異なる場合があります。



間違い3:平均の過度の信頼



そのため、関数のパフォーマンスをより正確に評価するには、一連の測定を行うことをお勧めします。 しかし、異なる入力データで異なる速度で実行される場合、関数のパフォーマンスを決定する方法は? 最初に、同じ入力でランタイムを10回実験して測定しましょう。 結果は次のようになります。



 Took 0.2730 milliseconds to generate: 77005292 Took 0.0234 milliseconds to generate: 77005292 Took 0.0200 milliseconds to generate: 77005292 Took 0.0281 milliseconds to generate: 77005292 Took 0.0162 milliseconds to generate: 77005292 Took 0.0245 milliseconds to generate: 77005292 Took 0.0677 milliseconds to generate: 77005292 Took 0.0289 milliseconds to generate: 77005292 Took 0.0240 milliseconds to generate: 77005292 Took 0.0311 milliseconds to generate: 77005292
      
      





最初の値が他の値と異なることに注意してください。 最も可能性が高いのは、その理由が正確に準最適化を実行し、コンパイラを「ウォームアップ」する必要があるためです。 これを回避するためにできることはほとんどありませんが、誤った結論から身を守ることができます。



たとえば、最初の値を除外し、他の9つの算術平均を計算できます。 ただし、すべての結果を取得して中央値を計算する方が適切です。 結果は順番にソートされ、平均が選択されます。 ここでは、何でもできる値を取得できるため、 performance.now()



非常に役立ちます。



それでは、もう一度測定してみましょうが、今回はサンプルの中央値を使用します。



 var numbers = []; for (var i=0; i < 10; i++) { var t0 = performance.now(); makeHash('Peter'); var t1 = performance.now(); numbers.push(t1 - t0); } function median(sequence) { sequence.sort(); // note that direction doesn't matter return sequence[Math.ceil(sequence.length / 2)]; } console.log('Median time', median(numbers).toFixed(4), 'milliseconds');
      
      





間違い4:予測可能な順序で関数を比較する



これで、複数の測定値を取得して平均値を取得することが常に優れていることがわかりました。 さらに、最後の例は、理想的には平均ではなく中央値を取るべきであることを示唆しています。



ランタイム測定は、最速の機能を選択するのに適しています。 同じ入力を使用して同じ結果を生成するが、動作が異なる2つの関数があるとします。 大文字小文字に関係なく、配列内で特定の文字列が見つかった場合にtrueまたはfalseを返す関数を選択する必要があるとしましょう。 この場合、 Array.prototype.indexOf



使用できません。



 function isIn(haystack, needle) { var found = false; haystack.forEach(function(element) { if (element.toLowerCase() === needle.toLowerCase()) { found = true; } }); return found; } console.log(isIn(['a','b','c'], 'B')); // true console.log(isIn(['a','b','c'], 'd')); // false
      
      





haystack.forEach



ループは、一致するものがすぐに見つかったとしても、すべての要素をhaystack.forEach



するため、このコードを改善できます。 古き良きを使用してみましょう:



 function isIn(haystack, needle) { for (var i = 0, len = haystack.length; i < len; i++) { if (haystack[i].toLowerCase() === needle.toLowerCase()) { return true; } } return false; } console.log(isIn(['a','b','c'], 'B')); // true console.log(isIn(['a','b','c'], 'd')); // false
      
      





では、どのオプションが速いか見てみましょう。 各機能を10回実行し、「正しい」結果を計算します。



 function isIn1(haystack, needle) { var found = false; haystack.forEach(function(element) { if (element.toLowerCase() === needle.toLowerCase()) { found = true; } }); return found; } function isIn2(haystack, needle) { for (var i = 0, len = haystack.length; i < len; i++) { if (haystack[i].toLowerCase() === needle.toLowerCase()) { return true; } } return false; } console.log(isIn1(['a','b','c'], 'B')); // true console.log(isIn1(['a','b','c'], 'd')); // false console.log(isIn2(['a','b','c'], 'B')); // true console.log(isIn2(['a','b','c'], 'd')); // false function median(sequence) { sequence.sort(); // note that direction doesn't matter return sequence[Math.ceil(sequence.length / 2)]; } function measureFunction(func) { var letters = 'a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z'.split(','); var numbers = []; for (var i = 0; i < letters.length; i++) { var t0 = performance.now(); func(letters, letters[i]); var t1 = performance.now(); numbers.push(t1 - t0); } console.log(func.name, 'took', median(numbers).toFixed(4)); } measureFunction(isIn1); measureFunction(isIn2);
      
      





次の結果が得られます。



 true false true false isIn1 took 0.0050 isIn2 took 0.0150
      
      





デモ: codepen.io/SitePoint/pen/YXmdZJ



これを理解する方法は? 最初の機能は3倍高速でした。 ありえない! 説明は簡単ですが、明らかではありません。 haystack.forEach



を使用する最初の関数は、ブラウザーベースのJSエンジンのレベルでの低レベルの最適化の恩恵を受けます。これは、配列インデックスを使用して行われません。 だからあなたが測定するまで、あなたは知りません!



結論



performance.now()



を使用してJavaScriptでパフォーマンス測定の精度を実証しようとすると、直観が失敗する可能性があることがわかりました。経験的データは仮定とまったく一致しませんでした。 高速なWebアプリケーションを作成する場合は、JSコードを最適化する必要があります。 また、コンピューターは実際には生き物であるため、予測不能で驚かされる可能性があります。 したがって、コードを高速化する最良の方法は、測定と比較です。



どのオプションがより高速になるかを事前に知ることができないもう1つの理由は、それがすべて状況に依存していることです。 最後の例では、大文字と小文字に関係なく、26個の値の一致を検索しました。 しかし、100,000個の値の中から検索すると、関数の選択が異なる場合があります。



考えられるエラーは、考えられる唯一のエラーではありません。 たとえば、非現実的なシナリオを測定したり、1つのJSエンジンのみを測定したりするなど、それらに追加できます。 ただし、重要なことを覚えておくことは重要です。高速なWebアプリケーションを作成する場合、 performance.now()



よりも優れたツールは見つかりません。 ただし、ランタイムの測定は1つの側面にすぎません。 パフォーマンスは、メモリ使用量とコードの複雑さの影響も受けます。



All Articles