FFmpeg 어셈블리 언어 학교
(github.com/FFmpeg)- FFmpeg의 첫 어셈블리 강의는 멀티미디어 처리에서 손으로 작성한 SIMD 어셈블리가 왜 필요한지와 FFmpeg식 함수 작성 관례를 입문 수준에서 정리함
- 대상 독자는 C 포인터와 고등학교 수준의 스칼라·벡터, 덧셈·곱셈 개념을 알고 있어야 하며, 강의는 x86 64-bit와 Intel 문법을 기준으로 진행됨
- FFmpeg에서는 assembly function, SIMD, vectorise가 거의 같은 의미로 쓰이고, 여러 데이터 요소를 한 번에 처리하는 방식이 이미지·비디오·오디오 처리에 잘 맞음
- 성능 면에서 수동 어셈블리를 선호하며, intrinsics는 보통 10~15% 느리고 dav1d의 자동 벡터화는 약 2x, 수동 작성 버전은 8x까지 도달한 사례가 비교됨
- 첫 예제 함수는
x86inc.asm,INIT_XMM sse2,cglobal,movu,paddb,RET를 사용해 두uint8_t버퍼의 16바이트 값을 SIMD로 더한 뒤 첫 번째 버퍼에 다시 저장함
강의의 목적과 전제 지식
- FFmpeg Assembly Language Lesson One은 FFmpeg에서 어셈블리 언어가 작성되는 방식의 기초를 다루고, 컴퓨터 내부에서 실제로 어떤 일이 일어나는지 이해하도록 돕는 입문 강의임
- 필요한 지식은 다음과 같음
- C 언어, 특히 포인터
- C를 모른다면 The C Programming Language 학습 권장
- 고등학교 수학 수준의 스칼라와 벡터, 덧셈, 곱셈 개념
어셈블리 언어와 SIMD
- 어셈블리 언어는 CPU가 처리하는 명령어에 직접 대응하는 코드를 사람이 읽을 수 있게 작성하는 프로그래밍 언어임
- 사람이 읽을 수 있는 어셈블리 코드는 어셈블러를 거쳐 CPU가 이해하는 기계어(machine code) 바이너리 데이터로 변환됨
- FFmpeg의 어셈블리 코드는 대부분 SIMD(Single Instruction Multiple Data) 형태임
- 하나의 명령이 여러 데이터 요소에 동시에 동작함
- 벡터 프로그래밍이라고도 불림
- 일반적인 스칼라 프로그래밍은 한 번에 하나의 데이터 요소를 처리함
- SIMD는 메모리에 순차적으로 배치된 많은 데이터를 다루는 이미지, 비디오, 오디오 처리에 잘 맞음
- FFmpeg에서는 다음 표현들이 거의 같은 의미로 쓰임
assembly functionSIMDvectorise- 손으로 어셈블리 함수를 작성해 여러 데이터 요소를 한 번에 처리한다는 뜻임
FFmpeg가 어셈블리를 직접 쓰는 이유
- 핵심 목적은 멀티미디어 처리 속도 향상임
- 어셈블리 코드 작성으로 10x 이상의 속도 향상이 흔함
- 실시간 비디오 재생에서 끊김을 줄이는 데 중요함
- 에너지 사용을 줄이고 배터리 수명을 늘릴 수 있음
- 비디오 인코딩과 디코딩 함수는 최종 사용자와 데이터센터에서 매우 많이 사용되므로, 작은 개선도 빠르게 누적됨
- FFmpeg는 intrinsics 대신 손으로 작성한 어셈블리를 사용함
- intrinsics는 어셈블리 명령어에 대응하는 C 유사 함수임
- 보통 손으로 쓴 어셈블리보다 10~15% 느림
- 이 수치는 컴파일러에 따라 달라지며, intrinsics 지지자들은 동의하지 않을 수 있음
- Hungarian Notation 사용 때문에 읽기 어렵다는 의견도 있음
- inline assembly는 FFmpeg 일부 구버전 코드나 Linux Kernel 같은 프로젝트에 남아 있을 수 있음
- 별도 파일이 아니라 C 코드 안에 어셈블리를 직접 작성하는 방식임
- FFmpeg 같은 프로젝트에서는 읽기 어렵고, 컴파일러 지원이 넓지 않으며, 유지보수하기 어렵다는 의견이 우세함
- 컴파일러 자동 벡터화만으로 충분하다는 의견은 학습 목적상 무시하라고 권함
- dav1d 프로젝트의 최근 테스트에서 자동 벡터화는 약 2x 속도 향상을 보임
- 손으로 작성한 버전은 8x까지 도달할 수 있었음
문법 범위와 참고 자료
- 강의는 x86 64-bit 어셈블리에 집중함
- amd64라고도 불리며 Intel CPU에서도 동작함
- ARM, RISC-V 같은 다른 CPU용 어셈블리는 향후 확장 가능성이 있음
- x86 어셈블리 문법에는 AT&T와 Intel이 있음
- AT&T 문법은 더 오래되었고 Intel 문법보다 읽기 어렵다고 봄
- 강의는 Intel 문법을 사용함
- 일반적인 책이나 Stack Overflow 같은 온라인 자료는 FFmpeg 어셈블리 참고용으로 특히 유용하지 않을 수 있음
- 손으로 작성한 Intel 문법 어셈블리를 사용하기 때문임
- 많은 온라인 자료가 운영체제 프로그래밍, 하드웨어 프로그래밍, 비-SIMD 코드에 초점을 둠
- FFmpeg 어셈블리는 고성능 이미지 처리에 특화된 독특한 접근임
- The Art of 64-bit assembly의 후반부에 있는 SIMD 명령과 동작을 시각화한 다이어그램은 도움이 될 수 있음
- 질문을 위한 Discord 서버가 제공됨
레지스터 기본 개념
- 레지스터는 CPU 안에서 데이터를 처리하는 영역임
- CPU는 메모리를 직접 연산하지 않고, 데이터를 레지스터로 불러온 뒤 처리하고 다시 메모리에 씀
- 어셈블리에서는 일반적으로 한 메모리 위치에서 다른 메모리 위치로 데이터를 직접 복사할 수 없고, 먼저 레지스터를 거쳐야 함
범용 레지스터
- GPR(General Purpose Register) 은 데이터 또는 메모리 주소를 담을 수 있는 범용 레지스터임
- 여기서는 최대 64-bit 값을 담을 수 있음
- 포인터도 담을 수 있음
- 덧셈, 곱셈, 시프트 같은 연산이 가능함
- 많은 어셈블리 책은 GPR의 세부 사항과 역사적 배경을 길게 다룸
- FFmpeg의 어셈블리 코드에서 GPR은 주로 비계(scaffolding) 처럼 쓰이며, 복잡성 대부분은 필요하지 않거나 추상화됨
벡터 레지스터와 데이터 크기
- 벡터 레지스터는 여러 데이터 요소를 담음
- x86의 주요 벡터 레지스터는 다음과 같음
mm: MMX 레지스터, 64-bit 크기, 역사적이며 현재는 많이 쓰이지 않음xmm: XMM 레지스터, 128-bit 크기, 널리 사용 가능ymm: YMM 레지스터, 256-bit 크기, 사용할 때 일부 복잡성이 있음zmm: ZMM 레지스터, 512-bit 크기, 사용 가능성이 제한적임
- 비디오 압축과 해제의 대부분 계산은 정수 기반이므로 강의도 정수에 집중함
- 하나의
xmm레지스터 128-bit는 다음처럼 해석될 수 있음- 16개의 byte, 각 8-bit
- 8개의 word, 각 16-bit
- 4개의 doubleword, 각 32-bit
- 2개의 quadword, 각 64-bit
- 약어는 이후 중요하게 쓰임
- byte: 8-bit 데이터
- word: 16-bit 데이터
- doubleword: 32-bit 데이터
- quadword: 64-bit 데이터
- double quadword: 128-bit 데이터
x86inc.asm의 역할
x86inc.asm은 FFmpeg, x264, dav1d에서 사용하는 가벼운 추상화 계층임- 어셈블리 프로그래머가 코드를 쉽게 작성하도록 돕는 여러 기능을 제공함
- 초반에 중요한 기능 중 하나는 GPR에
r0,r1,r2같은 라벨을 붙여주는 것임- 실제 레지스터 이름을 기억하지 않아도 됨
- FFmpeg에서 GPR이 주로 비계 역할을 하기 때문에 작성 부담이 줄어듦
간단한 스칼라 asm 예제
mov r0q, 3
inc r0q
dec r0q
imul r0q, 5
- 첫 줄은 즉시값(immediate value)
3을r0레지스터에 quadword로 저장함- 즉시값은 메모리에서 가져오는 값이 아니라 어셈블리 코드 자체에 저장된 값임
- Intel 문법에서는 오른쪽의 소스 피연산자가 왼쪽의 목적지 피연산자로 전달됨
r0q = 3처럼 읽을 수 있음memcpy의 동작과 비슷한 순서임
r0q의q접미사는 레지스터가 quadword로 사용됨을 나타냄- 이후 동작은 다음과 같음
inc로 값이 4가 됨dec로 값이 다시 3이 됨imul로 5를 곱해 최종적으로r0q는 15가 됨
mov,inc같은 사람이 읽을 수 있는 명령어는 mnemonic이라고 부름- 어셈블러가 이를 기계어로 변환함
- 대문자
MOV,INC와 소문자mov,inc는 같음 - FFmpeg에서는 mnemonic은 소문자로 쓰고, 대문자는 매크로용으로 남김
첫 SIMD 함수 예제
%include "x86inc.asm"
SECTION .text
;static void add_values(uint8_t *src, const uint8_t * src2)
INIT_XMM sse2
cglobal add_values, 2, 2, 2, src, src2
movu m0, [srcq]
movu m1, [src2q]
paddb m0, m1
movu [srcq], m0
RET
- 이 함수는
src와src2의 데이터를 SIMD로 더하고, 결과를src위치에 다시 저장함 %include "x86inc.asm"은 x264, FFmpeg, dav1d 커뮤니티에서 개발한 헬퍼, 미리 정의된 이름, 매크로를 포함함SECTION .text는 실행할 코드가 놓이는 섹션을 나타냄- 상수 데이터는
.data섹션에 둘 수 있음
- 상수 데이터는
;static void add_values(uint8_t *src, const uint8_t * src2)는 C 함수 인자 형태를 보여주는 주석임- 어셈블리에서 세미콜론
;은 C의//와 같은 주석 역할을 함
- 어셈블리에서 세미콜론
INIT_XMM sse2는 XMM 레지스터와sse2명령어 집합을 사용하도록 함paddb가sse2명령이기 때문임
cglobal add_values, 2, 2, 2, src, src2는 C 함수add_values를 정의함- 함수 인자는 2개임
- 함수에서 사용할 GPR은 인자를 포함해 2개임
- 사용할 XMM 레지스터 수는 2개임
- 마지막 두 항목은 함수 인자 라벨
src,src2임 - 오래된 코드는 인자 라벨 없이
r0,r1같은 GPR을 직접 사용할 수 있음
load, packed add, store
movu m0, [srcq]
movu m1, [src2q]
movu는movdqu의 축약형이며, move double quad unaligned를 뜻함- 정렬(alignment)은 이후 강의에서 다루며, 여기서는
[srcq]에서 128-bit를 이동하는 명령으로 보면 됨 mov에서 대괄호는 주소를 역참조한다는 뜻임- C에서
*src와 비슷한 개념임 - 이 동작은 load임
- C에서
q접미사는 포인터 크기를 가리킴- 64-bit 시스템에서는 C의 포인터 크기인 8바이트를 나타냄
- x86asm은 32-bit 시스템에서는 32-bit를 사용함
- 실제 load는 128-bit임
- 벡터 레지스터는
xmm0같은 전체 이름 대신 추상화된m0으로 참조함- 이후 강의에서 같은 코드를 여러 SIMD 레지스터 크기에 대응하게 하는 방식과 연결됨
paddb m0, m1
paddb는 각 레지스터의 byte 요소를 서로 더함p접두사는 packed를 뜻하며, 벡터 명령과 스칼라 명령을 구분하는 데 쓰임b접미사는 byte 단위 덧셈을 나타냄- 16개 byte가 들어 있는 두 레지스터를 더하면 각 위치별로
a+q,b+r,c+s처럼 대응 요소끼리 더해짐
movu [srcq], m0
RET
movu [srcq], m0는 결과 데이터를srcq포인터가 가리키는 주소에 다시 씀- 이 동작은 store임
RET는 함수가 반환됨을 나타내는 매크로임- FFmpeg의 거의 모든 어셈블리 함수는 값을 반환하기보다 인자로 받은 데이터를 수정함
- 과제에서는 사용 가능한 어셈블리 함수에 대해 함수 포인터를 만들고 이를 사용하는 방식이 이어짐
댓글과 토론
Hacker News 의견들
-
같은 주제의 다른 자료: https://blogs.gnome.org/rbultje/2017/07/14/writing-x86-simd-...
여기 댓글을 보면 손으로 작성한 SIMD의 유용성이 “완전히 불분명”부터 “임무 필수”까지 갈리는데, “완전히 불분명” 쪽이 더 많아 보여서 임무 필수 쪽을 조금 설명해 봄
FFmpeg는 사용 빈도 때문에 명확한 사례지만, 범용 프로덕션 AV1 디코더인 dav1d가 손작성 SIMD의 영향을 정량화하기 더 쉬운 예라고 봄
dav1d는 주요 브라우저부터 Android 운영체제까지 거의 everywhere에서 쓰이며 libgav1을 대체했고, 성공의 큰 이유는 엄청난 속도인데 이는 코드베이스 상당 부분이 손작성 SIMD라서 가능함
Zig 같은 언어가 내장 SIMD 지원을 갖는 건 좋지만, 잠재적인 성능 차이도 조사할 가치가 있는 일부 용도에서는 직접 작성이 필요해짐. dav1d의 어떤 코드 줄은 하루에 수조 번 실행되므로 최대한 빨라야 하고, 손작성 SIMD와 컴파일러 생성 SIMD의 차이가 경우에 따라 최대 50%까지 날 수 있어 중요함
나도 비슷하게 아주 많이 실행될 코드를 쓰는 일에 어느 정도 관여하고 있어서, 이런 기술이 사라지지 않게 하려면 FFmpeg 어셈블리 언어 학교 같은 자료가 꽤 중요하다고 봄- dav1d의 재미있는 점 중 하나는 어셈블리로 작성되어 있어서 자체 호출 규약을 쓸 수 있다는 것임
메서드마다 호출 규약이 달라질 수도 있어서, 일반 플랫폼 호출 규약을 따르는 컴파일러가 생성하는 코드보다 스택 저장과 로드가 훨씬 적음 - 나도 임무 필수 쪽이지만, 흥미로운 반례도 있음. 작은 세부사항에 집중하거나 우발적 복잡성에 빠지면 알고리즘 최적화가 더 안 보일 수 있고, 플랫폼별 코드가 너무 많아 변경 마찰이 커지면 국소 최솟값에서 벗어나기 어려워짐
예를 들어 우리 새 행렬 곱셈은 LLM 추론용 유명 라이브러리보다 빠르며, 상대가 AMX를 쓰고 우리가 AVX512BF16을 써도 그런 경우가 있음. 이유는 스레딩 병목 같기도 하고 다른 원인일 수도 있는데, JIT가 끼면 파악이 어려움
플랫폼별 커널을 직접 써야 했다면 이런 결과는 나오지 않았을 것임. 하루 시간은 한정되어 있고, Highway로 단일 구현을 작성한 덕분에 새 커널 유형과 블록 크기뿐 아니라 병렬화 전략과 그 매개변수까지 고르는 자동 튜너를 포함해 더 넓은 설계 공간을 탐색할 수 있었음
두 번째 단계에서 일부를 손으로 튜닝할 수는 있겠지만, 레지스터 할당과 호출 규약을 미세 최적화하기 전에 더 넓은 탐색이 먼저 이뤄지길 바람 - Zig가 사소한 산술 연산 오버로드를 넘어서 내장 SIMD 지원으로 뭘 제공하는지 궁금함. SIMD 유용성의 90%는 그런 단순 연산 바깥에 있음
Zig는 좋아하지만, 대부분의 경우 C/C++처럼 CPU별 내장 함수를 써야 한다고 이해하고 있음
GCC와 Clang은vector_size속성과 그 “벡터화된” 타입의 산술 연산자 오버로드를 지원하고, 그 외에도 훨씬 많음. 실제로_mm256_mul_ps같은 인트린식도#define _mm256_mul_ps(a,b) (__m256)((v8sf)(a) * (v8sf)(b))처럼 구현됨
이런 기능들의 유용성은 Zig에서 가능한 것보다 훨씬 큼 - 정확한 지적임. 반대편인 인코더에서도 비슷한 이유로 손작성 SIMD를 아주 많이 씀
인코더 쪽에서는 특히 루프와 로드를 조기에 제거할 수 있도록 문제를 “구조화”해야 하는 경우가 많은데, 컴파일러는 그런 종류의 자동 벡터화 코드를 생성하지 못함
- dav1d의 재미있는 점 중 하나는 어셈블리로 작성되어 있어서 자체 호출 규약을 쓸 수 있다는 것임
-
예전에는 핵심 함수의 SIMD 버전을 꽤 많이 만들었지만, 지금은 거의 안 함. 시도해볼 만한 방법은 해당 코드를 분리해서 훌륭한 Compiler Explorer [0]에서 돌려보는 것임
그리고 생성된 코드를 들여다보면 됨
요즘은 자동 벡터화가 함수의 SIMD 버전을 꽤 훌륭하게 생성하는 경우가 많고, 컴파일러에 “힌트”만 주면 되는 때가 많음. 예를 들어 정렬을 명시하거나, 벡터 소스/대상 타입을 직접 제공하는 식임
컴파일러가 무엇을 할 수 있을지 생각하면서 C 코드를 “스타일링”하면 많은 걸 할 수 있음. 중간 변수를 더 쓰고, 원하는 연산을 정말 잘게 쪼개는 식임
정말 최악으로 컴파일러가 충분히 똑똑하지 않더라도, 생성된 어셈블리를 바탕으로 보일러플레이트를 직접 쓰지 않고 수정할 수 있음
대부분의 경우 결과 C 함수는 내가 손으로 짠 것만큼 또는 더 잘 벡터화되고, 그 외 많은 경우에도 “충분히 가까워서” 큰 차이가 안 남. 또 그 코드는 WASM이나 NEON 등에도 명시적 버전 없이 잘 벡터화될 가능성이 큼
[0] https://godbolt.org/- 우리도 약간 비슷하게 했음. 아주 드물게 분리된 부분, 예를 들어 GPU 드라이버에서 지원되지 않거나 GPU 작업을 띄울 만큼 크지 않은 이미지 업로드/다운로드와 변환 같은 경우에는 처음에 C로 작성하고, 원하는 코드를 생성하도록 정렬이나 허용되는 포인터 별칭 같은 컴파일러 주석을 사용했음
GCC와 Clang은 둘 다 일부 벡터 확장을 지원해서, 흩어진 로드/저장, 셔플, 단일 레지스터의 원소 마스킹처럼 “순수” C로는 사람이 읽기 쉽고 컴파일러 버전 사이에서도 항상 기대한 코드를 만들도록 명확히 표현하기 어려운 것들을 어느 정도 이식 가능하게 구현할 수 있음
하지만 다른 컴파일러와 플랫폼도 지원해야 해서, 실제 빌드에서는 결국 그 소스 파일에서 생성된 어셈블리를 가져와 쓰게 됨 - 반례로, 컴파일러가 잘 자동 벡터화하지 못하는 사소한 경우를 정기적으로 마주침: https://gcc.godbolt.org/z/rjEqzf1hh
이것은 부호 없는 바이트 포화 덧셈임. x86-64와 ARM64 모두에서 PADDUSB와 UQADD.16B라는 단일 명령으로 직접 지원됨
하지만 모든 컴파일러가 직관적인 설명에서 엉망을 만들며, 벡터화에 실패하거나 필요 이상으로 크고 느린 벡터화 코드를 생성함
이건 기본적이고 단순한 벡터화 원시 연산인데도 이렇다. 반올림 좁힘 포화 오른쪽 시프트(UQRSHRN) 같은 더 복잡한 명령을 컴파일러가 쓰게 만드는 건 어렵거나 거의 불가능함 - 문제는 컴파일러 출력을 살피고 기대와 비교해야 한다는 것임. 직접 썼을 코드와 맞을 때까지 조금 만져야 할 수도 있음
보통은 그냥 직접 쓰는 편이 더 빠름 - 내 경험상 자동 벡터화는 온갖 조건에서 조용히 실패하는 취약한 최적화라서 의존하고 싶지 않음
- 일반적인 경우에 컴파일러가 16바이트보다 넓은
pshufb를 생성하게 하는 방법을 모르겠음
16바이트 폭의 경우에도pshufb의 실제 정의를 쓰면pshufb를 얻지 못하고, 정의되지 않은 동작이 있는 버전을 쓰면pshufb가 나옴
- 우리도 약간 비슷하게 했음. 아주 드물게 분리된 부분, 예를 들어 GPU 드라이버에서 지원되지 않거나 GPU 작업을 띄울 만큼 크지 않은 이미지 업로드/다운로드와 변환 같은 경우에는 처음에 C로 작성하고, 원하는 코드를 생성하도록 정렬이나 허용되는 포인터 별칭 같은 컴파일러 주석을 사용했음
-
이 강의의 작성자임
뭐든 물어봐도 됨- ARM Mac 사용자로서 궁금한데, 이런 최적화 코드가 모든 플랫폼에서 똑같이 동작하게 하려면 얼마나 많은 노력이 드는가? 아주 철저한 테스트와 대체 알고리즘이 있을 것 같음
어셈블리가 그렇게 많다면 FFmpeg가 내 Mac에서 동작하는 게 기적처럼 보임. 손으로 포팅한 건가? - 어셈블리는 겨우 읽을 수 있지만, 아이디어를 어셈블리로 분해해서 쓰는 감각은 아직 없는 사람으로서 질문이 있음. 이를 배우거나 향상할 방법을 추천할 수 있나?
어느 시점에 “이건 어셈블리를 쓰면 빨라질 수 있겠다”고 깨닫는지 궁금함. 어셈블리로 만들면 정말 빠를 함수를 찾았다면 어떻게 작성하나? 컴파일러가 어셈블리로 바꾼 출력을 가져와 시작하나, 아니면 처음부터 쓰나? 그게 중요하기는 한가? - ARM SVE나 RISC-V V 확장 같은 가변 폭 SIMD 명령어 집합을 어떻게 보는지 궁금함
개발자 사용성과 코드 성능은 전통적인 SIMD와 어떻게 비교되나? 프로그래밍해야 할 SIMD 명령어 집합 종류가 줄어드는 세계로 가고 있나? - FFmpeg는 Windows에서 어셈블리 함수용 SEH 테이블을 어떻게 생성하나?
x86asm.inc가 처리하는 건지, 아니면 신경 쓰지 않는 건지 궁금함 - 90년대에 x86 최적화 코드를 직업적으로 작성했던 입장에서, 2025년에도 여전히 수동으로 해야 하나?
테스트만 작성하고 LLM이 10,000가지 알고리즘을 시도한 뒤 결과를 프로파일링하게 할 수는 없나?
아니면 10,000개의 무작위 시드를 줘도 LLM이 최적해를 찾기 어려운가?
그냥 궁금해서 묻는 것임. x86을 손으로 최적화하는 건 쉽지 않음. 생각하려면 모든 레지스터를 머릿속에 맞춰 넣고 조합을 따져야 하며, 각 명령 조합이 얼마나 걸릴지도 알아야 함. 또 일부 명령에는 사람이 감안하기 어려운 이상한 예외 상황이 있어서 훨씬 오래 걸리거나 훨씬 빨라질 때가 있음
- ARM Mac 사용자로서 궁금한데, 이런 최적화 코드가 모든 플랫폼에서 똑같이 동작하게 하려면 얼마나 많은 노력이 드는가? 아주 철저한 테스트와 대체 알고리즘이 있을 것 같음
-
실제로 해본 사람들에게 궁금함. 어셈블리를 배우거나 구현하는 데 LISP나 RISC-V처럼 어떤 즐거움이 있나, 아니면 특정 시스템 작업 때문에 COBOL을 배우는 것처럼 다른 일을 하기 위해 배우는 건가?
늘 흥미는 있었지만 일상 업무에서 파고들 이유가 딱히 없음. 재미로 시간을 투자할 가치가 있는지 궁금함- 더 배우고 싶어서 이 튜토리얼의 처음 27장을 했는데 정말 즐거웠음: https://mariokartwii.com/armv8/
이제는 어셈블리 코딩을 꽤 좋아함. 튜토리얼 이후로는 C에서 호출할 수 있는 배열 라이브러리를 만든 것 말고는 많이 하진 않았음
이 수준에서는 남는 마법이 거의 없어서 재미있는 것 같음. 정말 무슨 일이 일어나야 하는지 정확히 말하고, 보이는 것이 대체로 그대로 결과가 됨
링킹도 훨씬 잘 이해하게 됐고, 높은 수준에서는 알았지만 세부가 흐릿했던 것들도 이해에 도움이 됨
이제 이 FFmpeg 튜토리얼도 확인해보고 싶음. 이건 ARM이 아니라 x86이라서 - 어셈블리 언어 하나라도 배우는 건 아주 보람 있음. 실제 프로그래밍의 가장 원시적인 형태와 맞닿게 해주기 때문임
튜링 기계나 람다 계산처럼 더 단순한 이론 모델도 있지만, 프로그래머가 실제로 다루는 아키텍처에는 어느 정도 관대한 특성이 있음
겁낼 대상은 아님. 어셈블리는 복잡하다기보다 장황함. 하는 일마다 로드와 저장, 로드와 저장이 수없이 반복됨
매크로와 빌드 시점 검사를 조금 추가하거나, “어셈블리 덩어리를 실행”하는 인터프리터를 둘러 대화형 개발과 스크립팅을 가능하게 하는 Forth 시스템 맥락에 넣으면 C와도 크게 멀지 않고, 컴파일러의 마법을 제거해줌
레트로하게 접근하는 것도 추천함. 에뮬레이터 속 8비트 머신은 작업 모델을 작고 문서가 잘 된 영역에 가둬주며, 제약 덕분에 더 많은 작업을 어셈블리로 해보는 사고가 가치 있어짐. 32비트 이후 아키텍처에서 자원이 풍부할 때는 이런 경우가 흔치 않음
업무로 어셈블리를 개발하는 사람들은 더 구체적인 선호가 있겠지만, 초보자에게는 문서와 예제가 좋은 환경이 가장 필요함. Rosetta Code에는 학습용으로 쓸 만한 좋은 어셈블리 예제가 있음 - 재미있는 점 하나는 생각보다 더 고수준이라는 것임. 실제 칩은 분기 예측과 파이프라이닝 같은 일을 해서, 사람이 아주 조금밖에 제어할 수 없기 때문임
대학 수업에서 특정 작업에 대해 가장 성능 좋은 어셈블리 프로그램을 누가 만드는지 경쟁했던 기억이 있음. 모두가 최고의 성능을 짜내고 나쁜 분기 예측을 피하도록 여러 루프 펼치기 변형을 시도했음
제출 전날 밤 Ballmer Peak에 도달했는지는 모르겠지만, 다른 사람들이 대부분 놓친 구성을 시도해서 간발의 차로 우승했음
또한 https://github.com/chrislgarry/Apollo-11을 보고 “이건 Unix 시스템이네, 나 이거 알아!”라고 농담할 수 있는 엄청난 즐거움도 있음. 인류가 달에 간 언어를 읽을 수 있다는 경이로움은 사라지지 않음
짧게 답하면, 그렇다 - 어셈블리를 배운 것은 나에게 깊은 의미가 있었음. 30년 코딩하면서 실제로 쓴 적은 없지만, 트랜지스터에서 고수준 프로그래밍까지 그림을 완성해줬기 때문임
트랜지스터, 논리 게이트, CPU 아키텍처, 고수준 프로그래밍이 어떻게 맞물리는지 이해하는 순간은, 직업적으로 어셈블리를 전혀 쓰지 않더라도 들인 노력의 가치가 있음 - 지난 약 25년 동안 재미있어서 어셈블리에 깊이 빠져 있었음
가끔 유용하기도 하지만, 마지막 바이트 하나까지 제자리에 넣거나, 수십 년 동안 아무도 들여다보지 않은 바이너리를 파헤치거나, 예전에는 불가능했던 에뮬레이터를 만드는 데서 오는 즐거움이 큼
처음 시작했을 때처럼 아직도 마법을 느끼는 몇 안 되는 분야 중 하나임
- 더 배우고 싶어서 이 튜토리얼의 처음 27장을 했는데 정말 즐거웠음: https://mariokartwii.com/armv8/
-
개인적으로는 인트린식 대신 어셈블리를 직접 작성하는 데 큰 가치는 없다고 보지만, 어셈블리를 읽는 것은 정말 도움이 됐음
Compiler Explorer(https://godbolt.org/)를 자주 써서 생성된 어셈블리를 보고, 컴파일러가 성능 최적화 시 수행하는 최적화를 이해했음- 그 말은 글 내용과 직접 모순됨
“멀티미디어 처리를 빠르게 하기 위해서다. 어셈블리 코드를 작성하면 10배 이상의 속도 향상을 얻는 일이 매우 흔하며, 끊김 없이 실시간으로 비디오를 재생하려 할 때 특히 중요하다”
- 그 말은 글 내용과 직접 모순됨
-
K&R 참조가 반가웠음. C와 프로그래밍 전반을 배우려고 샀던 책이었음
처음에는 C++를 첫 언어로 시도했지만, 내부에서 무슨 일이 일어나는지 계속 궁금해서 너무 추상적으로 느껴졌고 배우기 어려웠음 -
이 자료는 딱 좋음. 386 시절의 x86 어셈블리는 알았지만, 더 고급 프로세서에서는 너무 복잡했음
최신 CPU의 SIMD를 더 배우고 싶었는데, 훌륭한 자료로 보임 -
“
q접미사는 포인터 크기를 가리킨다는 점에 유의하라 *(*즉 C에서는 64비트 시스템에서 *sizeof(*src) == 8을 의미하며, x86asm은 32비트 시스템에서는 32비트를 쓰도록 충분히 똑똑하다) 하지만 실제 로드는 128비트다”라는 문장이 혼란스러움
i.e는i.e.,가 되어야 할 것 같고,*(*는 무슨 뜻인가? 그냥 여는 괄호여야 하지 않나?
어떤 맥락에서*sizeof(*src)가 유효하다고 볼 수 있는지도 모르겠음. 내가 알기로sizeof는 포인터를 산출하지 않음
그 문장에 임의의 별표가 뿌려졌거나, 이탤릭을 뜻하는 별표와 C 문법을 섞으려다 잘못된 느낌임- 맞음. Markdown 자체나 원본 자료를 Markdown으로 변환하는 과정에서 뭔가 잘못된 것처럼 보임
- 처음 두 별표는 각주 쌍처럼 쓰인 것 같음
- 포인터의 크기를 반환하지 않나? 아키텍처 차이를 처리하려고만 쓰인 것 같음
-
분할 방식은 신경 안 쓰고, 이 가이드가 정말 좋다고 말하고 싶음
예전에 아주 저수준에 관심 있을 때 이런 자료가 있었으면 좋았을 것 같음 -
어셈블리가 C보다 10배 빠르다고? 어느 시점에는 분명 사실이었겠지만 지금도 그런가? 컴파일러가 정말 그렇게 정체돼서 손으로 짠 어셈블리에 가까이도 못 가는 건가?
- 인트린식을 쓴 C는 순수 어셈블리 성능에 매우 가까워질 수 있음
FFmpeg 개발자들은 인트린식에 다소 악명 높게 반대하는 편이고, 기억이 맞다면 동등한 어셈블리만큼 성능이 좋아도 코드베이스에서 허용하지 않음. 그래도 글 자체 추정으로도 인트린식과 어셈블리의 차이는 대략 10~15% 수준임
꼼꼼히 최적화한 어셈블리와 순진한 C를 비교하면, 벡터화가 가능하지만 컴파일러가 활용하지 못하는 경우 10배 차이를 볼 수 있음. 이런 일은 흔한데, 자동 벡터화는 사소한 경우를 넘어서면 아직 대체로 별로임
다만 전문가가 짠 코드가 순진한 코드를 압도하는 건 놀랄 일은 아님 - 컴파일러 정체의 문제가 아님. 컴파일러는 어셈블리 작성자가 설계에 활용하는 정보를 알 수 없음
더 간단히 말하면, C 컴파일러는 평범한 C 구현만 보고 특정 수학을 더 효율적인 SIMD 인트린식으로 표현하려 한다는 걸 추론할 수 없음. 작성자가 하려는 수학적 의도에 접근할 수 없기 때문임
대상별 고려사항도 있음. 컴파일러는 필연적으로 범용 컴파일러임. 자원, 예를 들어 레지스터 할당 같은 문제는 NP-완전이고 배낭 문제와 동등함. 컴파일러가 절대적으로 최적인 어셈블리 생성을 찾으려고 몇 시간씩 쓰길 원하는 사람은 거의 없음. 정적으로 그 최적성을 알 수 있는지도 별개임 - 이건 CPU를 완전히 활용하기 위해 가능한 모든 해킹을 쓰는 고도로 벡터화된 코드에 관한 것임
컴파일러는 일반 코드에서는 똑똑하지만, 코덱은 일반 코드가 아님. FFmpeg 프로그래머는 아니지만 오디오를 다뤄본 배경이 있음 - C 컴파일러는 아직도 자동 벡터화를 꽤 못함
SIMD가 적용 가능한 문제에서는 순진한 스칼라 구현보다 2배에서 16배 정도 빨라지는 걸 합리적으로 기대할 수 있음 - 아마 매우 틈새적인 것들이 있을 것임
내가 C보다 10배 나은 어셈블리를 쓸 수는 없지만, 아무도 못 한다고 가정하진 않겠음
- 인트린식을 쓴 C는 순수 어셈블리 성능에 매우 가까워질 수 있음