変数を何度も作成しなければならないのに、なぜ後者の関数は10%高速なのですか?


14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

そして、より高速な機能:(同じ変数kb / mb / gbを何度も繰り返し計算する必要があることに注意してください)。パフォーマンスはどこで得られますか?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};

3
静的に型付けされた言語では、「変数」は定数としてコンパイルされます。たぶん、現代のJSエンジンは同じ最適化を行うことができます。変数がクロージャーの一部である場合、これは機能しないようです。
usr

6
これは、使用しているJavaScriptエンジンの実装の詳細です。理論上の時間と空間は同じです。これらを変えるのは、特定のJavaScriptエンジンの実装だけです。したがって、質問に正しく答えるためには、これらを測定した特定のJavaScriptエンジンをリストする必要があります。おそらく、誰かがその実装の詳細を知っているので、あるものを他のものよりも最適にした理由/理由を言うことができます。また、測定コードを投稿する必要があります。
ジミーホファ

定数値を参照するために「計算」という言葉を使用します。あなたが参照しているものには、実際に計算するものは何もありません。定数値の演算は、コンパイラーが行う最も単純で明白な最適化の1つです。そのため、定数値のみを含む式が表示された場合は、式全体が単一の定数値に最適化されていると見なすことができます。
ジミーホファ

@JimmyHoffaはtrueですが、関数呼び出しごとに3つの定数変数を作成する必要があります...
トミー

@Tomy定数は変数ではありません。それらは変化しないため、コンパイル後に再作成する必要はありません。通常、定数はメモリに配置され、その定数の将来の到達範囲はまったく同じ場所に向けられます。値は変化しないため、再作成する必要はありません。したがって、変数ではありません。コンパイラは通常、定数を作成するコードを発行せず、コンパイラが作成を行い、作成したものへのすべてのコード参照を指示します。
ジミーホファ

回答:


23

最新のJavaScriptエンジンはすべてジャストインタイムコンパイルを行います。「何度も何度も作成しなければならない」ことについて、推測することはできません。どちらの場合でも、この種の計算は比較的簡単に最適化できます。

一方、定数変数のクローズは、JITコンパイルの対象となる典型的なケースではありません。通常、異なる呼び出しでこれらの変数を変更できるようにする場合は、クロージャーを作成します。また、OOPでのメンバー変数とローカルintへのアクセスの違いのように、これらの変数にアクセスするための追加のポインター逆参照を作成しています。

このような状況が、人々が「時期尚早な最適化」ラインを捨てる理由です。簡単な最適化は、コンパイラーによって既に行われています。


あなたが言及しているように、損失を引き起こしているのは、変数解決のためのスコープトラバーサルだと思います。合理的なようだが、誰が本当に... JavaScriptのJITエンジンでどのような狂気の嘘を知っている
ジミー・ホッファ

1
この回答の拡張の可能性:JITがオフラインコンパイラーにとって簡単な最適化を無視する理由は、コンパイラー全体のパフォーマンスが異常な場合よりも重要だからです。
ルーシェンコ

12

変数は安価です。実行コンテキストとスコープチェーンは高価です。

本質的に「クロージャのため」に要約されるさまざまな答えがあり、それらは本質的に真実ですが、問題はクロージャに特にあるのではなく、異なるスコープの変数を参照する関数があるという事実です。これらがwindowIIFE内のローカル変数ではなく、オブジェクトのグローバル変数である場合、同じ問題が発生します。試してみてください。

したがって、最初の関数では、エンジンが次のステートメントを見ると:

var gbSize = size / GB;

次の手順を実行する必要があります。

  1. size現在のスコープで変数を検索します。(それを見つけた。)
  2. GB現在のスコープで変数を検索します。(見つかりません。)
  3. GB親スコープで変数を検索します。(それを見つけた。)
  4. 計算を行い、に割り当てgbSizeます。

ステップ3は、単に変数を割り当てるよりもかなり高価です。また、あなたはこれを行うに5回の両方に二回を含め、GBおよびMB。関数の先頭でこれらvar gb = GBのエイリアスを作成し(例:)、代わりにエイリアスを参照すると、実際にはわずかな高速化が得られますが、一部のJSエンジンはすでにこの最適化を実行している可能性もあります。そしてもちろん、実行を高速化する最も効果的な方法は、単にスコープチェーンをまったく走査しないことです。

