最初の関数呼び出しが他のすべての順次呼び出しより2倍速く実行されるのはなぜですか?

ラテックス

カスタムJSイテレータ実装と後者の実装のパフォーマンスを測定するためのコードがあります。

const ITERATION_END = Symbol('ITERATION_END');

const arrayIterator = (array) => {
  let index = 0;

  return {
    hasValue: true,
    next() {
      if (index >= array.length) {
        this.hasValue = false;

        return ITERATION_END;
      }

      return array[index++];
    },
  };
};

const customIterator = (valueGetter) => {
  return {
    hasValue: true,
    next() {
      const nextValue = valueGetter();

      if (nextValue === ITERATION_END) {
        this.hasValue = false;

        return ITERATION_END;
      }

      return nextValue;
    },
  };
};

const map = (iterator, selector) => customIterator(() => {
  const value = iterator.next();

  return value === ITERATION_END ? value : selector(value);
});

const filter = (iterator, predicate) => customIterator(() => {
  if (!iterator.hasValue) {
    return ITERATION_END;
  }

  let currentValue = iterator.next();

  while (iterator.hasValue && currentValue !== ITERATION_END && !predicate(currentValue)) {
    currentValue = iterator.next();
  }

  return currentValue;
});

const toArray = (iterator) => {
  const array = [];

  while (iterator.hasValue) {
    const value = iterator.next();

    if (value !== ITERATION_END) {
      array.push(value);
    }
  }

  return array;
};

const test = (fn, iterations) => {
  const times = [];

  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    times.push(performance.now() - start);
  }

  console.log(times);
  console.log(times.reduce((sum, x) => sum + x, 0) / times.length);
}

const createData = () => Array.from({ length: 9000000 }, (_, i) => i + 1);

const testIterator = (data) => () => toArray(map(filter(arrayIterator(data), x => x % 2 === 0), x => x * 2))

test(testIterator(createData()), 10);

テスト関数の出力は非常に奇妙で予期しないものです。最初のテスト実行は、他のすべての実行よりも常に2倍速く実行されます。結果の1つで、配列にはすべての実行時間が含まれ、数値は平均です(ノードで実行しました)。

[
  147.9088459983468,
  396.3472499996424,
  374.82447600364685,
  367.74555300176144,
  363.6300039961934,
  362.44370299577713,
  363.8418449983001,
  390.86111199855804,
  360.23125199973583,
  358.4788999930024
]
348.6312940984964

Denoランタイムを使用しても同様の結果が見られますが、他のJSエンジンではこの動作を再現できませんでした。V8でその背後にある理由は何でしょうか?

Environment: Node v13.8.0, V8 v7.9.317.25-node.28, Deno v1.3.3, V8 v8.6.334

jmrk

(ここではV8開発者です。)要するに、エンジンのヒューリスティックによって決定されるように、インライン化されているか、その欠如です。

最適化コンパイラの場合、呼び出された関数のインライン化には大きな利点があります(たとえば、呼び出しのオーバーヘッドを回避する、定数畳み込みを可能にする、重複計算を排除する、追加のインライン化の新しい機会を作成するなど)が、コストがかかります。コンパイル自体が遅くなり、保持されないことが判明した何らかの仮定のために、最適化されたコードを後で破棄(「最適化解除」)しなければならないリスクが高まります。何もインライン化しないとパフォーマンスが無駄になり、すべてをインライン化するとパフォーマンスが無駄になります。正確に正しい関数をインライン化するには、プログラムの将来の動作を予測できる必要がありますが、これは明らかに不可能です。したがって、コンパイラはヒューリスティックを使用します。

V8の最適化コンパイラには、現在、特定の場所で呼び出されたのと常に同じ関数である場合にのみ、関数をインライン化するヒューリスティックがあります。この場合、それは最初の反復の場合です。その後の反復では、コールバックとして新しいクロージャが作成されます。これは、V8の観点からは新しい関数であるため、インライン化されません。(V8は実際には、同じソースからの関数インスタンスを重複排除し、とにかくインライン化できる高度なトリックをいくつか知っていますが、この場合は適用されません[理由はわかりません])。

だから、最初の反復では、(を含むすべてのものx => x % 2 === 0とはx => x * 2)にインライン化されますtoArray2回目以降はそうではなく、生成されたコードが実際の関数呼び出しを実行します。

それはおそらく問題ありません。ほとんどの実際のアプリケーションでは、違いはほとんど測定できないと思います。(テストケースを減らすと、そのような違いがより目立つ傾向がありますが、小さなテストで行われた観察に基づいて大きなアプリのデザインを変更することは、多くの場合、時間を費やす最も影響力のある方法ではなく、最悪の場合、事態を悪化させる可能性があります。)

