자바스크립트

[번역] 웹어셈블리의 현재 위치와 미래

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 마지막 글인 Where is WebAssembly now and what’s next?의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래 (현재글)

 

2월 28일, 4개의 주요 브라우저들은 웹어셈블리의 MVP가 완료되었다는 합의에 이르렀다고 발표했다. 이는 브라우저들이 탑재를 시작할 수 있는 최초의 안정적인 버전을 제공한다.

logo_party01-500x169.png

이는 브라우저가 탑재할 수 있는 안정적인 코어를 제공한다. 이 코어는 커뮤니티 그룹이 계획하고 있는 모든 기능을 포함하고 있지는 않지만, 웹어셈블리를 빠르고 사용가능하도록 만들기에는 충분하다.

이것을 이용하면, 개발자들은 웹어셈블리 코드를 탑재할 수 있다. 이전 버전의 브라우저를 위해서 개발자들은 asm.js 버전의 코드를 내려보낼 수 있다. asm.js는 자바스크립트의 서브셋이기 때문에, 모든 JS 엔진이 이를 실행할 수 있다. Emscripten을 이용하면 동일한 앱을 웹어셈블리와 asm.js로 컴파일 할 수 있다.

최초 버전임에도 불구하고, 웹어셈블리는 빠를 것이다. 하지만 수정과 새로운 기능들이 더해지면, 앞으로는 더 빨라질 것이다.

브라우저에서 웹어셈블리 성능 향상시키기

브라우저들이 엔진상의 웹어셈블리 지원을 개선할수록 속도는 더 빨리질 것이다. 브라우저 벤더들이 이러한 이슈들을 위해 독자적으로 노력하고 있다.

JS와 웹어셈블리 간의 더 빠른 함수 호출

현재 JS 코드에서 웹어셈블리 함수를 호출하는 것은 필요 이상으로 느리다. 이는 트램폴린(trampolining)”이라는 것이 필요하기 때문이다. JIT는 웹어셈블리를 직접 어떻게 다루어야 할 지 모르기 때문에, 그 역할을 하는 것에게 웹어셈블리를 전달해야 한다. 이것은 엔진 내부에 있는 코드중 느린 부분인데, 최적화된 웹어셈블리 코드를 실행하기 위한 셋업 작업을 한다.

06-02-trampoline01-500x399.png

이 작업은 JIT가 웹어셈블리를 직접 어떻게 다룰지 아는 경우에 비해 100배 이상 느릴 수 있다.

만약 당신이 웹어셈블리 모듈에게 하나의 큰 작업만 넘겨준다면 이러한 오버헤드를 눈치채지 못할 수도 있다. 하지만 만약 웹어셈블리와 JS간에 빈번한 교류가 발생한다면(작은 여러개의 작업을 이용해서) 이 오버헤드가 두드러질 것이다.

더 빠른 로딩 시간

JIT는 더 빠른 로딩 시간과 더 빠른 실행 시간 사이의 트레이드-오프를 관리해야 한다. 만약 당신이 미리 컴파일하고 최적화하는 데에 더 많은 시간을 사용한다면, 실행 시간은 빨라질 수 있겠지만, 초기 구동 시간은 느려지게 된다.

코드가 한 번 실행된 후에 더 이상 끊김이 없도록 보장하기 위해 미리 컴파일을 하는 것과 대부분의 코드는 충분히 자주 실행되지 않기 때문에 최적화하는 것이 큰 의미가 없다는 사실 사이의 균형을 유지하기 위해 많은 작업들이 진행되고 있다.

웹어셈블리는 어떤 타입들이 사용될 지 추측할 필요가 없기 때문에, 실행 시점에 어떤 타입들이 사용되는지를 엔진이 모니터링할 필요가 없다. 이는 엔진에게 더 많은 선택지를 주는데, 예를 들면 컴파일을 실행과 병행해서 진행할 수도 있다.

게다가, 최근에 추가된 자바스크립트 API는 스트림을 이용해서 웹어셈블리를 컴파일할 수 있도록 해 준다. 즉, 바이트를 다운로드 받는 동안에 엔진이 컴파일을 시작할 수 있게 된다.

파이어폭스에서 우리는 두가지의 컴파일러 시스템을 작업하고 있다. 하나는 미리 실행되어 꽤 괜찮은 수준까지 코드를 최적화한다. 코드가 실행되는 동안에는 다른 컴파일러가 백그라운드에서 완벽하게 최적화를 한다. 완전히 최적화된 코드가 준비되면 기존 코드와 교체되어 들어간다.

MVP 이후 스펙에 추가될 기능들

웹어셈블리의 목표 중 하나는 모든 것을 한 번에 디자인하는 것보다는, 작은 조각으로 나누어 구체화하면서 계속해서 테스트하는 것이다.

즉, 기대되는 기능들이 있긴 하지만, 이들이 미리 100% 완벽하게 구체화되지는 않았다는 의미이다. 이들은 모든 브라우저 벤더들이 참여해서 스펙화하는 과정을 거쳐야만 할 것이다.

이 기능들은 미래 기능(future features)이라 불린다. 몇 가지만 살펴보겠다. 

DOM 직접 다루기

현재로선 DOM과 상호작용할 수 있는 방법이 존재하지 않는다. 즉, 웹어셈블리에서는  element.innerHTML과 같은 식으로 노드를 갱신할 수 없다는 의미이다. 

대신에, JS를 통해서 값을 설정하도록 해야 한다. 이 말은 자바스크립트 호출자(caller)에게 값을 다시 전달한다는 의미가 될 수도 있다. 아니면, 웹어셈블리 내부에서 자바스크립트 함수를 호출한다는 의미가 될 수도 있다. (자바스크립트와 웹어셈블리 함수 모두 웹어셈블리 모듈에서 임포트해서 사용할 수 있다)

06-03-dom01-500x418.png

어느 방법이든, 자바스크립트를 통하는 것은 직접 접근하는 것보다는 느릴 것이다. 일부 웹어셈블리 어플리케이션들은 이 문제가 해결될 때까지 기다려야 할 수도 있을 것이다.

공유 메모리의 동시성

코드의 속도를 향상시키는 한가지 방법은 코드의 다른 부분들이 동시에 병렬적으로 실행될 수 있게 만드는 것이다. 하지만 이 방법은 가끔 역효과를 일으키는데, 쓰레드간의 커뮤니케이션으로 인한 오버헤드가 원래 하려는 작업보다 더 많은 시간이 걸릴 수 있기 때문이다.

하지만 쓰레드간에 메모리를 공유할 수 있다면 이러한 오버헤드를 줄일 수 있다. 이를 위해 웹어셈블리는 자바스크립트에서 새롭게 추가된 SharedArrayBuffer를 사용할 것이다. 이 기능이 브라우저에서 제공되기 시작하면 웹어셈블리가 어떻게 이를 다룰지에 대해 작업 그룹이 스펙을 구체화할 수 있을 것이다.

SIMD

만약 웹어셈블리에 대한 다른 글을 읽거나 발표를 본 적이 있다면, 아마도 SIMD 지원에 대해서 들어보았을 것이다. SIMD는 단일 명령, 다중 데이터 (single instruction, multiple data)의 약자이다. 이는 병렬 실행을 위한 또다른 방법이다.

SIMD는 여러 수들로 이루어진 벡터와 같은 대량의 데이터 구조를 취해서, 각기 다른 부분에 동일한 명령을 동시에 적용할 수 있도록 해 준다. 이 방식으로 게임이나 VR에 필요한 복잡한 연산에 대한 성능을 급격하게 향상시킬 수 있다.

