- 이름: 박현우
- 깃헙: https://github.com/HyeonuPark
- 블로그: https://medium.com/@nemo1275
- 자기소개서: https://github.com/HyeonuPark/me/blob/master/RESUME.md
- Nal 언어: 구조적 타이핑과 비동기 지향의 고성능 언어
- Ringbuf: 링 버퍼 기반의 비동기 메세징 채널
제가 처음으로 만졌던 프로그래밍 언어는 Java였습니다. 마인크래프트에 모드를 엄청나게 깔다가 결국 모드를 직접 만들고 싶어졌거든요. 마인크래프트 포럼을 찾아보며 시키는 대로 이클립스를 깔고 코드를 따라 쳐 보니 제가 입력하는 대로 화면 속 세상이 바뀌는 것은 어린 저에게 굉장히 매력적인 경험이었습니다. 원하는 기능을 구현하려고 구글링을 하면서 컴퓨터가 어떤 식으로 동작하는지 생각하게 되었고, 덤으로 간결한 이름짓기가 왜 중요한지도 알게 되었습니다. 아마 이때부터 좋은 언어란 어떤 언어일까에 대해 고민하게 되었다고 생각합니다.
모딩보다 프로그래밍 그 자체가 재밌어질 무렵 제 관심은 JS로 옮겨갔습니다. 당시 한창 SPA가 힙하던 때라 AngularJS로 사진공유 사이트 등을 만들었고, 여기에 BaaS로 Firebase를 붙이다가 NodeJS를 본격적으로 만지기 시작했습니다. 오픈소스에 관심을 가지게 된 것도 이때쯤이었을 겁니다. npm 에코시스템에 참여하며 좋은 프로젝트란 어떤 것일지 생각해 보았습니다. 그런데 아시다시피 JS라는 언어는 그렇게 치밀하게 완성된 물건이 아닌지라 암시적 형변환이나 전역이름공간 등 이상한 동작들이 산재해 있습니다. 이런 부분을 찾아보다 보니 결국 JS라는 언어 자체에 관심을 가지게 되었고, 이 언어가 어떻게 정의되어 있으며 실제로 어떻게 동작하는지 알아보다 보니 자연스럽게 내부 구현에도 관심이 가게 되었습니다. 그렇게 C++를 익혀볼까 고민하던 중 알게 된 언어가 바로 Rust 였습니다.
JS처럼 자유롭게 발등을 찍을 수 있는 언어를 쓰다가 Rust처럼 정적검사를 수행하는 엄격한 언어를 시작하는 것이 쉽지만은 않았습니다. 하지만 이전부터 ESLint 등의 툴을 사용하며 컴파일러가 퍼붓는 잔소리에 익숙했었고, 꾸준히 테스트를 작성하는 습관을 들여놓은 덕에 생각보다 수월하게 적응할 수 있었습니다. 오히려 언어 수준에서 여러 함수형 패턴들을 강력하게 지원하는 덕분에 코딩이 더 편해졌다는 느낌도 들었습니다.
그 외에도 Go, Scala, Haskell 등 다른 언어들과 동시성, 자료 구조 등에 대한 논문들을 찾아보았고, 최근에는 이런 경험들을 바탕으로 Nal 언어를 만들고 있습니다. 또한 3년째 리눅스 데스크탑을 사용하며 나에게 가장 편리한 개발 환경을 찾고 있으며, 디지털오션에서 호스팅되는 개인 서버에 올릴 서비스를 고민 중입니다. 그리고 깃허브 저장소에 별이 좀 많이 찍히기를 기원하고 있습니다.
개인 프로젝트로서 Nal 프로그래밍 언어를 만들고 있습니다. 구조적 타이핑과 비동기 지향의 고수준 고성능 언어가 목표입니다. Nal 언어의 설계에 영향을 미친 언어들로는 Go, JavaScript, Rust가 있습니다.
그중 언어의 실행 의미론에 가장 큰 영향을 준 언어는 Go입니다. 처음 Go 언어를 봤을 때 상당히 신선한 충격을 받았습니다. 이때부터 작업 수준 병렬화와 m:n 그린 스레드가 어플리케이션에서 멀티코어 프로세서를 활용하는 효율적인 방법이라고 생각하게 되었고, 이는 Nal 언어의 런타임 구조로 이어졌습니다.
언어의 문법에 가장 큰 영향을 준 언어는 JavaScript입니다. 흔히 ES6으로 불리는 ECMAScript 의 새 문법에 오랫동안 관심을 가지고 있었고, 일부 논의에는 직접 참여하여 의견을 제시하기도 하였습니다. 또한 de facto standard 라고 할 수 있는 Babel transpiler 의 구조를 살펴보고 이에 연동되는 플러그인을 만들기도 하였습니다. Lua와 함께 JS의 특징 중 하나는 극한으로 자유로운 객체 구조입니다. 의미론적으로 JS의 객체는 {필드명 -> 필드값} 의 해시테이블과 같습니다. 여기서 Object.keys() 등 기존 정적타입언어에서 찾아볼 수 없는 JS만의 독특한 패턴이 가능해집니다. 이런 패턴을 구조적 타이핑과 정적 분석을 통해 컴파일 타임에 추론하도록 구현한다면 전혀 새로운 방식의 메타프로그래밍이 가능해질 것입니다.
컴파일러의 역할에 가장 큰 영향을 준 언어는 Rust입니다. Rust에서 컴파일러는 타입 체크, 소유권 체크, 불변성 체크 등 단순히 소스코드를 실행가능한 파일로 변환하는 것보다 훨씬 더 많은 일을 합니다. Nal 또한 이런 이점을 가질 것입니다. 컴파일 타임에 강제된 여러 제약은 재미있는 최적화들을 가능하게 합니다. 가령, 평범한 순차적 코드를 값 의존성과 불변성 분석을 통해 병렬실행되는 코드로 최적화하는 건 어떨까요?
기초적인 파서와 동적 타입 인터프리터 프로토타입을 만든 후 한차례 설계 변경이 있었습니다. 소스 코드는 파서 -> 추상문법트리(AST) -> 제어흐름그래프(CFG) 의 단계를 거쳐 실행 가능한 형태로 변환됩니다. 우선적으로 CFG를 동적 타이핑으로 실행하는 엄격한 인터프리터를 만들어 이를 기반으로 테스트 인프라를 작성하고, 이후 정적 타입 체커, 문법적 설탕, 매크로 확장 등을 추가하고 최종적으로는 정적 타이핑된 고성능 LLVM IR으로 변환할 예정입니다.
현재 Rust의 std::sync::mpsc 와 futures::channel 은 모두 Compare-And-Swap 기반의 Linked list 형태로 구현되어있습니다. 그리고 Linked list 의 가장 큰 단점은 역시 캐시 미스입니다. 매 send/receive 작업마다 캐시 미스가 발생하기 쉬워 메모리 입출력이 병목이 되어 CPU의 성능을 제대로 활용하지 못하게 되기 때문입니다. 링크드리스트를 대체할 생각이라면 가장 먼저 생각나는것은 역시 단순한 배열입니다. FIFO 큐를 원한다면 Ring buffer 가 되겠죠. 이 방면으로 유명한건 LMAX Disruptor 가 있는데요, 영국 LMAX 사 에서 FX 거래 처리를 위해 JAVA로 구현한 오픈소스 메시징 채널 라이브러리로 2011년에 공개되어 많은 관심을 받았습니다. Disruptor 는 단순한 메시징 채널 외에도 많은 것들을 제공하지만, 이 프로젝트에서는 가장 단순한 빌딩블럭인 메시징 채널만을 구현하는 것을 목표로 삼았습니다.
Ringbuf 구현의 핵심 아이디어이자 가장 재미있는 부분은 Sender 와 Receiver 가 동형(isomorphic)이라는 사실입니다. 실제로 둘은 Half라는 자료형을 감싸는 얇은 레이어일 뿐입니다. 길이 n의 링 버퍼를 n개의 슬롯의 집합이라고 생각할 때, 각 슬롯은 비어 있거나 메시지를 가지고 있습니다. 편의상 각각을 빨간 슬롯과 파란 슬롯이라고 부르겠습니다. Sender의 경우 버퍼에서 빨간 슬롯을 하나 점유한 후, 이를 가지고 있던 파란 슬롯과 바꾸고 점유를 해제합니다. Receiver의 경우 버퍼에서 파란 슬롯을 하나 점유한 후, 이를 가지고 있던 빨간 슬롯과 바꾸고 점유를 해제합니다. 논리적으로 정확히 같은 일을 한다고 할 수 있죠.
실제 구현에서는 Sender와 Receiver의 역할을 Role trait로 분리하고(Sender는 버퍼에 쓰고 Receiver는 버퍼에서 읽어옵니다), 버퍼의 슬롯을 점유/해제 하는 역할은 Sequence trait에게 맡겼습니다. 그리고 이 둘을 사용하여 Half 자료형이 try_advance 메서드를 구현하고, Sender와 Receiver는 사용자 API를 번역해 주는 역할만 맡습니다. 그리고 이 모든 과정에서 함수포인터 등 동적 디스패치는 전혀 사용되지 않았으므로 컴파일러 최적화를 거친 코드는 추상화 없이 손으로 한땀 한땀 짜낸 코드와 이론상 같은 속도를 낼 수 있습니다. 그러니까, Zero-cost Abstraction 이죠.
현재 Sequence trait의 구현체는 Owned 와 Shared 의 두 가지로, 후자는 여러 작업간 공유가 가능합니다. 이 둘을 조합하면 같은 코드로 spsc/spmc/mpsc/mpmc 를 모두 얻을 수 있습니다. 특히 Owned 는 반복문을 사용하지 않고 구현되었으므로, spsc 의 경우 wait-free 형태로 사용이 가능합니다. 추후 보낸 메시지를 모든 Receiver가 복사해서 나누어갖는 Broadcast Sequence 를 구현할 예정이며, 이 경우 추가적인 작업 없이도 spbc/mpbc 를 바로 얻을 수 있습니다.
현재 Ringbuf는 핵심이 되는 채널 구현은 사실상 완성된 상태이며, 최근에는 동기화 메커니즘들을 실험하고 있습니다. Thread-blocking mode와 futures 를 모두 지원할 예정으로 std::sync::mpsc 및 futures::channel 보다 빠른 속도가 목표입니다.