また、エンジン/コンパイラーのコードを手動で最適化することは難しいバランスです。私は一般的にそうしないことをお勧めします(エンジンは時間の経過とともに改善され、コードを高速化するのは本当に彼らの仕事だからです)。一方、明らかに効率の高いコードと効率の低いコードがあり、全体的な効率を最大化するには、関係者全員が自分の役割を果たす必要があります。つまり、可能な場合はエンジンの作業を単純化することをお勧めします。

これのパフォーマンスを微調整したい場合は、コードとデータを分離することで微調整できます。これにより、常に同じ関数が呼び出されるようになります。たとえば、コードのこの変更されたバージョンのように:

const ITERATION_END = Symbol('ITERATION_END');

class ArrayIterator {
  constructor(array) {
    this.array = array;
    this.index = 0;
  }
  next() {
    if (this.index >= this.array.length) return ITERATION_END;
    return this.array[this.index++];
  }
}
function arrayIterator(array) {
  return new ArrayIterator(array);
}

class MapIterator {
  constructor(source, modifier) {
    this.source = source;
    this.modifier = modifier;
  }
  next() {
    const value = this.source.next();
    return value === ITERATION_END ? value : this.modifier(value);
  }
}
function map(iterator, selector) {
  return new MapIterator(iterator, selector);
}

class FilterIterator {
  constructor(source, predicate) {
    this.source = source;
    this.predicate = predicate;
  }
  next() {
    let value = this.source.next();
    while (value !== ITERATION_END && !this.predicate(value)) {
      value = this.source.next();
    }
    return value;
  }
}
function filter(iterator, predicate) {
  return new FilterIterator(iterator, predicate);
}

function toArray(iterator) {
  const array = [];
  let value;
  while ((value = iterator.next()) !== ITERATION_END) {
    array.push(value);
  }
  return array;
}

function test(fn, iterations) {
  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    console.log(performance.now() - start);
  }
}

function createData() {
  return Array.from({ length: 9000000 }, (_, i) => i + 1);
};

function even(x) { return x % 2 === 0; }
function double(x) { return x * 2; }
function testIterator(data) {
  return function main() {
    return toArray(map(filter(arrayIterator(data), even), double));
  };
}

test(testIterator(createData()), 10);

そこには、これ以上の動的ホットパス上の関数を作成する方法を守っていないと、「パブリックインターフェイス」(すなわち道arrayIteratormapfilter、およびtoArrayコン)は、詳細が変更されている唯一のボンネット下、前と全く同じです。すべての関数に名前を付けることの利点は、より有用なプロファイリング出力が得られることです;-)

あなたのコード内のいくつかの場所を持っている場合、そのコール:賢明な読者は、この変更はわずかな距離の問題をシフトことがわかりますmapし、filter異なる修飾子は/述語が、その後、inlineabilityの問題が出てくると再び。上で述べたように、実際のアプリは通常異なる動作をするため、マイクロベンチマークは誤解を招く傾向があります...

(FWIW、これは、この関数呼び出しの実行時間が変更されるのはなぜですか?とほぼ同じ効果です。)

この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。

侵害の場合は、連絡してください[email protected]

編集
0

コメントを追加

0

関連記事

分類Dev

引数のない関数呼び出しがより速く実行されるのはなぜですか?

分類Dev

すぐに呼び出されるラムダを追加すると、JavaScriptコードが2倍速くなるのはなぜですか?

分類Dev

StringBuilderのコンストラクターでのこの連結の使用がappend()の呼び出しよりも100倍速いのはなぜですか?

分類Dev

関数が再度呼び出された場合、関数の最初の呼び出しの実行を停止するにはどうすればよいですか?

分類Dev

最初のajax呼び出しは非常に遅くなり、後続の呼び出しはすばやく実行されます-なぜですか?

分類Dev

関数を呼び出せなくても、関数呼び出しの引数部分の式が評価されるのはなぜですか?

分類Dev

関数を直接呼び出すよりも、goプラグインを介して関数を呼び出す方が速いのはなぜですか

分類Dev

このプログラム内で想定されているように関数呼び出しが実行されないのはなぜですか?

分類Dev

xorps命令を追加すると、cvtsi2ssを使用してこの関数が作成され、追加が最大5倍速くなるのはなぜですか?

分類Dev

私の角度関数は最初の呼び出しで実行されていません。2回目の呼び出しでのみ実行されます

分類Dev

コンストラクタの最初の呼び出しに他の呼び出しの10倍の時間がかかるのはなぜですか?

分類Dev

このラップされた関数呼び出しが通常の関数よりも速いのはなぜですか?

分類Dev

同じ行に2つの関数呼び出しがあるのに、なぜ関数呼び出しがスキップされるのですか?

分類Dev

Python:同じ関数の2つの呼び出しが並行して実行されないのはなぜですか?

分類Dev

最初の4回の実行でsetInterval呼び出しが抑制されないのはなぜですか?

分類Dev