보통의 웹 앱 개발자들에게는 이것이 크게 중요하지 않을 것이다. 하지만 게임과 같은 멀티미디어 관련 개발자들에게 이 기능은 아주 중요하다.

예외 처리

C++과 같은 언어들의 많은 코드 베이스는 예외(exception)를 사용한다. 하지만 예외는 웹어셈블리의 일부로써 정의되어 있지 않다.

만약 Emscripten을 이용해 코드를 컴파일한다면, 컴파일러 최적화 단계에서 예외 처리를 에뮬레이트 해 줄 것이다. 하지만 꽤나 느리기 때문에, DISABLE_EXCEPTION_CATCHING 플래그를 이용해서 이 기능을 끄고 싶을 수도 있을 것이다. 

웹어셈블리에서 직접 예외 처리를 지원하게 된다면 이러한 에뮬레이션은 더이상 필요없을 것이다.

그 외에 개발자 편의를 위한 개선들

몇가지 미래 기능들은 성능에 영향을 끼치지는 않지만, 개발자들이 웹어셈블리를 다루기 쉽게 만들어 줄 것이다.

  • 일급 소스 레벨 개발자 도구. 현재 브라우저에서 웹어셈블리를 디버깅 하는 것은 순수 어셈블리를 디버깅하는 것이나 마찬가지일 것이다. 하지만 아주 소수의 개발자들만이 머릿속에서 자신의 코드를 어셈블리로 맵핑할 수 있다. 우리는 개발자들이 소스코드를 디버깅하는 것을 지원하는 도구를 어떻게 개선할 수 있을지 조사하고 있다.
  • 가비지 컬렉션. 만약 타입을 미리 정의할 수 있다면, 그 코드를 웹어셈블리로 변환할 수도 있어야만 한다. 그러므로 타입스크립트 등을 이용한 코드는 웹어셈블리와 호환 가능해야만 한다. 하지만 현재 딱 한가지 걸림돌이 있는데, 그것은 바로 웹어셈블리가 JS 엔진에 내장된 것과 같은 기존의 가비지 컬렉터와 어떻게 상호작용해야 하는지를 모른다는 점이다. 이 미래 기능의 아이디어는, 웹어셈블리에게 내장된 GC에 대한 일급 접근 권한과 하위 레벨 GC 원시 타입 및 연산을 제공하는 것이다.
  • ES6 모듈 통합. 브라우저는 현재 script 태그를 이용해서 자바스크립트 모듈을 로딩할 수 있도록 지원을 추가하고 있다. 이 기능이 추가된 후에는 <script src=url type=module> 같은 태그에서 url이 웹어셈블리 모듈을 가리킨다고 해도 동작할 수 있을 것이다.

결론

웹어셈블리는 지금도 빠르지만, 브라우저가 새로운 기능과 개선 사항을 구현하고 나면 더욱 더 빨라질 것이다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 웹어셈블리는 왜 빠를까?

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 다섯 번째 글인 What makes WebAssembly fast?의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까? (현재글)
  6. 웹어셈블리의 현재 위치와 미래

 

이전 글에서 나는 웹어셈블리와 자바스크립트는 양자택일의 문제가 아니라는 점을 설명했다. 우리는 너무 많은 개발자들이 웹어셈블리만을 이용해 코드를 작성하기를 바라지 않는다.

그러므로 개발자들이 어플리케이션을 개발할 때 웹어셈블리와 자바스크립트 사이에서 하나를 선택할 필요는 없다. 다만, 자바스크립트 코드의 일부를 웹어셈블리로 교체할 수는 있을 것이다.

예를 들어, 리액트를 개발하고 있는 팀은 그들의 리콘사일러(reconciler) 코드 (일명 Virtual DOM) 를 웹어셈블리 버전으로 교체할 수 있을 것이다. 리액트를 사용하는 사람들은 아무것도 할 필요가 없다. 그들의 앱은 웹어셈블리의 장점을 취한다는 것 빼고는 이전과 정확하게 똑같을 것이다.

리액트 팀과 같은 개발자들이 이런 식의 교체를 할 만한 이유는 바로 웹어셈블리가 빠르기 때문이다. 그런데, 웹어셈블리는 왜 빠른걸까?

오늘날의 자바스크립트 성능은 어떨까?

자바스크립트와 웹어셈블리의 성능차이를 이해하려면, 먼저 자바스크립트 엔진이 하는 일을 이해해야 한다.

이 다이어그램은 오늘날 어플리케이션의 시작(start-up) 성능을 간략하게 보여준다.

자바스크립트 엔진이 각각의 작업들을 진행할 때 걸리는 시간은 해당 페이지가 사용하는 자바스크립트에 따라 다를 것이다. 이 다이어그램은 정확한 성능 수치를 나타내지 않는다. 대신, 같은 기능을 하는 자바스크립트와 웹어셈블리가 어떤 성능차이를 갖는지에 대한 고차원의 모델을 의미한다.

05-01-diagram_now01-500x129.png

각각의 막대는 특정 작업을 할 때 걸리는 시간을 나타낸다.

  • 구문해석 (Parsing) – 소스코드를 인터프리터가 실행할 수 있는 형태로 가공하는데 걸리는 시간
  • 컴파일 + 최적화 – 기본(baseline) 컴파일러와 최적화(optimizing) 컴파일러에서 걸리는 시간. 일부 최적화 컴파일러의 작업은 메인 스레드에서 진행되지 않기 때문에 여기에서 제외된다.
  • 재-최적화 (Re-optimizing) – JIT(Just in time Compiler)가 그들의 가정이 틀렸을 때 재조정하는데 걸리는 시간. 이는 다시 최적화를 하거나, 기본코드로 되돌리는 작업을 모두 포함한다.
  • 실행 – 코드를 실행하는 데 걸리는 시간
  • 가비지 컬렉션 – 불필요한 메모리를 비우는 데 걸리는 시간

한가지 중요한 사실 : 이 작업들은 구분된 단위를 갖거나 특정한 순서로 진행되지 않고, 서로 맞물려서 진행된다. 구문 해석이 일부 진행된 후, 일부 실행, 일부 컴파일, 다시 일부 구문 해석, 일부 실행 등의 형태로 진행된다.

이렇게 작은 단위로 분해함으로써, 다음 다이어그램과 같았던 초기 자바스크립트의 성능으로부터 큰 개선을 이룰 수 있었다.

05-02-diagram_past01-500x147.png

단순히 자바스크립트를 실행하는 인터프리터였던 초기에는 실행속도가 상당히 느렸다. JIT가 도입된 이후로 실행속도는 급격하게 증가했다.

트레이드-오프는, 코드를 감지하고 컴파일할 때 부하가 발생한다는 점이다. 만약 자바스크립트 개발자들이 지금까지 해왔던 방식으로만 코드를 작성한다면 구문해석과 컴파일 시간은 아주 작을 것이다. 하지만 자바스크립트의 성능이 개선될수록 개발자들은 자바스크립트로 더 복잡한 어플리케이션을 개발하게 될 것이다. 즉, 좀 더 개선할 여지가 있다는 의미이다.

웹어셈블리는 어떻게 다를까?

아래 다이어그램은 웹어셈블리가 전형적인 웹 어플리케이션과 어떻게 다른지를 보여준다.

05-03-diagram_future01-500x214.png

각각의 단계를 어떻게 다루는지는 브라우저마다 조금씩 다르다. 여기서는 SpiderMonky를 모델로 설명하겠다.