JavaScriptは、コンパイラがコンパイル時にこれらの変数アドレスを解決する、コンパイル済みの静的に型付けされた言語とは異なることに注意してください。JSエンジンはそれらを名前で解決する必要あり、これらの検索は実行時に毎回行われます。したがって、可能な場合はそれらを避けたいと思います。

JavaScriptでは、変数の割り当ては非常に安価です。私はその声明をバックアップするものは何もありませんが、実際には最も安価な操作かもしれません。それにもかかわらず、変数の作成を回避しようとすることはほとんど決して良い考えではないと言うのは安全です。その領域で実行しようとするほとんどすべての最適化は、実際にはパフォーマンス面で事態を悪化させることになります。


「最適化」は、パフォーマンスに悪影響を与えていない場合でも、ほぼ確実にされる負のコードの読みやすさに影響を与えることだろう。クレイジーな計算処理を行わない限り、ほとんどの場合、これは悪いトレードオフです(明らかに、パーマリンクアンカーはありません。「2009-02-17 11:41」で検索してください)。まとめとして、「速度が絶対に必要でない場合は、速度よりも明瞭さを選択します。」
CVn

動的言語用の非常に基本的なインタープリターを作成する場合でも、実行時の変数アクセスはO(1)操作になる傾向があり、O(n)スコープトラバーサルは初期コンパイルでも必要ありません。各スコープでは、新しく宣言された各変数に番号が割り当てられるため、としてvar a, b, cアクセスできます。すべてのスコープには番号が付けられ、このスコープが5つのスコープの深さでネストされている場合、解析中に認識されるスコープによって完全にアドレス指定されます。ネイティブコードでは、スコープはスタックセグメントに対応します。クロージャはより多くの彼らは、バックアップと置き換える必要がありますので、複雑さbscope[1]benv[5][1]env
アモン

@amon:それはどのようにしたい理想的であるかもしれないようなこと仕事に、それが実際にどのように動作するかそうではありません。私がこれについて本を書いたよりもはるかに知識が豊富で経験豊富な人々。特に、Nicholas C. Zakasによる高性能JavaScriptを紹介します。ここにスニペットがあり、彼はそれをバックアップするためのベンチマークについても話しました。もちろん、彼だけではなく、最も有名な人物でもあります。JavaScriptにはレキシカルスコープがあるため、実際にはクロージャーはそれほど特別ではありません。本質的にはすべてがクロージャーです。
アーロンノート

@Aaronaught面白い。その本は5年前なので、現在のJSエンジンがどのように変数検索を処理するか興味があり、V8エンジンのx64バックエンドを調べました。静的分析中、ほとんどの変数は静的に解決され、スコープ内のメモリオフセットが割り当てられます。関数スコープはリンクリストとして表され、アセンブリは展開されたループとして出力され、正しいスコープに到達します。ここでは*(scope->outer + variable_offset)、アクセス用のCコードに相当するものを取得します。追加の関数スコープレベルごとに、1つの追加のポインター逆参照がかかります。私たちは両方正しかったようです:)
アモン

2

1つの例にはクロージャーが含まれ、もう1つの例には含まれません。クロージャーの実装は、トリッキーです。変数上でクローズされると、通常の変数のようには機能しません。これは、Cのような低レベル言語ではより明白ですが、JavaScriptを使用してこれを説明します。

クロージャは、関数だけでなく、それが閉じたすべての変数で構成されます。その関数を呼び出したいときは、すべての閉じた変数を提供する必要もあります。これらの閉じた変数を表すオブジェクトを最初の引数として受け取る関数によって、クロージャーをモデル化できます。

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

closure.apply(closure, ...realArgs)これが必要とする厄介な呼び出し規約に注意してください

JavaScriptの組み込みオブジェクトのサポートにより、明示的なvars引数を省略することが可能になり、this代わりに使用できるようになります。

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

これらの例は、実際にクロージャーを使用するこのコードと同等です。

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

この最後の例では、オブジェクトは、返された2つの関数をグループ化するためにのみ使用されます。this結合は無関係です。クロージャーを可能にする詳細-隠されたデータを実際の関数に渡し、クロージャー変数へのすべてのアクセスをその隠されたデータのルックアップに変更する-は、言語によって処理されます。

しかし、クロージャーの呼び出しには、その追加データを渡すオーバーヘッドが含まれ、クロージャーの実行には、その追加データのルックアップのオーバーヘッドが含まれます。クロージャーに依存しないソリューションのパフォーマンスが向上します。特に、クロージャーでできることは、非常に安価な算術演算がいくつかあるためです。これは、解析中に定数に折り畳まれることさえあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.