소개
안녕하세요, 여러분. 11월이에요. 잘 지내고 계시죠?
10월은 정말 바빴어요. 커뮤니티 솔루션에 큰 개선을 막 적용했거든요. 이제 중복을 제거해서 비슷한 솔루션은 한 번만 보여주고, 새로운 정렬 옵션과 코드로 검색하는 기능도 추가했어요. C#을 확인해 보면 프로그래밍 개념별로 필터링하는 기능도 추가된 걸 볼 수 있을 거예요. 그래서 비트 시프트나 재귀 등 원하는 걸 쓴 솔루션을 찾아볼 수 있어요. 다른 트랙에도 커뮤니티 위크에 순차적으로 적용할 예정이에요.
그럼 지금은 #12in23에 집중해 봐요. 10월은 객체 지향 언어를 살펴보는 흥미로운 달이었는데, 이번 달은 좀 더 하드코어하게 가요. 이번에는 어셈블리어, 그중에서도 MIPS 어셈블리, x86-64 어셈블리, WebAssembly에 집중해요. 늘 그렇듯, Erik이 이 언어들의 흥미롭고 독특한 점을 설명해 줄 거예요.
배지
언제나처럼 이 언어들로 연습 문제 5개를 완료하면 Nibbly November 배지를 받을 수 있어요. 많은 분들이 목표로 삼고 있는 Year-Long 배지도 있어요. 이를 위해 완료해 보면 좋을 추천 연습 문제 5개를 준비했어요. 바로:
- Pop Count: 숫자에서 1인 비트의 개수 세기
- Grains: 칸마다 두 배씩 늘어나는 체스판에서 곡식의 개수 계산하기
- Resistor Color: 저항 띠의 색을 숫자 표현으로 변환하기
- Rotational Cipher: 회전 암호(일명 시저 암호) 구현하기
- Nucleotide Count: DNA 문자열에서 각 염기가 몇 번 나타나는지 계산하기
배경
왜 이름이 Nibble November일까요?
아마 아시겠지만, 바이트는 8비트예요. 그리고 니블은 4비트고요. byte의 y를 i로 바꾸면 bite가 되는데, 그렇게 보면 이름이 이해가 가기 시작해요. 그러면 니블은 작은 bite인 셈이에요.
어셈블리어란 무엇일까요?
먼저 기본부터 시작해 봐요. CPU는 '두 수를 더하기'나 '비트를 왼쪽으로 시프트하기' 같은 명령어를 실행해요. 이런 명령어를 기계어 명령어라고 하는데, 특정한 비트 나열에 지나지 않아요. 프로그램을 실행한다는 건 (따옴표로 강조하자면) '단순히' CPU가 이 비트 나열을 처리하고 실행하는 것이에요.
명령어를 비트 나열로 직접 쓰는 건 번거롭고 오류가 나기 쉬워서, Kathleen과 Andrew Donald Booth는 1947년에 이미 기계어 명령어를 표현하는, 사람이 더 다루기 쉬운 언어를 고안했어요. 이렇게 기계어 명령어를 표현하는 언어를 어셈블리어라고 해요. 그리고 이 어셈블리어는 '어셈블러'를 통해 기계어 명령어로 변환돼요.
어셈블리어의 흥미로운 점은 보통 운영체제와는 독립적이지만, CPU 아키텍처에는 직접적으로 묶여 있다는 거예요.
참고로, 아주 초창기 컴퓨터에서 프로그램을 실행시키는 데 쓰던 그 큰 카드, 천공 카드를 기억하신다면, 그것도 일종의 어셈블리어였어요!
어셈블리어는 지금 우리가 쓰는 언어와 어떻게 다를까요?
가장 큰 차이는 어셈블리어가 아주 저수준이라는 거예요. 익숙해져 있던 수많은 추상화가 사라져요. 클래스나 객체는 어디에도 없어요. 루프? 점프를 써서 직접 작성해야 해요. 함수? 없어요! 어셈블리 코드를 짜다 보면 현대 언어가 얼마나 삶을 편하게 해 주는지 깨닫게 돼서, 저는 굉장히 겸손해지는 기분이었어요. 그런데 어셈블리 코드를 작성하는 건 정말 유용하기도 해요. 사물이 실제로 어떻게 동작하는지 훨씬 잘 이해하게 되거든요.
재미있는 사실: RollerCoaster Tycoon의 소스 코드 99%는 손으로 작성한 어셈블리 코드였어요! 정말 놀라운 성과인데, 직접 어셈블리를 조금 해보고 나면 그 대단함이 더욱 와닿을 거예요.
사람들은 아직도 어셈블리 코드를 작성할까요?
음, 예전보다는 덜해요. 예전에는 손으로 짠 어셈블리가 컴파일러가 생성한 기계어보다 더 빠른 경우가 많았어요(그래서 C++ 언어에서는 어셈블리 코드를 직접 넣을 수 있게 해 주기도 하고요). 하지만 요즘 컴파일러는 기계어를 너무 잘 만들어 내서 이런 경우는 거의 없어졌어요. 그렇긴 해도 성능이 중요한 환경이나 자원이 제한된 환경에서는 여전히 어셈블리어가 쓰이는 걸 볼 수 있어요.
개요
MIPS
- MIPS(Microprocessor without Interlocked Pipelined Stages)는 RISC(축소 명령어 집합 컴퓨터) 명령어 집합 구조 계열이에요.
- MIPS Computer Systems가 개발했고 1985년에 처음 공개됐어요.
- 여러 버전이 있어요: MIPS I, II, III, IV, V와 MIPS32/64. 처음 두 버전은 32비트 전용이었지만, MIPS III에서 64비트 지원이 도입됐어요.
- SIMD 명령어나 압축 같은 여러 선택적 확장이 있어요.
- 이후 RISC 아키텍처에 큰 영향을 줬어요.
- MIPS는 2021년에 MIPS 아키텍처 개발을 중단하고 RISC-V(오픈 소스이며 로열티가 없는 아키텍처예요)로 옮겨갔다고 발표했어요.
- 주로 임베디드 시스템(예: 라우터)과 서버(실리콘 그래픽스 컴퓨터가 사용했는데, 영화 특수 효과에 쓰인 걸로 유명하죠), NEC Cenju-4 슈퍼컴퓨터, 테슬라의 Model S 자동차, NASA의 뉴 허라이즌스 탐사선에 쓰였고, 대학에서 어셈블리를 가르치거나 여러 게임 콘솔(예: 초기 PlayStation, Playstation Portable, Nintendo 64)에도 쓰였어요.
x86-64 어셈블리
- AMD가 설계했고 1999년에 AMD64 아키텍처로 공개됐어요.
- x86 명령어 집합의 64비트 버전이에요. x86은 1978년 Intel이 8086 마이크로프로세서를 출시하면서 거슬러 올라가요. 그건 16비트 프로세서였는데, 이후 80386이 32비트 명령어를 추가했고, 그 명령어 집합이 x86과 사실상 같은 뜻이 됐어요.
- 64비트가 가능하게 한 핵심은 더 많은 메모리를 다루는 것이었어요(32비트 주소 지정은 4GB로 제한돼요). 이게 병목이 되고 있었거든요. 64비트는 이론적으로 16엑사바이트까지 다룰 수 있지만, 현재는 48비트만 사용해서 256TB까지 다룰 수 있어요(필요해지면 나중에 확장할 수 있어요).
- AMD64는 x86 명령어 집합을 확장한 것으로, 호환 모드를 통해 기존 16비트, 32비트 애플리케이션과 완전히 호환되도록 설계됐어요.
- Intel은 AMD가 참여하지 않은 채 IA-64를 설계했어요. 이는 아주 새롭고, 매우 다르며, 하위 호환이 되지 않는 64비트 명령어 집합이었어요. 결국 AMD64가 승리했고, Intel은 (약간의 의미상 차이만 있는) 자체 버전을 구현했어요.
- 어디에나 쓰여요. 워크스테이션부터 서버(슈퍼컴퓨터 포함)까지, 임베디드 시스템부터 게임 콘솔(예: PS5, Xbox Series X)까지요.
WebAssembly
- 웹 기술 표준 기구인 W3C가 설계했어요.
- 설계 목표는 다음과 같아요:
- 빠르고, 안전하고, 이식성이 좋을 것
- 효율적이고 이식성 있는 표현
- 예전에는 웹에서 빠른 실행을 하려면 보통 Flash나 Silverlight 같은 전용 브라우저 플러그인을 썼어요. JavaScript 자체는 고성능 컴퓨팅에 그다지 어울리지 않았거든요. 이런 플러그인의 큰 단점은 보안 문제가 많고 표준화되어 있지 않다는 점이었어요.
- Mozilla는 JavaScript의 부분 집합인 asm.js를 설계했어요. 이는 브라우저에서 뛰어난 성능으로 코드를 실행할 수 있게 하는 걸 목표로 했고, 타입 일관성(타입을 동적으로 바꾸지 않음)과 가비지 컬렉션 없음을 통해 그 목표를 이뤘어요. 그래서 언어들이 asm.js로 컴파일하면 웹에서도 좋은 성능을 낼 수 있었어요. 그래도 여전히 JS였기 때문에 할 수 있는 게 한정적이었죠. 그래서 새로운 언어에 대한 제안이 나왔어요: WASM.
- 실행할 명령어 집합을 제공한다는 점에서 어셈블리와 비슷한 언어예요. 중요한 건 특정 CPU에 묶여 있지 않아서 플랫폼에 독립적이고, 플랫폼마다 구현체(가상 머신)가 필요하다는 거예요. 즉 WebAssembly는 사실 기계어가 아니라 바이트코드예요.
- 정적 타입이에요(JS와의 결정적 차이예요).
- 보통 미리 컴파일하거나 실행 중에 컴파일해요(인터프리트될 수도 있어요).
- 공개 표준이며 두 가지를 정의해요:
- 바이너리 형식
- 텍스트 형식(바이너리 형식으로 컴파일돼요)
- 모든 주요 브라우저에 구현체가 있어요.
- Google Earth, Figma, Unity, Autocad처럼 높은 성능이 필요한 많은 웹 페이지에서 쓰여요. 서버 쪽에서도 입지를 넓히고 있는데, 예를 들어 마이크로서비스를 실행하거나 SaaS 플랫폼(예: CloudFlare 워커)에서, 또는 Docker에서 쓰여요.
프로그래밍 관점에서는 어떻게 다를까요?
MIPS
- 로드-스토어 구조(레지스터-레지스터 구조라고도 해요)를 사용해요. 여기서 명령어는 메모리 접근을 하거나 산술 연산을 하는데, 데이터는 항상 레지스터에서만 다뤄요.
x86-64 어셈블리
- 레지스터-메모리 구조를 사용해서, 레지스터뿐 아니라 메모리에서도 연산을 수행할 수 있어요.
WebAssembly
- 스택 기반 프로그래밍을 사용하고(레지스터가 없어요), 메모리에서 데이터를 읽거나 메모리로 쓰는 것도 가능해요.
이 언어들을 멋지게 만드는 점은 무엇일까요?
MIPS
- 작아요. MIPS 명령어 집합은 모든 명령어를 한 페이지에 담을 수 있어요.
- 잘 정립된 호출 규약 덕분에 사용 가능한 레지스터를 어떻게 쓸지 알 수 있어요. 예를 들어 인자를 넘길 때 어떤 걸 쓰고, 결과를 반환할 때 어떤 걸 쓰는지요.
- 안정적이에요. 마지막 버전은 2014년에 공개됐어요.
- 특히 학술 자료에 문서화가 잘 되어 있어요.
- 실제 사용처가 많아요. 수십억 대의 기기에 들어가 있죠.
x86-64 어셈블리
- x64의 확장이면서도 많은 새로운 기능이 추가됐어요:
- 64비트 정수 지원
- 추가 레지스터
- SSE 명령어(벡터 명령어)
- 상대 데이터 접근(공유 라이브러리를 쓸 때 더 효율적이에요)
- 실행 방지 비트(특정 메모리 페이지에서 코드가 실행되는 걸 막는 보안 기능이에요)
- 익숙해요. x86 명령어 집합을 확장한 것이기 때문에, x86 명령어 집합에 익숙한 사람이라면 비교적 배우기 쉬워요. 방대하고 자세한 문서도 있고요.
- 안정적이에요. 정기적으로 새 버전이 추가되고 있지만, 핵심은 여전히 매우 안정적이고 하위 호환도 잘 돼요.
WebAssembly
- 스택 기반인 WebAssembly 가상 머신은 실제 프로세서(RISC 포함)의 어셈블리어에 비해 간결하고 단순해요. 그래서 컴파일 대상으로 삼기가 비교적 쉬워요.
- WebAssembly 텍스트 형식은 S-표현식 '슈가'를 사용해 익숙한 명령형 스타일을 구현하고, 이는 스택 기반 코드로 변환돼요. S-표현식이 있는 형태를 '슈가 형태'라고 부르며, 바이너리에 들어 있는 것과 동등한 다른 형태로 '디슈가'돼요. S-표현식은 LISP을 다뤄 본 사람이라면 누구나 익숙할 거예요.
- JavaScript와 상호 운용이 뛰어나요. JavaScript로, 그리고 JavaScript에서 데이터를 주고받기가 간단해요. 중요한 단서: WASM은 (아직) DOM과 상호 작용하는 걸 허용하지 않아요.
- 계속해서 개선되고 있어요. WASM 가상 머신뿐 아니라 표준 자체도 활발히 개발되고 있어요. SIMD 관련 명령어, 가비지 컬렉션, 스레드, 꼬리 호출 최적화 등 수많은 새 기능이 설계되고 작업되고 있어요.
- 안전해요. 코드가 검증되어 샌드박스 환경에서 실행되는데, 이는 JavaScript나 네이티브 어셈블리어보다 더 높은 수준의 정적 검증을 제공해요. 의미가 잘 정의되어 있어서 검사하고 추론하기 쉬워요.
돋보이는 특징
MIPS
- 효율성. MIPS 프로세서는 매우 효율적이라 임베디드 시스템에 잘 어울려요.
- 성능. 성능이 뛰어나서 MIPS가 슈퍼컴퓨터에 쓰이기도 했어요.
- 배우기 쉬워요. 명령어가 적고 각 명령어가 단순한 일 하나만 하기 때문에 배우기 쉬워요. 교육용으로 아주 좋아요.
x86-64 어셈블리
- 강력해요. x86-64는 수십 년에 걸쳐 다듬어져서 성능에 도움이 되는 수많은 명령어가 있어요. 그 예가 SIMD(단일 명령, 다중 데이터)인데, 하나의 명령어를 여러 데이터에 병렬로 실행할 수 있는 명령어예요.
- 어디에나 있어요. x86-64를 실행하는 기기가 넘쳐나요. Intel과 AMD CPU가 이를 구현하고, 꽤 오랫동안 사실상 표준이었어요.
- 꾸준히 업데이트돼요. 예를 들어 SSE3-5, AVX, AVX-512 등과 함께 새로운 벡터 명령어가 추가돼요.
WebAssembly
- 효율적이에요. 바이너리 형식이 간결하고, 빠른 단일 패스로 디코딩, 검증, 컴파일을 할 수 있어요. 스트리밍도 가능해서 모든 데이터를 다 받기 전에 최대한 빨리 디코딩, 검증, 컴파일을 시작할 수 있어요. 병렬 처리도 가능하고요. 그래서 고성능 웹 애플리케이션에 딱 맞아요.
- 훌륭한 컴파일 대상이에요. 여러 언어로 작성한 코드를 웹에서 실행할 수 있게 해 줘요. 대부분의 주요 언어가 WebAssembly 바이너리로 컴파일하는 걸 지원하는데, 덕분에 JavaScript를 쓰지 않고도 코드를 웹에서 실행할 수 있어요. 어떤 언어들은 코드를 WebAssembly로 컴파일하는 대신 런타임을 WebAssembly로 컴파일하고, 그러면 바이트코드를 그대로 실행할 수 있어요.
- 배포하고 실행하기 쉬워요. 바이트코드를 실행할 수 있는 가상 머신만 있으면 되고, 모든 주요 브라우저에 이런 가상 머신이 들어 있어요.
- 웹에만 묶여 있지 않고 서버 쪽에서도 실행할 수 있어요. WebAssembly 시스템 인터페이스(WASI)는 어떤 플랫폼으로든 이식할 수 있도록 설계된 인터페이스(ABI와 API)예요. Unix 시스템의 표준 인터페이스인 POSIX와 비슷하고, I/O 같은 것들을 제공해요. 보안이 설계의 핵심 요소인데, 샌드박싱과 권한 지향적이라는 점이 포함돼요(파일이나 소켓 같은 것에 대해서는 권한을 명시적으로 요청해야 해요). WASI는 언어 간 인터페이스를 쉽게 만들어 줄 가능성도 있어요. Docker의 공동 창업자인 Solomon Hykes는 2019년에 "WASM+WASI가 2008년에 있었더라면 Docker를 만들 필요가 없었을 거예요"라고 썼어요.
무엇을 선택할까요
- 어셈블리어를 한 번도 다뤄 본 적이 없다면, WebAssembly가 아마 시작하기 가장 쉬운 언어일 거예요. 다만 기계어로 컴파일되는 어셈블리어를 배우고 싶다면 MIPS 어셈블리를 해보세요.
- x86-64 기기에서 작업하고 있다면(아마 그럴 거예요), x86-64 어셈블리를 해보세요.
- LISP에 익숙하다면, WebAssembly가 S-표현식을 쓴다는 점이 반가울 거예요.
- 웹 앱을 다루고 있다면, WebAssembly가 가장 논리적인 선택이에요.
- 성능을 중요하게 생각한다면 x86-64와 MIPS가 좋은 선택이에요. 아니면 웹 성능이 중요하다면 WebAssembly를 해보세요.