가져오기 (Fetching)

다이어그램에는 나오지 않았지만, 단순히 서버에서 파일을 가져오는 일에도 시간이 소모된다.

웹어셈블리는 자바스크립트보다 더 간결하기 때문에 데이터를 가져오는 속도가 더 빠르다. 압축 알고리즘이 자바스크립트 번들 파일의 사이즈를 상당히 줄일 수 있을지는 몰라도, 압축된 바이너리 형태의 웹어셈블리가 여전히 더 작을 것이다.

즉, 서버에서 클라이언트로 전송하는 시간이 더 적게 걸리게 된다. 느린 네트워크 환경에서는 더 의미가 있을 것이다.

구문해석 (Parsing)

자바스크립트 소스가 브라우저에 전달되게 되면, 추상 구문 트리(AST : Abstract Syntax Tree)로 변환된다.

브라우저는 종종 이 작업을 지연해서 실행하는데, 초기 실행에 필요한 부분만 먼저 해석하고 아직 호출되지 않은 함수들은 스텁(stub)만 만들어 둔다. 이때 AST는 중간 표현 형식(바이트코드라고 불리는)으로 변환되는데, 이는 각 자바스크립트 엔진에 따라 다르다. 반면, 웹어셈블리는 그 자체로 이미 중간 표현 형식이기 때문에 이 변환 작업을 거칠 필요가 없다. 웹어셈블리는 단지 해독된(decoded) 이후에 에러가 없는지만 검증되면(validated) 된다.

05-04-diagram_compare02-500x169.png

컴파일 + 최적화

JIT에 대한 글에서 설명했듯이, 자바스크립트는 코드를 실행하는 도중에 컴파일된다. 런타임에 어떤 타입이 사용되는지에 따라 동일한 코드가 다양한 버전으로 컴파일되기도 한다.

브라우저들은 웹어셈블리를 각자 다른 방식으로 컴파일한다. 일부 브라우저들은 실행하기 전에 기본 컴파일 단계를 거치고, 다른 브라우저들은 JIT를 사용한다.

어떤 방식이든, 웹어셈블리는 기계 코드와 훨씬 가까운 상태에서 시작한다. 예를 들면 프로그램의 일부로써 타입이 포함되어 있는데, 이는 다음의 몇가지 이유로 더 빠르다.

  1. 최적화된 코드를 컴파일하기 전에, 어떤 타입이 사용되었는지 확인하기 위해 코드를 실행해볼 필요가 없다.
  2. 같은 코드에서 대해서 여러가지 타입들이 사용되고 있을 때, 여러가지 버전으로 컴파일할 필요가 없다.
  3. LLVM 에서 이미 많은 최적화가 진행된 상태이기 때문에, 컴파일이나 최적화에 필요한 작업이 적다.

05-05-diagram_compare03-500x175.png

재-최적화 (Reoptimizing)

JIT는 가끔 최적화된 버전을 버리고, 다시 최적화해야한다.

이는 JIT가 코드 실행에 기초하여 만들어낸 가정이 잘못된 것으로 판명될 때 발생한다. 예를 들어 순환문 내부에서 사용되는 변수에 이전 주기와는 다른 값이 할당되거나, 프로토타입 체인내부에 다른 함수가 추가되면, 역최적화(deoptimization)가 발생한다.

역최적화에는 두가지 비용이 따른다. 첫째로, 최적화된 코드를 내버리고 기본 코드로 되돌릴 때 시간이 소모된다. 둘째로, 해당 함수가 여전히 많은 빈도로 호출되고 있다면 JIT는 해당 함수를 다시 최적화 컴파일러에게 보내기로 결정할 수도 있는데, 여기에서 컴파일을 두 번 하는 것에 따른 비용이 발생한다.

웹어셈블리에서는 타입과 같은 것들이 명시적이기 때문에, JIT가 런타임에 데이터를 수집해서 타입을 추론할 필요가 없다. 즉, 재-최적화 단계가 필요없다는 의미이다.

05-06-diagram_compare04-500x201.png

실행

자바스크립트가 빠르게 실행될 수 있도록 작성하는 것은 가능하다. 이를 위해서는 JIT가 어떻게 최적화하는지에 대해서 알고 있어야 한다. 예를 들어 JIT에 관한 예전 글에서 설명했던 것 처럼, 컴파일러가 타입을 특수화(type specialize : 타입이 항상 동일하다고 가정하고 코드를 최적화하는 과정)를 하도록 만드려면 어떻게 코드를 작성해야하는지를 알아야 한다.

하지만, 대부분의 개발자들은 JIT 내부에 대해서 알지 못한다. 심지어 JIT 내부에 대해서 알고 있는 개발자들도, 정확한 개선 포인트를 알기는 쉽지 않다. 코드를 읽기 쉽게 만들기 위해 사용되는 코딩 패턴들(공통 작업을 추상화해서 타입에 관계없이 동작하는 함수로 만드는 작업 등)은 컴파일러가 코드를 최적화하는 것을 방해한다.

게다가 JIT가 사용하는 최적화는 브라우저마다 달라서, 특정 브라우저에 맞추어 코딩을 하면 다른 브라우저에서는 성능이 느릴수도 있다.

이러한 이유로, 웹어셈블리로 작성된 코드를 실행하는 것은 일반적으로 더 빠르다. 자바스크립트를 위해 JIT가 하는 많은 최적화 작업들(타입 특수화와 같은)은 웹어셈블리에서는 전혀 필요없다.

뿐만 아니라 웹어셈블리는 컴파일러를 목표로 디자인되었다. 즉, 인간 프로그래머가 작성하는 용도가 아닌, 컴파일러가 생성해내는 용도로 디자인 되었다는 의미이다.

인간 프로그래머가 직접 프로그래밍할 필요가 없기 때문에, 웹어셈블리는 기계에게 더 이상적인 명령(instruction) 셋을 제공할 수 있다. 코드가 어떤 종류의 작업을 하느냐에 따라 이러한 명령들은 10% 부터 800% 까지 더 빠를 수 있다.

05-07-diagram_compare05-500x171.png

가비지 컬렉션

자바스크립트에서 개발자들은 더이상 필요없는 오래된 변수들을 메모리에서 제거하는 작업에 대해 신경쓸 필요가 없다. 대신에 자바스크립트 엔진이 가비지 컬렉터를 이용해 이러한 작업을 자동으로 해 준다.

하지만 만약 예측가능한 성능을 원한다면, 이는 문제가 될 수도 있다. 가비지 컬렉터가 언제 작동할 지 제어할 수 없기 때문에, 안좋은 타이밍에 실행될 지도 모른다. 대부분의 브라우저들은 이를 잘 스케쥴링해 주고 있지만, 여전히 코드 실행을 방해하는 요인이 될 수도 있다.

최소한 현재까지는, 웹어셈블리는 가비지 컬렉션을 전혀 지원하지 않는다. 메모리는 수동으로 (C나 C++와 같이) 관리된다. 이로 인해 개발자들이 프로그래밍 하기는 더 어려울 수도 있지만, 성능에 있어서는 더 안정된 결과를 만들어낼 수 있을 것이다.

05-08-diagram_compare06-500x204.png

결론

