web-dev-qa-db-ja.com

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

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';
 }
};
14
Tomy

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

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

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

23
Karl Bielefeldt

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

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

したがって、最初の関数で、エンジンがこのステートメントを確認すると、次のようになります。

var gbSize = size / GB;

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

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

手順3は、単に変数を割り当てるよりもかなりコストがかかります。さらに、これを5回実行し、GBMBの両方を2回実行します。関数の先頭でこれらにエイリアスを付けた場合は、(たとえば、var gb = GB)の代わりにエイリアスを参照すると、実際にはわずかな速度向上が得られますが、一部のJSエンジンがすでにこの最適化を実行している可能性もあります。そしてもちろん、実行を高速化する最も効果的な方法は、スコープチェーンを全然経由しないことです。

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

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

12
Aaronaught

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バインディングは関係ありません。クロージャーを可能にするすべての詳細(隠しデータを実際の関数に渡し、クロージャー変数へのすべてのアクセスをその隠しデータ内のルックアップに変更する)は、言語によって処理されます。

ただし、クロージャーを呼び出すと、余分なデータを渡すオーバーヘッドが発生し、クロージャーを実行すると、余分なデータを検索するオーバーヘッドが発生します。これは、通常の変数と比較すると、キャッシュの局所性が悪く、通常はポインターの逆参照によって悪化するため、驚くことではありません。クロージャに依存しないソリューションの方がパフォーマンスが優れています。特に、クロージャーを使用することで節約できることはすべて、非常に安価な算術演算であり、構文解析中に定数が折りたたまれる場合さえあります。

2
amon