Generator vs Iterator

dotoritos·2024년 11월 9일
0
post-thumbnail

개요

adjacentPairs는 인접한 n 개의 요소를 묶어서 순회하는 유틸리티 함수이다. 이때 isClosed가 true이면 배열의 시작과 끝도 연결돼 있다고 간주하고, 마지막 원소까지 순회한다.

function adjacentPairs<T>(
	itr: Iterable<T>,
	n: number,
	isClosed?: boolean,
): IterableIterator<T[]>

예를 들어서 [0, 1, 2, 3, 4] 배열에서 n = 3 인 경우, 각각 다음과 같은 동작을 한다.

isClosed === false
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
isClosed === true
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 0]
[4, 0, 1]

Generator 구현체

Generator 문법을 쓰면 다음과 같이 매우 간단하게 구현할 수 있다.

export function* adjacentPairs<T>(
	itr: Iterable<T>,
	n: number,
	isClosed?: boolean,
) {
	const first = new Array(n - 1) as T[];
	const buffer = new Array(n) as T[];
	let count = 0;
	for (const value of itr) {
		pull(buffer, value);
		if (++count >= n) {
			yield [...buffer];
		} else if (count < n) {
			first[count - 1] = value;
		}
	}
	if (count >= n && isClosed) {
		for (const value of first) {
			pull(buffer, value);
			yield [...buffer];
		}
	}
}

function pull<T>(values: T[], newValue: T) {
	values.shift();
	values.push(newValue);
}

Iterator 구현체

Iterator는 좀 귀찮지만 다음과 같이 수행할 수 있다.

export function adjacentPairs<T>(
	itr: Iterable<T>,
	n: number,
	isClosed?: boolean,
): IterableIterator<T[]> {
	const buffer = new Array<T>(n);
	const it = itr[Symbol.iterator]();
	let res = it.next();
	// n개를 불러오고 시작
	for (let i = 0; i < n; ++i) {
		if (res.done) {
			return {
				[Symbol.iterator]() {
					return this;
				},
				next() {
					return {
						value: undefined,
						done: true,
					};
				},
			};
		}

		buffer[i] = res.value;
		res = it.next();
	}

	const first = buffer.slice(0, n - 1);
	let isEnd = false;
	let ringCount = 0;

	return {
		[Symbol.iterator]() {
			return this;
		},
		next() {
			if (isEnd) {
				return {
					value: undefined,
					done: true,
				};
			}

			const ret = buffer.slice();

			if (res.done) {
				if (isClosed) {
					if (ringCount < n - 1) {
						pull(buffer, first[ringCount++]);
					} else {
						isEnd = true;
					}
				} else {
					isEnd = true;
				}
			} else {
				pull(buffer, res.value);
				res = it.next();
			}

			return {
				value: ret,
				done: false,
			};
		},
	};
}

벤치마크 실험 결과

각 구현체에 대해 다음의 for문을 수행한다.

const N = 10000;
const huge = Array.from(new Array(N), () => 0);
for (const i of adjacentPairs(huge, n, true)) {
	i;
}

테스트는 https://jsbench.me/ 여기서 진행하였다.

함수 정의부는 모두 setup에서 수행

nIterator (ops/s)Generator (ops/s)Slow Rate (%)
23800 ± 1.41%2100 ± 1.44%46.08
33700 ± 2.05%2100 ± 0.87%44.46
502700 ± 2.32%967 ± 2.37%63.97
1001900 ± 4.3%485 ± 6.64%74.03

실무 데이터 실험 결과

실무에서 특히 제너레이터를 많이 쓰는 로직에 빌드하는데 얼마나 시간이 걸리는지 측정하였다.

#1#2#3#4#5평균(ms)
Iterator (ms)17749.5217031.4317047.0617355.8517008.6917328.51
Generator (ms)29249.8729583.8728810.9929398.4529237.5029256.13
테스트 케이스 비공개

결론

제너레이터는 성능에 있어 매우 좋지 않다.

profile
졸려

0개의 댓글