웹어셈블리는 많은 경우에 자바스크립트보다 더 빠르다. 왜냐하면 :

  • 웹어셈블리 코드가 자바스크립트보다 더 간결하기 때문에 (압축된 경우에도 마찬가지), 코드를 가져오는 데에 더 적은 시간이 걸린다.
  • 웹어셈블리 코드를 해독(decode)하는 시간이 자바스크립트의 구문을 해석 (parse) 하는 시간보다 적게 걸린다.
  • 웹어셈블리 코드는 자바스크립트 코드보다 더 머신 코드에 가깝고, 서버단에서 미리 최적화가 되어 있기 때문에 컴파일하고 최적화하는 시간이 적게 걸린다.
  • 웹어셈블리는 타입이나 다른 정보가 미리 내장되어 있기 때문에, 자바스크립트 엔진이 실행시점에 분석할 필요가 없어서 재-최적화하는 시간이 필요없다.
  • 웹어셈블리 코드는 성능을 위해 컴파일러를 느리게 만드는 요인들에 대해 개발자들이 미리 알고 있을 필요가 없으며, 머신에 적합한 명령어 셋을 갖고 있기 때문에 실행시간도 주로 더 적게 걸린다.
  • 메모리를 직접 관리하기 때문에 가비지 컬렉션이 필요없다.

이것이 바로 같은 작업을 할 때 웹어셈블리가 자바스크립트보다 더 좋은 성능을 내는 이유이다.

몇몇 경우에는 기대한 것 만큼 웹어셈블리가 빠르지 않을 때도 있지만, 머지않아 이를 개선할 변화가 있을 것이다. 다음 글에서 이런 내용들을 다루도록 하겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 웹어셈블리 모듈의 생성과 동작

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 네 번째 글인 Creating and working with WebAssembly modules의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작 (현재글)
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

웹어셈블리는 자바스크립트가 아닌 프로그래밍 언어를 웹페이지에서 실행하기 위한 방법이다. 예전에는 웹페이지 내의 다른 부분들과 상호작용하기 위해 브라우저에서 코드를 실행시키기 위한 유일한 선택지가 자바스크립트였다.

그래서 사람들이 웹어셈블리가 빠르다고 이야기할 때, 유일한 비교대상은 자바스크립트다. 하지만 그것이 웹어셈블리와 자바스크립트 중에서 하나만 선택해서 사용해야 한다는 의미는 아니다.

사실, 우리는 개발자들이 같은 어플리케이션 내에서 웹어셈블리와 자바스크립트를 모두 사용하길 바란다. 심지어 웹어셈블리를 직접 작성하지 않더라도, 그 이점을 누릴 수 있다.

웹어셈블리 모듈은 자바스크립트에서 사용될 수 있는 함수들을 정의한다. 그러므로 오늘날 npm 을 이용해서 lodash와 같은 모듈을 다운로드 받아서 API 에 있는 함수를 호출하듯이, 나중에는 웸어셈블리 모듈을 다운로드 받을 수 있게 될 것이다.

그러면, 웹어셈블리 모듈을 어떻게 생성할 수 있고, 어떻게 자바스크립트에 이용할 수 있는지를 살펴보자.

웹어셈블리는 어디에 어울릴까?

어셈블리에 대해 설명한 글에서, 나는 컴파일러가 상위 레벨 프로그래밍 언어들을 어떻게 읽어들여서 기계 코드로 번역하는지에 대해서 이야기했다.

04-01-langs09-500x306.png

이 그림에서 웹어셈블리는 어떤 역할을 할까?

어쩌면 웹어셈블리가 또 하나의 대상 어셈블리 언어라고 생각할지도 모르겠다. 어느 정도는 사실이지만, 이들 각각의 언어들 (x86, ARM)은 특정한 기계의 아키텍쳐와 연관된다는 점에서 차이가 있다.

사용자의 기계에서 실행될 코드를 웹을 통해서 전달할 때, 당신은 코드가 실행될 대상 아키텍쳐가 무엇인지를 알 수가 없다.

그러므로 웹어셈블리는 다른 종류의 어셈블리와는 약간 다르다. 웹어셈블리는 실제의 물리적인 기계가 아닌 개념상의 기계를 위한 언어이다.

이러한 이유로, 웹어셈블리의 명령(instruction)들은 가끔 가상 명령(virtual instruction)이라고도 불린다. 이들은 자바스크립트 코드보다는 훨씬 더 기계 코드와 직접적으로 맵핑된다. 이들은 공통적으로 널리 쓰이는 하드웨어에서 효과적으로 사용될 수 있는 일종의 교집합을 나타낸다. 하지만 이들이 특정한 하드웨어를 위한 특정 기계 코드에 직접적으로 맵핑되는 것은 아니다.

04-02-langs08-500x326.png

브라우저는 웹어셈블리를 다운로드한다. 그러면 브라우저는 웹어셈블리로부터 대상 기계의 어셈블리 코드를 위한 짧은 홉(hop)을 만들어낼 수 있다.

.wasm 으로 컴파일하기

현재 웹어셈블리를 가장 잘 지원하는 컴파일러 도구 모음(toolchain)은 LLVM이라 불린다. 많은 수의 프론트엔드와 백엔드가 LLVM으로 변환이 가능하다.

Note: 대부분의 웹어셈블리 모듈 개발자들은 C나 Rust 등의 언어들로 개발한 후 웹어셈블리로 컴파일하지만, 웹어셈블리 모듈을 생성하기 위한 다른 방법도 있다. 예를 들면, 타입스크립트를 이용해 웹어셈블리 모듈을 생성할 수 있는 실험적인 도구도 존재하고, 직접 텍스트 형태로 코드를 작성할 수도 있다.

우리가 C를 이용해 웹어셈블리를 작성한다고 가정하자. 우리는 clang 프론트엔드를 이용해서 C를 LLVM 중간 표현 방식으로 변환할 수 있을 것이다. LLVM의 IR로 변환하고 나면, LLVM이 그것을 이해할 수 있으므로 LLVM이 몇가지 최적화를 할 수 있게 된다.

LLVM의 IR(intermidiate representation)에서 웹어셈블리로 변환하기 위해서 우리는 백엔드가 필요하다. 현재 LLVM 프로젝트에서 하나가 진행되고 있다. 이 백엔드는 대부분의 사항이 완료되었고, 곧 완성될 예정이다. 하지만, 지금 당장 사용하기에는 약간 어려울 수도 있다.

지금 당장 사용하기에 좀더 쉬운 Emscripten 이라는 또다른 도구도 있다. 이는 자신만의 백엔드를 갖고 있는데, 이 백엔드는 또다른 대상 (asm.js)으로 컴파일한 후에 그것을 웹어셈블리로 변환하는 방식으로 웹어셈블리를 생성할 수 있다. 하지만 내부적으로는 LLVM을 사용하고 있기 때문에, Emscripten를 이용하면 두개의 백엔드 사이를 전환할 수 있다.

04-03-toolchain07-500x411.png

Emscripten은 전체 C/C++ 코드베이스를 변환할 수 있게 해 주는 많은 추가적인 도구와 라이브러리를 포함하기 때문에, 컴파일러라고 하기 보다는 소프트웨어 개발자 킷 (SDK: Software Developer Kit)에 가깝다. 예를 들어 시스템 개발자들은 주로 쓰기와 읽기를 할 수 있는 파일시스템을 갖고 있는데, Emscripten은 IndexedDB를 이용해서 파일 시스템을 시뮬레이트할 수 있다.

당신이 어떤 도구 모음을 사용했는지와는 관계 없이, 최종 결과는 .wasm 으로 끝나는 파일이 된다. 아래에서 .wasm의 구조에 대해 좀 더 설명하도록 하겠다. 먼저 이것을 자바스크립트에서 어떻게 사용할 수 있는지부터 살펴보자.