これらのprint()呼び出しが間違った順序で実行されているように見えるのはなぜですか?

分類Dev

clojurescriptが「-」を使用して一部のjs関数を呼び出し、他の関数を呼び出さないのはなぜですか?

分類Dev

コードを関数として呼び出す方が、Clozure Common lispで直接呼び出すよりも時間がかかるのはなぜですか?

分類Dev

Reactで関数が2回呼び出されるのはなぜですか?

分類Dev

'ls' が fork() ではなく execve() 呼び出しによって作成されるのはなぜですか

分類Dev

どの非同期呼び出しが最初に返されるかに応じて関数を実行します

分類Dev

表示関数が2回呼び出されるのはなぜですか?

分類Dev

次の再帰コードの出力が53 1 1 3 5であるのはなぜですか?5 3 1 -1 1 3 5.であるべきではありませんか?この関数は-1によっても呼び出されます。説明してください?

分類Dev

実際のルートが呼び出され、存在しないルートが呼び出されるまでのすべてのルート関数

分類Dev

割り当て解除関数が2つではなく1つの引数で呼び出されるのはなぜですか?

分類Dev

この関数呼び出しの実行時間が変わるのはなぜですか?

分類Dev

この次のコードでは、JavaScript関数式が呼び出される/実行される/呼び出されるのはいつですか?

分類Dev

関数呼び出し後にフィールドが切り捨てられるのはなぜですか?

分類Dev

関数呼び出し後にフィールドが切り捨てられるのはなぜですか?

Related 関連記事

  1. 1

    引数のない関数呼び出しがより速く実行されるのはなぜですか?

  2. 2

    すぐに呼び出されるラムダを追加すると、JavaScriptコードが2倍速くなるのはなぜですか?

  3. 3

    StringBuilderのコンストラクターでのこの連結の使用がappend()の呼び出しよりも100倍速いのはなぜですか?

  4. 4

    関数が再度呼び出された場合、関数の最初の呼び出しの実行を停止するにはどうすればよいですか?

  5. 5

    最初のajax呼び出しは非常に遅くなり、後続の呼び出しはすばやく実行されます-なぜですか?

  6. 6

    関数を呼び出せなくても、関数呼び出しの引数部分の式が評価されるのはなぜですか?

  7. 7

    関数を直接呼び出すよりも、goプラグインを介して関数を呼び出す方が速いのはなぜですか

  8. 8

    このプログラム内で想定されているように関数呼び出しが実行されないのはなぜですか?

  9. 9

    xorps命令を追加すると、cvtsi2ssを使用してこの関数が作成され、追加が最大5倍速くなるのはなぜですか?

  10. 10

    私の角度関数は最初の呼び出しで実行されていません。2回目の呼び出しでのみ実行されます

  11. 11

    コンストラクタの最初の呼び出しに他の呼び出しの10倍の時間がかかるのはなぜですか?

  12. 12

    このラップされた関数呼び出しが通常の関数よりも速いのはなぜですか?

  13. 13

    同じ行に2つの関数呼び出しがあるのに、なぜ関数呼び出しがスキップされるのですか?

  14. 14

    Python:同じ関数の2つの呼び出しが並行して実行されないのはなぜですか?

  15. 15

    最初の4回の実行でsetInterval呼び出しが抑制されないのはなぜですか?

  16. 16

    これらのprint()呼び出しが間違った順序で実行されているように見えるのはなぜですか?

  17. 17

    clojurescriptが「-」を使用して一部のjs関数を呼び出し、他の関数を呼び出さないのはなぜですか?

  18. 18

    コードを関数として呼び出す方が、Clozure Common lispで直接呼び出すよりも時間がかかるのはなぜですか?

  19. 19

    Reactで関数が2回呼び出されるのはなぜですか?

  20. 20

    'ls' が fork() ではなく execve() 呼び出しによって作成されるのはなぜですか

  21. 21

    どの非同期呼び出しが最初に返されるかに応じて関数を実行します

  22. 22

    表示関数が2回呼び出されるのはなぜですか?

  23. 23

    次の再帰コードの出力が53 1 1 3 5であるのはなぜですか?5 3 1 -1 1 3 5.であるべきではありませんか?この関数は-1によっても呼び出されます。説明してください?

  24. 24

    実際のルートが呼び出され、存在しないルートが呼び出されるまでのすべてのルート関数

  25. 25

    割り当て解除関数が2つではなく1つの引数で呼び出されるのはなぜですか?

  26. 26

    この関数呼び出しの実行時間が変わるのはなぜですか?

  27. 27

    この次のコードでは、JavaScript関数式が呼び出される/実行される/呼び出されるのはいつですか?

  28. 28

    関数呼び出し後にフィールドが切り捨てられるのはなぜですか?

  29. 29

    関数呼び出し後にフィールドが切り捨てられるのはなぜですか?

ホットタグ

アーカイブ