ES6 생성기 대 반복기 성능

tldr;



ES6 생성기는 매우 간결하고 명확한 코드로 반복을 허용합니다. 그러나 이러한 편리함에는 대가가 따릅니다.



다음 서명을 사용하여 iterable에 대해 범용flatMap을 작성한다고 가정합니다.

function flatMap<T, U>(
    items: Iterable<T>,
    mapper: (item: T) => Iterable<U>
): Iterable<U>


제너레이터와 이터레이터로 구현하고 레이스를 만들어 봅시다!

발전기



제너레이터 구현이 얼마나 훌륭하고 짧은지 보십시오. 확실히 버그가 있을 자리는 없습니다!

function *flatMap<T, U>(
    items: Iterable<T>,
    mapper: (item: T) => Iterable<U>
): Iterable<U> {
    for (const item of items) {
        yield* mapper(item);
    }
}


반복자



구현이 다소 복잡합니다. 독자는 그것을 얻기 위해 몇 가지 접근 방식을 취해야 합니다.

function flatMap<T, U>(
    items: Iterable<T>,
    mapper: (item: T) => Iterable<U>
): Iterable<U> {
    return {
        [Symbol.iterator]() {
            const outer = items[Symbol.iterator]();
            let inner: Iterator<U>;
            return {
                next() {
                    for ( ; ; ) {
                        if (inner) {
                            const i = inner.next();
                            if (!i.done) return i;
                        }

                        const o = outer.next();
                        if (o.done) {
                            return {
                                done: true,
                                value: undefined,
                            };
                        }
                        inner = mapper(o.value)[Symbol.iterator]();
                    }
                }
            };
        }
    }
}


경마 대회!



벤치마크를 작성해 보겠습니다.

import * as Benchmark from 'benchmark';

import { flatMap as flatMapGen } from './flatMapGen';
import { flatMap as flatMapItr } from './flatMapItr';

let suite = new Benchmark.Suite();

[1, 10, 100, 1000, 10000, 100000].map(makeInput).forEach(input => {
    suite = suite.add(
        `Gen[${input.length}]`,
        () => consume(flatMapGen(input, i => [i, i + 1, i + 2])),
    );
    suite = suite.add(
        `Itr[${input.length}]`,
        () => consume(flatMapItr(input, i => [i, i + 1, i + 2])),
    );
});


suite
    .on('cycle', (event: Event) => console.log(String(event.target)))
    .run();

function makeInput(n: number) {
    const a = [];
    for (let i = 0; i < n; i++) a[i] = i * Math.random();
    return a;
}

function consume(itr: Iterable<number>) {
    let x = 0;
    for (const i of itr) x += i;
    if (x > 1e12) console.log('Never happens');
}


결과



숫자는 작업/초입니다.


N
발전기
반복자
우승자


1
3,466,783
1,438,388
발전기는 2.4배 더 빠릅니다.

10
486,073
621,149
반복자는 1.2배 더 빠릅니다.

100
58,009
102,465
반복자는 1.8배 더 빠릅니다.

1,000
5,600
10,699
반복자는 1.9배 더 빠릅니다.

10,000
557
1,115
반복자는 2.0배 더 빠릅니다.

100,000
54.15
106
반복자는 2.0배 더 빠릅니다.


메모:
  • 노드 버전은 14.8.0입니다.
  • 힙 크기는 4GB입니다.
  • 귀하의 수치는 다를 수 있지만 최근 Node 및 Chrome의 경우 비율은 동일해야 합니다.
  • 다른 브라우저에서는 숫자가 완전히 다르며 생성기는 아직 더 느립니다
  • .

    제너레이터가 똑같이 보이는 이유는 무엇입니까?



    상태와 클로저가 있는 단순한 객체인 반복자와 달리 제너레이터는 일시 중단된 함수입니다. C++ 또는 Java의 스레드와 마찬가지로 자체 실행 스택이 있지만 기본 스레드와 병렬로 실행되지 않습니다. 인터프리터는 next() 에서 생성기 실행을 시작하거나 다시 시작하고 yield 에서 기본 스레드로 다시 시작합니다. 이것을 "코루틴"이라고 부르기도 하지만 JS에서는 그리 흔한 용어는 아닙니다.
    n=1에서 알 수 있듯이 현재 스택을 포크하는 것은 여러 객체와 클로저를 만드는 것보다 훨씬 저렴합니다. 그러나 스택을 전환하는 것은 링크를 역참조하고 일반 JS 함수를 호출하는 것보다 비용이 더 많이 듭니다.

    결론: 발전기를 사용해야 합니까?



    코드가 복잡하고 다르게 작성하면 이해하기 어렵다고 생각되면 생성기를 사용하십시오! 좋은 코드는 이해할 수 있는(필요한 경우 최적화된) 코드임을 기억하십시오.

    그러나 flatMap 와 같은 간단한 작업, 라이브러리 및 자주 실행되는 루틴의 경우 단순 반복자가 여전히 선호되는 옵션입니다.

    즐거운 코딩!

    좋은 웹페이지 즐겨찾기