자바스크립트에서 .wasm 모듈 로딩하기

.wasm 파일은 웹어셈블리 모듈이며, 자바스크립트에서 로드될 수 있다. 이 시점부터 로딩 과정은 약간 복잡해진다.

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =&amp;amp;amp;amp;amp;amp;gt;
    response.arrayBuffer()
  ).then(bytes =&amp;amp;amp;amp;amp;amp;gt;
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =&amp;amp;amp;amp;amp;amp;gt;
    results.instance
  );
}

여기에 대한 더 깊은 설명은 우리 문서에서 확인할 수 있다.

우리는 이 과정을 좀더 쉽게 만들기 위해 작업하고 있다. 우리는 도구 모음을 좀더 발전시키고, 현존하는 webpack과 같은 모듈 번들러나 SystemJS와 같은 로더들을 통합할 수 있게 되길 기대한다. 우리는 웹어셈블리 모듈을 로딩하는 것이 자바스크립트 모듈을 로딩하는 것만큼 쉬워질 수 있다고 믿는다.

하지만, 웹어셈블리 모듈과 JS 모듈 사이에는 중요한 차이점이 있다. 현재, 웹어셈블리의 함수들은 파라미터나 반환값으로 오직 숫자형(정수 혹은 부동소수)만을 사용할 수 있다.

04-04-memory04-500x93.png

문자열과 같은 더 복잡한 데이터 형을 위해서는, 웹어셈블리 모듈의 메모리를 사용해야만 한다.

만약 당신이 대부분의 작업을 자바스크립트로 해 왔다면, 메모리에 직접 접근하는 것이 그리 익숙하지는 않을 것이다. C나 C++ 혹은 Rust와 같은 더 고성능의 언어들은 주로 메모리를 직접 관리한다. 웹어셈블리 모듈의 메모리는 이러한 언어들에서 찾을 수 있는 힙(heap)을 시뮬레이트한다.

이를 위해서, 웹어셈블리 모듈은 자바스크립트에서 배열 버퍼라 불리는 것을 사용한다. 배열 버퍼는 바이트의 배열이다. 그러므로 배열의 인덱스들은 메모리의 주소의 용도로 사용된다.
당신이 자바스크립트와 웹어셈블리 사이에서 문자열을 전달하려고 한다면, 해당 문자들을 동등한 문자 코드로 변환해야만 한다. 그리고 그 코드를 메모리 배열에 저장한다. 인덱스들이 정수형이기 때문에 인덱스 하나를 웹어셈블리 함수로 전달할 수 있다. 그러므로 해당 문자열의 첫번째 문자에 해당하는 인덱스가 포인터로써 사용될 수 있다.

04-05-memory12-500x400.png

아마 웹개발자들에 의해 사용될 웹어셈블리 모듈을 개발하는 사람은 누구나 해당 모듈 주변에 래퍼를 생성해 줄 것이다. 그렇게 되면 모듈을 사용하는 입장에서는 메모리 관리에 대해 알 필요가 없게 된다.
만약 좀더 배우고 싶다면, 웹어셈블리의 메모리 다루기에 대한 우리의 문서를 확인해보기 바란다.

.wasm 파일의 구조

만약 당신이 상위 레벨 언어로 코드를 작성한 후에 웹어셈블리로 컴파일한다면, 웹어셈블리 모듈이 어떻게 구성되는지에 대해서는 알 필요가 없다. 하지만 기본을 이해하는 것은 도움이 될 것이다.

아직 읽어보지 않았다면, 어셈블리에 대한 글을 읽어보길 권한다. (이 시리즈의 3부)
다음은 웹어셈블리로 변환할 C 함수이다.

int add42(int num) {
  return num + 42;
}

WASM Explorer를 이용하면 이 함수를 컴파일 할 수 있다.

.wasm 파일을 열어보면 (만약 에디터가 지원한다면), 다음과 같은 것을 보게 될 것이다.

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

이것은 모듈의 “바이너리” 표현 방식이다. 따옴표로 강조한 이유는 보통은 16진법으로 표시되기 때문인데, 이는 쉽게 바이너리 형태나 인간이 읽을 수 있는 형태로 변환이 가능하다.

예를 들어 num + 42 는 다음과 같을 것이다.

04-06-hex_binary_asm01-500x254.png

코드의 작동 방식 : 스택 기계

궁금해할 것 같아서 설명하자면, 이들 명령들은 아래와 같이 동작할 것이다.

04-07-hex_binary_asm02-500x175.png

그림 번역 :
get_local 0 -> 첫번째 파라미터의 값을 읽어서 스택에 추가한다
i32.const 42 -> 상수값을 스택에 추가한다
i32.add -> 스택의 최상위에 있는 두 값을 더해서 결과값을 스택에 추가한다

add 연산이 어디에서 값을 가져와야 하는지에 대해서는 전혀 언급이 없는 것을 눈치챘을지도 모르겠다. 그 이유는 웹어셈블리가 스택 기계라고 불리는 것의 일종이기 때문이다. 이는 한 연산이 실행되기 전에 그 연산이 필요로 하는 모든 값들이 스택에 쌓여 있다는 의미이다.

add와 같은 연산들은 스스로 몇 개의 값이 필요한지를 알고 있다. add는 두 개를 필요로 하기 때문에, 스택의 최상단에서 두 개의 값을 가져올 것이다. 이는 add 명령이 출처나 대상 레지스터를 지정할 필요가 없으므로 short 형(단일 바이트)이 될 수 있다는 것을 의미한다. 이는 .wasm 파일의 사이즈를 줄여주는데, 이로 인해 다운로드에 걸리는 시간도 줄어들게 된다.

비록 웹어셈블리가 스택 기계의 특징을 갖고있긴 하지만, 물리적 기계에서도 똑같이 동작하는 것은 아니다. 브라우저가 웹어셈블리를 자신이 구동되고 있는 기계를 위한 기계 코드로 번역할 때는 레지스터를 사용할 것이다. 하지만 웹어셈블리 코드는 레지스터를 명시하지 않기 때문에, 브라우저로 하여금 해당 기계에 맞는 최선의 레지스터 할당을 할 수 있도록 유연성을 제공해 준다.

모듈의 섹션

.wasm 파일에는 add42 함수 자신 이외에도 다른 부분이 존재한다. 이들을 섹션이라고 한다. 어떤 섹션들은 어떤 모듈에 필수 요건이며, 어떤 섹션들은 선택 요건이다.

필수 요건 :

  1. 타입. 이 모듈에 정의된 함수들이나 임포트한 함수들을 위한 함수 서명(signatures)을 포함한다.
  2. 함수. 이 모듈에 정의된 각 함수에 대한 인덱스를 제공한다.
  3. 코드. 이 모듈의 각 함수에 대한 실제 함수 본문.

선택 요건 :

  1. 익스포트(export). 다른 어셈블리 모듈이나 자바스크립트에서 사용가능한 함수, 메모리, 테이블, 전역 등을 생성한다.
  2. 임포트(import). 다른 어셈블리 모듈이나 자바스크립트에서 임포트한 함수, 메모리, 테이블, 전역 등을 지정한다.
  3. 시작. 웹어셈블리 모듈이 로드되었을 때 자동으로 실행될 함수 (기본적으로 메인 함수와 유사함)
  4. 전역. 모듈을 위한 전역 변수들을 선언한다.
  5. 메모리. 이 모듈이 사용할 메모리를 정의한다.
  6. 테이블. 자바스크립트 객체와 같이 웹어셈블리 모듈 외부에 있는 값들을 맵핑할 수 있도록 해 준다. 이는 간접적으로 함수를 호출할 수 있도록 할 때 특히 유용하다.
  7. 데이터. 임포트된 메모리나 로컬 메모리를 초기화한다.
  8. 요소. 임포트된 테이블이나 로컬 테이블을 초기화한다.

