LHJ

I'm a FE developer.

28. Abstract Loop & Lazy Execution - Lazy Execution

14 Aug 2020 » codespitz_re

Lazy Execution

자바스크립트는 자바스크립트이다.
언어를 컴파일해서 컴퓨터에 전달하는 것은 전부 C가 한다.
그리고 C 개발자들이 그 속도를 계속해서 개선해준다.

현재 Generator 가 느려보여도 C 개발자들은 빠른시일내에 이를 개선해준다.
즉 브라우저가 빠른 시일내에 업그레이드 된다는 것이다.
이러한 이유로 최신 언어 스펙 제안대로 코드를 작성하는 것이 무조건 좋다.

하지만 자바스크립트 코드로 DOM을 다루는 코드를 최적화하는 것은 굉장히 의미가 있다.
개발자창 - 프로파일링 부분을 확인하면 어느 브라우저나 예외없이 97% 랜더링으로 나온다.
여러분들이 작성한 코드가 최적화 되던 말던 3%라는 것이다.
두배 빠르게 개선했다고해도 1.5% 빨라지는 것이다.

하지만 DOM을 최적화하면?
97%의 5%만 최적화해도 4.xx%가 빨라지게 되는 것이다.

즉 일반적인 최적화는 랜더링에 달려있다.
자바스크립트 알고리즘에 달려있는 것이 아니다.
즉 자바스크립트는 내장된 언어 스펙을 무조건 가져다 쓰는 것이 좋다.
스크립트 언어를 바라보는 관점은 주어진 클래스 라이브러리를 최대한 활용하는 것이다.

JAVA 같은 언어들은 직접 작성한 코드들이 내장 객체와 속도가 동일할 수도 있다.
하지만 자바스크립트는 그렇지 않다.
무조건 V8 엔진 내장 객체가 훨씬 더 빠르다.
무조건 랜더링 개선에 달렸다.

오히려 속도 개선보단 구조적인 개선에 초점을 맞춰야한다.

프로그램의 구조적 문제는 대부분 if가 복잡해지는 것에 있어 발생한다.
어떻게하면 if 중첩을 줄일까?
한 단계 if를 줄일 때마다 if가 가지고 있는 모든 경우의 수만큼의 객체를 만든 다음에 그 객체를 선택한 선택기를 바깥으로 빼면, 선택기를 확장하면서 로직들은 개별 객체로 넘어가기 때문에, 복잡성이 한단계 줄어든다. (그래봤자 한단계지만..)
if 중첩이 3번이면 그런 행동을 3단계나 해야된다.

전략객체가 그 전략객체를 소비하고, 그 전략객체가 또 다른 전략객체를 소비하고..
안타깝지만 아키텍처상 방법은 이것 뿐이다.

Execution을 Lazy하게

const odd = function*(data) {
    for (const v of data) {
        console.log("odd", odd.cnt++);
        // 나머지 구하는 식은 양의 정수만 된다.
        // 따라서 절대값으로 계산한다.
        if (Math.abs(v) % 2) yield v;
    }
};
odd.cnt = 0;
for (const v of odd([1,2,3,4])) console.log(v)

// odd 0
// 1
// odd 1
// odd 2
// 3
// odd 3

일단 Execution을 Lazy하게 하는 방법 중 하나가 yield였다.
yield를 하면 필요한 만큼만 루프를 돌릴 수 있다. 위의 코드는 루프를 바져나오라는 말이 없으므로 전부 다 돌고 빠져나온다.

const take = function*(data, n) {
    for (const v of data) {
        console.log("take", take.cnt++);
        if (n--) yield v; else break;
    }
};
take.cnt = 0;
for (const v of take([1,2,3,4], 2)) console.log(v);

// take 0
// 1
// take 1
// 2
// take 2

take란 함수는 n 값으로 루프를 몇번 돌게할지를 제어한다.

위의 두 개 기능을 합쳐라 : 홀수이면서 두개만 추출해라.

상태를 이용하는 방법이 먼저 떠오를 것이다.

  • 홀수를 먼저 걸러낸 후 배열을 만든 다음, 그 중에 2개를 골라낸다.

하지만 위와 같은 방법이 효율적인 방법일까?

  • 루프를 돌려서 첫번째 홀수를 찾아내어 take 함수가 가져가도록하고 또 루프를 돌려서 두번째 홀수를 찾아내 take 함수가 가져가도록 한 후 종료한다.

이런 방법이 훨씬 더 효율적일 것이다.
위 방법으로 하려면 루프를 돌리다가 중간에 중지시켜야된다.
코루틴은 이를 가능하게한다.
루프를 돌리다가 조건을 충족하면 yield로 중지시키면 된다.

const odd = function*(data) {
    for (const v of data) {
        console.log("odd", odd.cnt++);
        // 나머지 구하는 식은 양의 정수만 된다.
        // 따라서 절대값으로 계산한다.
        if (Math.abs(v) % 2) yield v;
    }
};
odd.cnt = 0;

const take = function*(data, n) {
    for (const v of data) {
        console.log("take", take.cnt++);
        if (n--) yield v; else break;
    }
};
take.cnt = 0;

for (const v of take(odd([1,2,3,4]), 2)) console.log(v);
// odd 0
// take 0

// -- 이때 n이 2 --

// 1 

// -- 이때 n이 1 --

// odd 1
// odd 2
// take 1
// 3

// -- 이때 n이 0 --

// odd 3

상태로 루프를 만들면 이런식으로 못돌린다.
상태로 루프를 만들면 전체 홀수를 한번 걸러내주고 그 다음에 두 개를 고르는 식으로 작성할 수밖에 없다.
배열 length가 2000개라면 2000번 반복하고 두 개 거르는 반복까지 더 돌려야되는 것이다.

하지만 위와 같은 코루틴 서스팬션을 사용하면 짧게 돌릴 수 있다.
제어문이 실행되다가 멈출 수 있다는 성질을 이용한 것이다.
이렇게하면 효율성이 증대된다.

위의 식에서 yieldyield를 체인했다.
그렇다는 말은 n개의 yield를 체인할 수 있다는 말이다.