カスタム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
(ここではV8開発者です。)要するに、エンジンのヒューリスティックによって決定されるように、インライン化されているか、その欠如です。
最適化コンパイラの場合、呼び出された関数のインライン化には大きな利点があります(たとえば、呼び出しのオーバーヘッドを回避する、定数畳み込みを可能にする、重複計算を排除する、追加のインライン化の新しい機会を作成するなど)が、コストがかかります。コンパイル自体が遅くなり、保持されないことが判明した何らかの仮定のために、最適化されたコードを後で破棄(「最適化解除」)しなければならないリスクが高まります。何もインライン化しないとパフォーマンスが無駄になり、すべてをインライン化するとパフォーマンスが無駄になります。正確に正しい関数をインライン化するには、プログラムの将来の動作を予測できる必要がありますが、これは明らかに不可能です。したがって、コンパイラはヒューリスティックを使用します。
V8の最適化コンパイラには、現在、特定の場所で呼び出されたのと常に同じ関数である場合にのみ、関数をインライン化するヒューリスティックがあります。この場合、それは最初の反復の場合です。その後の反復では、コールバックとして新しいクロージャが作成されます。これは、V8の観点からは新しい関数であるため、インライン化されません。(V8は実際には、同じソースからの関数インスタンスを重複排除し、とにかくインライン化できる高度なトリックをいくつか知っていますが、この場合は適用されません[理由はわかりません])。
だから、最初の反復では、(を含むすべてのものx => x % 2 === 0
とはx => x * 2
)にインライン化されますtoArray
。2回目以降はそうではなく、生成されたコードが実際の関数呼び出しを実行します。
それはおそらく問題ありません。ほとんどの実際のアプリケーションでは、違いはほとんど測定できないと思います。(テストケースを減らすと、そのような違いがより目立つ傾向がありますが、小さなテストで行われた観察に基づいて大きなアプリのデザインを変更することは、多くの場合、時間を費やす最も影響力のある方法ではなく、最悪の場合、事態を悪化させる可能性があります。)
また、エンジン/コンパイラーのコードを手動で最適化することは難しいバランスです。私は一般的にそうしないことをお勧めします(エンジンは時間の経過とともに改善され、コードを高速化するのは本当に彼らの仕事だからです)。一方、明らかに効率の高いコードと効率の低いコードがあり、全体的な効率を最大化するには、関係者全員が自分の役割を果たす必要があります。つまり、可能な場合はエンジンの作業を単純化することをお勧めします。
これのパフォーマンスを微調整したい場合は、コードとデータを分離することで微調整できます。これにより、常に同じ関数が呼び出されるようになります。たとえば、コードのこの変更されたバージョンのように:
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);
そこには、これ以上の動的ホットパス上の関数を作成する方法を守っていないと、「パブリックインターフェイス」(すなわち道arrayIterator
、map
、filter
、およびtoArray
コン)は、詳細が変更されている唯一のボンネット下、前と全く同じです。すべての関数に名前を付けることの利点は、より有用なプロファイリング出力が得られることです;-)
あなたのコード内のいくつかの場所を持っている場合、そのコール:賢明な読者は、この変更はわずかな距離の問題をシフトことがわかりますmap
し、filter
異なる修飾子は/述語が、その後、inlineabilityの問題が出てくると再び。上で述べたように、実際のアプリは通常異なる動作をするため、マイクロベンチマークは誤解を招く傾向があります...
(FWIW、これは、この関数呼び出しの実行時間が変更されるのはなぜですか?とほぼ同じ効果です。)
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加