섹션에 대한 추가적인 설명을 원한다면 이들 섹션들이 어떻게 동작하는지에 대한 설명을 참고하길 바란다.

다음 주제

이제 웹어셈블을 어떻게 사용하는지 알았으니, 왜 어셈블리가 빠른지에 대해 알아보자.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 어셈블리 집중 코스

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 세 번째 글인 A crash course in assembly의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스 (현재글)
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

웹어셈블리가 어떻게 작동하는지를 이해하기 위해서는, 어셈블리가 무엇이고 컴파일러가 어떻게 그것을 생성해내는지를 이해하는 것이 도움이 될 것이다.

JIT에 대해 설명한 글에서 나는 기계와 의사소통을 하는 것이 마치 외계인과 의사소통을 하는 것과 비슷하다고 설명했다.

03-01-alien03-500x286.png

이제 나는 그 외계인의 뇌가 어떻게 작동하는지를 – 즉, 기계의 뇌가 들어오는 정보를 해석하고 이해하는지 – 를 살펴보도록 하겠다.

기계의 뇌에는 생각을 위해 존재하는 부분이 있으며 더하기, 빼기 혹은 논리 연산 등을 수행한다. 또한 그와 가까운 곳에 단기 기억을 제공하는 부분과 장기 기억을 제공하는 부분도 있다.

이들 각각의 부분은 이름을 갖는다.

  • 생각을 담당하는 부분은 산술 논리 장치 (ALU : Arithmetic Logic Unit)이다.
  • 단기 기억은 레지스터에 의해 제공된다.
  • 장기 기억은 랜덤 억세스 메모리 (RAM : Random Access Memory)이다.

03-02-computer_architecture09-500x302.png

기계 코드 내에서의 문장들은 명령(instruction)이라고 불린다.

이러한 명령들 중 하나가 뇌 속으로 들어오면 어떤 일이 벌어질까? 그 명령은 각기 다른 의미를 가진 부분으로 나누어진다.

이 명령이 나누어지는 방식은 뇌의 회로에 의해 특정지어진다.

예를 들어 이런 식으로 구성된 회로를 갖는 뇌는 항상 처음 여섯 비트를 ALU 로 전송할 것이다. ALU는 1과 0의 위치를 기반으로, 두 값을 함께 더해야 한다는 것을 알아낸다.

이 조각은 “opcode” 혹은 연산 코드(operation code)라고 불리는데, 왜냐하면 이것이 ALU에게 어떤 연산을 수행할지를 말해주기 때문이다.

03-03-computer_architecture12-500x354.png

그 후에 뇌는 덧셈을 할 두 개의 수가 무엇인지 알아내기 위해 세 개의 비트로 이루어진 다음 두 조각을 읽어온다. 이들은 레지스터의 주소일 것이다.

03-04-computer_architecture17-500x352.png

기계 코드 위에 있는 주석을 잘 살펴보자. 이를 통해 우리 인간이 기계 코드를 좀더 쉽게 이해할 수 있는데, 이것이 바로 어셈블리이다. 상징적(symbolic) 기계 코드라고도 불린다. 이것이 인간이 기계의 코드를 이해할 수 있는 방법이다.

여기서 어셈블리와 기계 코드 사이에는 꽤 직접적인 관계가 있는 것을 볼 수 있을 것이다. 이 때문에 여러 종류의 기계 아키텍쳐에 맞는 각기 다른 어셈블리가 존재한다. 다른 아키텍쳐의 머신에게는 그 머신에게만 통용되는 별도의 어셈블리가 필요하다.

즉, 번역의 목표 대상이 하나만 있는 것이 아니다. 단순히 하나의 기계 코드가 있는 것이 아니다. 아주 다양한 종류의 기계 코드가 있다. 사람들이 각자 다른 언어로 이야기하듯이, 기계들도 각자 다른 언어로 이야기한다.

인간의 언어를 외계인에게 번역한다면, 아마 영어나 러시아어 혹은 중국어를 외계인 언어 A 혹은 외계인 언어 B로 번역하게 될 것이다. 프로그래밍 용어로 말하자면 C, C++, Rust 등을 x86 이나 ARM으로 번역하는 것이 될 것이다.

이들 상위 레벨 프로그래밍 언어들 중의 하나를 어셈블리 언어들 중 하나로 번역한다고 해 보자. 가능한 한 가지 방법은 각 언어에서 각 어셈블리로 번역할 수 있는 각기 다른 모든 번역기를 만드는 것이다.

03-05-langs05-500x308.png

이 방법은 꽤나 비효율적이다. 이를 해결하기 위해 대부분의 컴파일러는 둘 사이에 최소한 하나의 레이어를 둔다. 컴파일러는 상위 레벨 프로그래밍 언어를 읽어서, 너무 상위 레벨도 아니면서 기계 코드의 레벨에서 동작하지도 않는 무언가로 번역한다. 이것을 중간 표현 형식 (IR: Intermediate representation) 이라고 한다.

03-06-langs06-500x317.png

이 말은 곧 컴파일러가 이들 상위 레벨 언어들 중 어떤 것이든 읽어서 단일 IR 언어로 번역할 수 있다는 의미이다. 여기서부터 컴파일러의 다른 부분이 이 IR을 읽어서 특정 대상의 아키텍쳐에 맞게 컴파일할 수 있게 된다.

컴파일러의 프론트엔드는 상위 레벨 프로그래밍 언어를 IR로 번역한다. 컴파일러의 백엔드는 IR을 대상 아키텍쳐의 어셈블리 코드로 번역한다.

03-07-langs09-500x306.png

결론

지금까지 어셈블리가 무엇이고, 컴파일러가 상위 레벨 프로그래밍 언어를 어떻게 어셈블리로 번역하는지를 살펴보았다. 다음 글에서는 웹어셈블리가 여기에 어떻게 적용되는지를 살펴보겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 저스트-인-타임(JIT) 컴파일러 집중 코스

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 두 번째 글인 A crash course in just-in-time (JIT) compilers의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스 (현재글)
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

자바스크립트는 태생이 느렸지만, JIT라고 불리는 무언가 덕분에 빨라지게 되었다. 그런데 JIT는 어떻게 동작하는걸까?

자바스크립트는 브라우저에서 어떻게 실행될까

개발자로서 자바스크립트를 페이지에 추가하면, 당신은 목표와 문제를 갖게 된다.

목표 : 당신은 컴퓨터에게 무엇을 할지 말하고 싶다.

문제 : 당신과 컴퓨터는 다른 언어를 사용해서 이야기한다.

당신은 인간의 언어를 사용하고, 컴퓨터는 기계의 언어를 사용한다. 비록 당신이 자바스크립트나 다른 상위레벨 프로그래밍 언어들이 인간의 언어가 아니라고 생각할 지라도, 이들은 실제로 인간의 언어이다. 이들 언어는 기계가 아닌 인간이 이해하기 위해 디자인되었다.

그러므로 자바스크립트 엔진의 임무는 당신이 사용하는 인간 언어를 기계가 이해할 수 있는 무언가로 변경시키는 것이다.

