책 You Don’t Know JS : this와 객체 프로토타입, 비동기와 성능
스터디에서 진행한 [PART II 비동기와 성능 - 5장 프로그램 성능] 관련 정리입니다.

서론

JS에서 비동기성이 중요한 이유? 성능..성능..성능!

  • 독립적인 2개의 AJAX 요청을 어떻게 처리할까?

    (1) 순차(Serial) : 첫 번째 요청이 완료되면 두 번째 요청을 시작

    (2) 동시(Concurrent) : 프라미스/제네레이터 처럼 병렬 전송 후 모두 관문을 통과할 때 까지 대기

    → 후자의 성능 및 UX가 훨씬 우수하다

  • 이제 로컬 영역의 비동기 패턴보다 넓은 시야인 프로그램 수준의 성능을 알아보자

웹 워커

들어가기 전에...

프로그램을 둘로 나누어 하나는 메인 스레드, 하나는 보조 스레드에서 실행하는 아키텍쳐가 있다고 하면, 다음 두 가지를 생각해 볼 수 있다.

이슈1

별개의 스레드에서 실행
→ 보조 스레드에서 메인 스레드를 중단시키지 않고 병렬 실행한다는 뜻?
가상 스레딩은 JS로 비동기 동시성을 구현한 것 보다 나을게 없다.

이슈2

동시에 같은 공유 스코프/자원에 접근 가능한가?
→ 결국 다른 멀티스레딩 언어에서 발생하는 문제들을 수반하게 된다.

스코프/자원 공유가 불가능하다면 어떻게 통신할까?

HTML5 무렵부터 웹 워커라는 웹 플랫폼의 특성으로 이같은 질문에 결실을 맺게 되었다.
하지만 이는 브라우저 특성으로 JS와는 관계없으며, JS는 현재까지 스레드 실행을 지원하지 않는다.

브라우저 환경에서 다수의 독립적인 스레드 조각을 (웹) 워커라고 하며,
이는 프로그램을 덩이로 나누어 병렬 실행하는 '작업 병행성(Task Parelleism)'을 추구한다.

먼저 웹 워커를 생성하는 방법을 알아본다.

워커 인스턴스 생성

워커로 읽어들일 js 파일의 URL을 지정하면, 브라우저는 이 파일을 별도의 스레드에서 실행한다.

    var w1 = new Worker("http://some.url.1/myWorker.js");

URL로 실행한 워커는 전용 워커(Dedicated Worker)라고 하며,
Blob URL로 인라인 워커를 생성하는 방법도 있다.

워커의 이벤트 메시징 체계

워커는 다른 워커 혹은 메인 프로그램과 스코프/자원을 공유하지 않으며, 이벤트 메시징 체계 바탕으로 연결된다.

워커 객체 w1의 구성 : 이벤트 리스너 + 트리거

    // 메인 js
    w1.addEventListener("메시지", function(ev) { /*ev.data*/ });
    w1.postMessage("재미난 얘기");

워커의 구조 역시 비슷하다.

    // mycoolworker.js
    addEventListener("메시지", function(ev) { /*ev.data*/ });
    postMessage("쿨한 대답");

전용 워커는 자신을 만든 프로그램과 1:1 관계 이므로, 메시지 이벤트를 구별할 필요는 없다.

기타 워커의 특징

  • 워커는 필요시 자신의 자식 워커들을 만들 수 있다. (subworkers)
  • 세부 로직은 마스터 워커에게 넘기고, 작업을 단계별로 처리할 다른 워커를 생성하도록 위임하는 것이 가능
  • 워커를 spawn한 프로그램은 이 워커 객체를 terminate()으로 제거한다.
  • 브라우저에서 다수의 페이지가 같은 파일 URL로부터 워커를 생성하려 하면 각 페이지는 별개의 워커로 동작한다.

1.1 워커 환경

  • 워커 내부에서는 메인 프로그램 자원 접근이 불가능
  • AJAX / 웹소켓 / 타이머 설정 / navigator / location / JSON / applicationCache 등을 사용 가능
  • 워커에 추가 자바스크립트를 읽을때 워커 내부에 다음과 같이 작성한다.

          importScripts("foo.js", "bar.js")
    

    동기적으로 실행하므로, 해당 파일 처리가 완료될 때까지 나머지 워커 코드는 중지된다.

  • 워커에서 <canvas> API 사용
    <canvas>는 DOM의 일부이므로 Worker에서 사용이 불가능했었다.
    2018.10 현재 → Chrome 69에 추가 된 OffscreenCanvas API 도입!
    참고: https://www.slideshare.net/deview/122-html5-canvas

웹 워커의 4가지 주요 용도

웹 워커는 주로 다음과 같은 처리를 위해 사용한다.

  • 처리 집약적 수학 계산
  • 대용량 데이터 세트 정렬
  • 데이터 작업 (압축, 오디오 분석, 이미지 픽셀 변환)
  • 트래픽 높은 네트워크 통신

1.2 데이터 전송

웹 워커 이용의 주 목적 : 이벤트 체계를 바탕으로 스레드간 대량의 데이터가 양방향 전송되어야 함

  • 초기
    : 전체 데이터를 문자열로 직렬화. 양방향 직렬화로 인한 속도 저하와, 메모리 사용량이 컸다.

  • 현재
    : 객체 전달 시 구조화된 복제 알고리즘을 통해 복사한다.
    객체←→문자열 변환은 줄일 수 없지만 메모리에 사본을 둘 수 있다는 장점이 있다.

트랜스퍼러블 객체

데이터 세트 규모가 방대할 경우 사용을 고려할 수 있다.
데이터는 그대로 두고 객체의 소유권만 양방향으로 전송한다.
어떤 객체가 워커가 되면, 원래 위치에서 접근할 수 없으므로 공유 스코프에서의 위험 요소를 제거할 수 있다.

Transferable objects are instances of classes like ArrayBuffer, MessagePort or ImageBitmap objects can be transferred.

    postMessage(aMessage, transferList);

    // Unit8Array Example
    postMessage(foo.buffer, [ foo.buffer ] );

1.3 공유 워커

공유 워커(shared worker)는 여러 스크립트에서 공통 접근할 수 있다.
이 스크립트는 windows, iframe, 그리고 worker까지 포함된다.

  • port 객체

    공유 워커는 여러 스크립트와 연결 가능하므로 메시지의 출처를 알기 위해 port라는 식별자가 존재한다.

    var myWorker = new SharedWorker("worker.js");

    myWorker.port.start(); // port 커넥션을 여는 메소드
  • connection 이벤트 처리

    connect라는 이벤트에서 특정 연결에 관한 port 객체를 제공한다.

          addEventListener("connect", function(ev) {
              var port = ev.ports[0];
    
              port.addEventListener("message", function (ev) {
                      port.postMessage( ... )
              }
    
              port.start();
          }
    
    • 공유 워커는 port 부분만 차이가 있으며, 나머지 기능은 전용 워커와 동일하다.
    • 전용 워커는 자신을 초기화한 프로그램이 종료되면 같이 사라지지만, 공유 워커는 열려 있는 포트가 있다면 지속된다.

1.4 폴리필

워커는 API이므로 어느정도 폴리필이 가능하다.
일반적으로, 워커를 지원하지 않는 브라우저는 멀티스레딩이 어렵다.
JS의 비동기성 근원은 이벤트 루프 큐 이므로, 타이머로 워커를 강제 조작함으로써 비동기성을 주는 방법이 있다.

2. SIMD (Single Instruction Multiple Data)

: 한번에 여러 데이터를 명령어 하나로 처리하는 방식

SIMD는 데이터 병행성(Data Parallelism)을 나타내는 형식으로
웹 워커의 작업 병행성(Task Parallelism)과는 반대 개념이다.

즉, 프로그램 로직을 병렬 실행하는 게 아니라 여러 데이터 비트를 병렬로 처리한다.
CPU가 숫자 벡터와 모든 숫자에 병렬 연산이 가능한 명령어 세트를 이용하여 SIMD를 제공한다.

2018년 3월 기준 SIMD.js는 WebAssembly를 위한 SIMD제안이 진행됨에 따라 Candidate 단계에서 제거되었다. (참고 : NAVER D2 )

3. asm.js

최적화에 적합한 형태를 가진 자바스크립트의 부분집합
asm.js는 특히 C/C++ 코드를 웹으로 포팅하는 데 유용하다

구체적인 명세나 구문이 제안된 상태는 아니다.
asm.js 규칙에 부합하는 현 자바스크립트 구문을 인식하고, 엔진이 어떻게 최적화 할지 방향을 제시한다.

초창기 버전에선 use strict와 같이 use asm 과 같은 지시자를 쓰도록 했음
개발자가 use같은 명시를 하지 않아도 휴리스틱하게 자동 최적화를 해야 한다는 주장도 있다.

3.1 asm.js 최적화

  • 기본적으로 다음 방법을 통해 JS의 타입 추론 및 강제변환에 걸리는 시간을 줄인다.

    1. 변수,연산 타입 추론에 힌트를 준다.
    2. 강제 변환 추적 단계를 건너뛰도록 한다
  • 정수 타입으로 강제하는 예시

      // 일반적인 핟랑 구문
      var a = 42;
      var b = a;
    
      // asm.js 최적화
      var a = 42;
      var b = a | 0; // 값을 정수로 강제하기 위해 0을 OR연산
    

4. 정리

비동기 코딩 패턴을 통한 성능 향상

근본적으로 단일 이벤트 루프 스레드에 묶여있다는 한계가 존재한다.

프로그램 수준의 성능 개선 3가지

  1. 웹 워커

    비동기 이벤트를 이용하여 스레드간 메시지를 교환하여 JS 파일을 개별 스레드단위로 실행
    장점 : 메인 UI 스레드의 응답성을 높이면서도 긴 시간이 걸리거나 자원 소모가 큰 작업을 분산시킴

  2. SIMD

    CPU 수준의 병렬 연산에 특화된 JS API로 연결짓기

  3. asm.js

    최적화하기 어려운 영역을 피해 JS 엔진이 해당 부류의 코드를 자동 인식하고, 공격적인 최적화를 하도록 유도하는 부분 집합을 말함

    직접 작성할 수 있지만 어셈블리만큼 지루하고 실수하기 쉬움
    요지는 다른 최적화된 프로그래밍 언어에서 크로스 컴파일할 대상으로 한다는 것