나는 이 상황이 영화 Arrival 에서 인간과 외계인이 서로 대화하려고 하는 장면과 비슷하다고 생각한다.

02-01-alien03-500x286.png

이 영화에서 인간과 외계인은 단지 단어를 단어로 옮기는 식으로 번역하지 않는다. 두 집단은 세상에 대해 생각하는 방식이 다르다. 그리고 이는 인간과 기계 사이에서도 마찬가지이다. (여기에 대해서는 다음 글에서 설명하겠다.)

그러면 어떤 식으로 번역이 이루어질까?

프로그래밍에서 기계 언어로 번역하는 방식에는 일반적으로 2가지가 있다. 바로, 인터프리터와 컴파일러다.

인터프리터를 사용하면, 실시간으로 한줄, 한줄 번역을 진행한다.

02-02-interp02-500x291.png

반면, 컴파일러는 실시간으로 번역하지 않는다. 컴파일러는 미리 작동해서 번역 내용을 생성하고 그것을 적어둔다.

02-03-compile02-500x297.png

이들이 번역하는 방식에는 각각 장점과 단점이 있다.

인터프리터의 장점과 단점

인터프리터는 빠르게 준비해서 실행할 수 있다. 코드를 실행하기 위해 전체 코드를 컴파일하는 단계를 거칠 필요가 없다. 첫번째 줄을 번역하면 바로 실행할 수 있다. 

이 덕분에, 인터프리터는 자바스크립트 같은 언어에 잘 어울리는 것 같다. 웹개발자에게는 시작하자마자 빠르게 코드가 실행되도록 하는 것이 중요하다.

초기 브라우저들이 자바스크립트 인터프리터를 사용한 것은 이러한 이유 때문이다.

동일한 코드를 한번 이상 실행하게 되면 인터프리터의 단점이 발생하기 시작한다. 예를 들어 반복문 내부에 있다고 가정해보자. 그러면 동일한 번역 작업을 계속해서 반복해야만 할 것이다.

컴파일러의 장점과 단점

컴파일러는 정반대의 트레이드-오프를 갖는다.

초기 구동 시간은 조금더 오래 걸리는데, 왜냐하면 처음부터 모든 컴파일 과정을 진행해야 하기 때문이다. 하지만 이후에 코드가 반복해서 실행되면 더 빠른데, 왜냐하면 루프가 진행되는 동안 동일한 번역을 반복할 필요가 없기 때문이다. 

또다른 차이점은 컴파일러가 코드를 좀더 빠르게 실행하기 위해 전체 코드를 둘러보고 편집을 하는 시간이 더 소요된다는 점이다. 이런 편집을 최적화라고 부른다. 

인터프리터는 런타임에 동작하기 때문에, 번역하는 동안에 이러한 최적화를 위해 많은 시간을 소요할 수가 없다.

저스트-인-타임 컴파일러 : 두 세상의 최선

인터프리터의 비효율성 반복문에서 매번 같은 코드를 다시 번역해야 하는 문제 을 제거하기 위해 브라우저는 인터프리터에 컴파일러를 혼합하기 시작했다.

브라우저 마다 방식이 조금씩 다르긴 하지만, 기본 개념은 동일하다. 이들은 자바스크립트 엔진에 모니터(혹은 프로파일러)라고 불리는 부분을 추가했다. 이 모니터는 코드가 실행되는 것을 관찰하면서 해당 코드가 얼마나 많이 실행되고, 어떤 타입이 사용되는지를 기록한다.

먼저, 모니터는 인터프리터를 통해서 모든 것을 실행한다.

02-04-jit02-500x365.png

만약 같은 줄의 코드가 적은 횟수로만 실행되면, 그 코드는 따뜻하다(warm)라고 칭한다. 만약 많이 실행된다면 뜨겁다(hot)라고 칭한다.

기본(Baseline) 컴파일러

어떤 함수가 따뜻해지기(warm) 시작하면 JIT는 그 함수를 컴파일 하기 위해 전송한다. 그 이후에 JIT는 컴파일된 정보를 저장한다.

02-05-jit06-500x368.png

함수의 각 줄은 스텁”으로 컴파일된다. 스텁은 줄번호와 변수 타입에 의해 색인(Index)된다 (이 과정이 왜 중요한지는 나중에 설명하겠다). 만약 동일한 코드가 동일한 변수 타입에 의해 실행되는 것을 모니터가 발견하게 되면, 해당 코드의 컴파일된 버전을 내보낸다.

이 과정은 속도를 향상시켜 준다. 하지만 내가 말했듯이, 컴파일러는 더 많은 일을 할 수 있다. 컴파일러는 최적화를 하기 위해 시간을 들여서 가장 효율적인 방법을 찾아낼 수 있다.

기본 컴파일러가 이러한 최적화의 일부를 담당한다 (아래에서 예제를 하나 보여주겠다). 기본 컴파일러는 너무 많은 시간이 소요되는 것을 원치 않는데, 왜냐하면 실행 시간을 너무 오랫동안 멈추고 싶지 않기 때문이다.

하지만, 만약 코드가 정말로 뜨겁다면(hot) 코드가 아주 많은 횟수로 실행된다면 최적화를 더 하기 위해 추가적인 시간을 들일만한 가치가 있을 것이다.

최적화 컴파일러

코드의 한 부분이 아주 뜨거울(hot) 때, 모니터는 그 부분을 최적화 컴파일러에게 전송한다. 이 컴파일러는 또 다른 더욱 빠른 버전의 함수를 생성하며, 이 함수 역시 저장된다.

02-06-jit09-500x365.png

더 빠른 버전의 코드를 생성하기 위해, 최적화 컴파일러는 몇가지 가정을 해야한다.

예를 들어, 만약 특정한 생성자로부터 만들어지는 모든 객체들이 같은 모양을 갖는다고 가정할 수 있다면 즉 항상 같은 프라퍼티명을 가지며 이들 프라퍼티들이 같은 순서로 추가된다면 이를 이용해서 몇가지 최적화를 할 수 있을 것이다. 

최적화 컴파일러는 이러한 판단을 하기 위해 모니터가 코드의 실행을 지켜보며 수집한 정보들을 사용한다. 이전에 반복문을 순회하는 동안 항상 참이었던 값이 있다면, 그 값이 계속해서 참일 거라고 가정할 수 있을 것이다.

하지만 물론 자바스크립트에서는 어떤 것도 보장할 수 있다. 객체가 99번동안 항상 같은 모양을 하고 있더라도, 100번째에는 프라퍼티 하나가 빠져있을 수도 있다. 

그러므로 컴파일된 코드는 실행하기 전에 가정이 유효한지를 확인해야만 한다. 유효하다면, 컴파일러는 코드를 실행한다. 하지만 유효하지 않다면 JIT는 가정이 잘못되었다고 판단하고 최적화된 코드를 버린다.

02-07-jit11-500x361.png

그러면 실행은 다시 인터프리터 혹은 기본 컴파일된 버전으로 돌아간다. 이 과정은 역최적화(deoptimization) 혹은 구제(bailing out)라고 한다. 

최적화 컴파일러는 주로 코드를 빠르게 만들지만, 가끔 예상하지 못한 성능 문제를 야기하기도 한다. 만약 코드가 계속해서 최적화되고 역최적화된다면, 단순히 기본 컴파일된 버전을 실행시키는 것 보다 느려지게 될 것이다.

대부분의 브라우저들은 이러한 최적화/역최적화 싸이클이 발생했을 때 중지할 수 있는 제한을 갖고 있다. 예를들어 만약 JIT가 10번의 이상의 최적화를 시도하고 계속해서 그 코드를 버린다면, 더이상 최적화를 그만 시도하게 될 것이다.

최적화 예제 : 타입 특수화

최적화에는 다양한 종류가 있는데, 어떻게 최적화가 발생하는지를 보여주기 위해 한가지 형태를 살펴보도록 하겠다. 최적화 컴파일러의 가장 중요한 성과들 중 하나는 타입 특수화라고 불리는 것에서 얻어진다.

자바스크립트가 사용하는 동적 타입 시스템은 실행시점에 약간의 추가 작업이 요구된다. 예를 들어 다음의 코드를 살펴보자.

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

위의 반복문 내부에 있는 += 연산은 간단해 보일 수도 있다. 이 연산은 한번에 할 수 있을것 처럼 보이지만, 동적 타입 때문에 예상했던 것 보다 더 많은 단계가 필요하다.

arr이 100개의 정수를 갖는 배열이라고 가정해보자. 코드가 사용되기 시작하면 기본 컴파일러는 함수안에 있는 각각의 연산에 대한 스텁을 생성할 것이다. 그래서 sum += arr[i]에 대한 스텁이 생성되며, 이 스텁이 += 연산을 정수 연산으로써 처리하게 된다.

하지만 sumarr[i]이 정수라고 보장할 수는 없다. 자바스크립트에서는 타입이 동적이기 때문에, 루프를 다음번에 순회할 때에는 arr[i]가 문자열일 수도 있다. 정수를 더하는 것과 문자열을 연결하는 것은 아주 다른 두 개의 연산이므로, 둘은 아주 다른 기계 코드로 컴파일될 것이다.

JIT는 여러개의 기본 스텁을 컴파일함으로써 이런 상황을 처리한다. 만약 코드 조각이 동일한 구조라면 (즉, 항상 같은 타입들로 호출된다면) 하나의 스텁만 가질 것이다. 만약 코드 조각이 다형 구조라면 (코드 이곳 저곳에서 각각 다른 타입으로 호출된다면), 해당 연산이 실행되는 각각의 타입 조합 만큼 스텁을 갖게 될 것이다.

이 말은 JIT가 스텁을 선택하기 전에 많은 질문을 던져야 한다는 의미이다.

02-08-decision_tree01-500x257.png

각 라인별 코드들이 각자의 스텁 묶음을 기본 컴파일러 내부에 갖고 있기 때문에, JIT는 각 라인의 코드가 실행될 때마다 타입을 확인해야만 한다. 매번 루프를 순회할 때마다, JIT는 같은 질문을 해야만 한다.

02-09-jit_loop02-500x323.png

만약 JIT가 이렇게 반복해서 확인할 필요가 없다면, 코드 실행이 훨씬 빨라질 수 있을 것이다. 이것이 바로 최적화 컴파일러가 하는 일 중 하나이다. 

최적화 컴파일러에서는 전체 함수가 함께 컴파일된다. 타입 확인은 루프 이전에 발생할 수 있도록 이동된다.02-10-jit_loop02-500x318.png

어떤 JIT는 여기서 더 최적화하기도 한다. 예를 들어, 파이어폭스에는 오직 정수만을 갖는 배열들을 위한 특수한 분류가 있다. 만약 arr이 이러한 배열들 중 하나라면, JIT는 arr[i]가 정수인지 확인할 필요가 없게 된다. 이는 JIT가 루프에 진입하기 전에 모든 타입을 확인할 수 있다는 의미이다.

결론

지금까지 JIT에 대해 간략하게 살펴보았다. JIT는 코드가 실행되는 것을 관찰하여 자주 실행되는 코드가 최적화될 수 있도록 전송하여 자바스크립트를 더 빠르게 실행되도록 만든다. 이는 대부분의 자바스크립트 어플리케이션에의 성능을 몇배로 향상시켰다.

하지만 이러한 향상에도 불구하고, 자바스크립트의 성능은 예측하기 어려울 수 있다. 또한 코드를 빠르게 만들기 위해서 JIT는 실행시점에 몇가지 오버헤드를 발생시키는데, 이는 다음을 포함한다:

  • 최적화와 역최적화
  • 모니터의 기록 영역과 코드가 버려질 때 복구를 위한 정보가 사용하는 메모리
  • 기본 버전과 최적화 버전의 함수를 저장하기 위한 메모리

여기에 좀더 개선할 여지가 있다: 이러한 오버헤드는 제거될 수 있다면, 성능이 좀더 예측 가능해진다. 이것이 바로 웹어셈블리가 하는 일 중의 하나이다.

다음 글에서는 어셈블리에 대해, 그리고 컴파일러가 어떤 식으로 함께 동작하는지에 대해서 설명하도록 하겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 카툰으로 소개하는 웹어셈블리

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 첫 번째 글인 A cartoon intro to WebAssembly의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리 (현재글)
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

웹어셈블리는 빠르다. 아마 당신도 들어봤을 것이다. 하지만, 웹어셈블리가 왜 빠른걸까?

이 연재물에서 나는 왜 어셈블리가 빠른지에 대해 설명해보려고 한다.

잠깐, 그래서 웹어셈블리가 뭔데?

웹어셈블리는 자바스크립트가 아닌 다른 프로그래밍 언어로 작성된 코드를 브라우저에서 실행시키기 위한 방법이다. 그래서 사람들이 웹어셈블리가 빠르다고 이야기하는 건, 자바스크립트와 비교했을 때 빠르다는 말이다.

지금 나는 자바스크립트나 웹어셈블리 둘 중의 하나만 선택해야 한다고 말하고 싶은 것이 아니다. 사실, 우리는 개발자들이 동일한 어플리케이션에서 웹어셈블리와 자바스크립트를 모두 사용하기를 기대한다.

하지만 두 언어를 비교해서 웹어셈블리가 가진 잠재적 효과를 이해할 수 있다면 유용할 것이다.

성능에 대한 약간의 역사

자바스크립트는 1995년에 만들어졌다. 자바스크립트는 빠르게 동작하도록 디자인되지 않았으며, 처음 10년동안은 실제로 빠르지 않았다.

그 이후에 브라우저들의 경쟁이 시작되었다.

2008년에 사람들이 성능 전쟁이라고 말하는 기간이 시작되었다. 많은 브라우저들이 저스트-인-타임(Just-in-time) 컴파일러, 즉 JIT를 장착했다. 자바스크립트가 실행되는 동안, JIT는 패턴들을 살펴볼 수 있었고, 이들 패턴을 기반으로 코드를 더 빨라지도록 만들었다.

JIT가 등장하면서 자바스크립트의 성능에는 변곡점이 생겼다. 자바스크립트의 실행 속도가 10배는 빨라진 것이다.

01-01-perf_graph05-500x409.png

이런 성능 향상에 힘입어, 자바스크립트는 Node.js를 이용한 서버사이드 프로그래밍 등과 같이 이전까지는 누구도 상상할 수 없었던 곳에서 사용되기 시작했다. 성능의 향상은 자바스크립트를 전혀 새로운 영역에서도 사용할 수 있도록 만들어 주었다.

지금 우리는 아마도 또다른 변곡점에 있는지도 모른다. 웹어셈블리로 인해.

01-02-perf_graph10-500x412.png

자, 그럼 좀더 자세한 내용으로 들어가서 웹어셈블리가 왜 빠른지를 이해해보도록 하자.

배경지식:

웹어셈블리, 현재:

웹어셈블리, 미래:

 

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark