2 điểm bởi GN⁺ 2025-02-23 | 1 bình luận | Chia sẻ qua WhatsApp
  • Bài giảng assembly đầu tiên của FFmpeg giới thiệu ở mức nhập môn vì sao SIMD assembly viết tay lại cần thiết trong xử lý đa phương tiện và các quy ước viết hàm theo phong cách FFmpeg
  • Độc giả mục tiêu cần biết con trỏ C và các khái niệm scalar·vector, cộng·nhân ở mức toán phổ thông; bài giảng được trình bày dựa trên x86 64-bit và cú pháp Intel
  • Trong FFmpeg, assembly function, SIMD và vectorise gần như được dùng với cùng một nghĩa, chỉ cách xử lý nhiều phần tử dữ liệu cùng lúc, rất phù hợp với xử lý ảnh, video và âm thanh
  • Về hiệu năng, dự án ưu tiên assembly thủ công; intrinsics thường chậm hơn khoảng 10~15%, auto-vectorization trong dav1d đạt khoảng 2x, còn bản viết tay có trường hợp đạt tới 8x
  • Hàm ví dụ đầu tiên dùng x86inc.asm, INIT_XMM sse2, cglobal, movu, paddb, RET để cộng SIMD 16 byte từ hai bộ đệm uint8_t rồi ghi lại vào bộ đệm thứ nhất

Mục tiêu của bài giảng và kiến thức nền

  • FFmpeg Assembly Language Lesson One là bài giảng nhập môn trình bày những điều cơ bản về cách viết assembly trong FFmpeg, đồng thời giúp hiểu điều gì thực sự diễn ra bên trong máy tính
  • Kiến thức cần có như sau
    • Ngôn ngữ C, đặc biệt là con trỏ
    • Nếu chưa biết C thì nên học The C Programming Language
    • Các khái niệm scalar và vector, cộng, nhân ở mức toán trung học phổ thông

Assembly và SIMD

  • Ngôn ngữ assembly là ngôn ngữ lập trình cho phép con người viết mã tương ứng trực tiếp với các lệnh mà CPU xử lý
  • Mã assembly mà con người đọc được sẽ đi qua assembler để được chuyển thành dữ liệu nhị phân mã máy (machine code) mà CPU có thể hiểu
  • Mã assembly trong FFmpeg phần lớn ở dạng SIMD (Single Instruction Multiple Data)
    • Một lệnh tác động đồng thời lên nhiều phần tử dữ liệu
    • Cũng được gọi là lập trình vector
    • Lập trình scalar thông thường xử lý một phần tử dữ liệu tại một thời điểm
  • SIMD rất phù hợp với xử lý ảnh, video và âm thanh, nơi phải làm việc với nhiều dữ liệu được bố trí liên tiếp trong bộ nhớ
  • Trong FFmpeg, các cách gọi sau gần như mang cùng một nghĩa
    • assembly function
    • SIMD
    • vectorise
    • Tức là viết hàm assembly thủ công để xử lý nhiều phần tử dữ liệu cùng lúc

Vì sao FFmpeg viết assembly trực tiếp

  • Mục tiêu cốt lõi là tăng tốc xử lý đa phương tiện
    • Viết assembly thường mang lại mức tăng tốc trên 10x
    • Điều này quan trọng để giảm giật hình trong phát video thời gian thực
    • Đồng thời có thể giảm điện năng tiêu thụ và kéo dài thời lượng pin
  • Các hàm mã hóa và giải mã video được dùng cực nhiều bởi người dùng cuối lẫn trung tâm dữ liệu, nên ngay cả cải thiện nhỏ cũng tích lũy rất nhanh
  • FFmpeg dùng assembly viết tay thay vì intrinsics
    • Intrinsics là các hàm kiểu C tương ứng với lệnh assembly
    • Thường chậm hơn assembly viết tay khoảng 10~15%
    • Con số này có thể thay đổi tùy compiler, và những người ủng hộ intrinsics có thể không đồng ý
    • Cũng có ý kiến cho rằng chúng khó đọc do dùng Hungarian Notation
  • Inline assembly có thể vẫn còn trong một số mã cũ của FFmpeg hoặc các dự án như Linux Kernel
    • Đây là cách viết assembly trực tiếp bên trong mã C thay vì để ở file riêng
    • Với các dự án như FFmpeg, quan điểm phổ biến là cách này khó đọc, compiler hỗ trợ không rộng và khó bảo trì
  • Tài liệu khuyên nên tạm bỏ qua ý kiến cho rằng chỉ cần auto-vectorization của compiler là đủ, để phục vụ mục đích học tập
    • Trong các thử nghiệm gần đây của dự án dav1d, auto-vectorization cho mức tăng tốc khoảng 2x
    • Phiên bản viết tay có thể đạt tới 8x

Phạm vi cú pháp và tài liệu tham khảo

  • Bài giảng tập trung vào assembly x86 64-bit
    • Cũng được gọi là amd64 và vẫn chạy trên CPU Intel
    • Về sau có thể mở rộng sang assembly cho các CPU khác như ARM, RISC-V
  • Cú pháp assembly x86 có AT&T và Intel
    • Cú pháp AT&T lâu đời hơn và được xem là khó đọc hơn cú pháp Intel
    • Bài giảng dùng cú pháp Intel
  • Các sách thông thường hoặc tài liệu trực tuyến như Stack Overflow có thể không đặc biệt hữu ích khi tham khảo assembly của FFmpeg
    • Vì FFmpeg dùng assembly cú pháp Intel viết tay
    • Nhiều tài liệu trực tuyến tập trung vào lập trình hệ điều hành, phần cứng hoặc mã không phải SIMD
    • Assembly của FFmpeg là một cách tiếp cận đặc thù, tối ưu cho xử lý ảnh hiệu năng cao
  • Các sơ đồ trực quan hóa lệnh SIMD và cách chúng hoạt động ở nửa sau của The Art of 64-bit assembly có thể hữu ích
  • Có một máy chủ Discord dành cho câu hỏi

Khái niệm cơ bản về thanh ghi

  • Thanh ghi là vùng bên trong CPU nơi dữ liệu được xử lý
  • CPU không tính toán trực tiếp trên bộ nhớ; nó nạp dữ liệu vào thanh ghi, xử lý rồi ghi lại ra bộ nhớ
  • Trong assembly, thông thường không thể sao chép dữ liệu trực tiếp từ một vị trí bộ nhớ sang vị trí bộ nhớ khác mà phải đi qua thanh ghi trước

Thanh ghi đa dụng

  • GPR (General Purpose Register) là thanh ghi đa dụng có thể chứa dữ liệu hoặc địa chỉ bộ nhớ
    • Ở đây nó có thể chứa giá trị tối đa 64-bit
    • Cũng có thể chứa con trỏ
    • Có thể thực hiện các phép toán như cộng, nhân, dịch bit
  • Nhiều sách assembly nói khá dài về chi tiết GPR và bối cảnh lịch sử của chúng
  • Trong mã assembly của FFmpeg, GPR chủ yếu được dùng như giàn giáo (scaffolding), nên phần lớn sự phức tạp là không cần thiết hoặc đã được trừu tượng hóa

Thanh ghi vector và kích thước dữ liệu

  • Thanh ghi vector chứa nhiều phần tử dữ liệu
  • Các thanh ghi vector chính trên x86 gồm
    • mm: thanh ghi MMX, kích thước 64-bit, mang tính lịch sử và hiện ít dùng
    • xmm: thanh ghi XMM, kích thước 128-bit, được hỗ trợ rộng rãi
    • ymm: thanh ghi YMM, kích thước 256-bit, có một số độ phức tạp khi dùng
    • zmm: thanh ghi ZMM, kích thước 512-bit, khả năng sử dụng còn hạn chế
  • Phần lớn tính toán trong nén và giải nén video là dựa trên số nguyên, nên bài giảng cũng tập trung vào số nguyên
  • Một thanh ghi xmm 128-bit có thể được diễn giải như sau
    • 16 byte, mỗi phần tử 8-bit
    • 8 word, mỗi phần tử 16-bit
    • 4 doubleword, mỗi phần tử 32-bit
    • 2 quadword, mỗi phần tử 64-bit
  • Các viết tắt sau sẽ quan trọng về sau
    • byte: dữ liệu 8-bit
    • word: dữ liệu 16-bit
    • doubleword: dữ liệu 32-bit
    • quadword: dữ liệu 64-bit
    • double quadword: dữ liệu 128-bit

Vai trò của x86inc.asm

  • x86inc.asm là một lớp trừu tượng nhẹ được dùng trong FFmpeg, x264 và dav1d
  • Nó cung cấp nhiều tính năng giúp lập trình viên assembly viết mã dễ hơn
  • Một trong những tính năng quan trọng ở giai đoạn đầu là gán nhãn như r0, r1, r2 cho các GPR
    • Không cần phải nhớ tên thanh ghi thực tế
    • Vì GPR trong FFmpeg chủ yếu đóng vai trò giàn giáo nên việc viết mã bớt nặng nề hơn

Ví dụ asm scalar đơn giản

mov r0q, 3
inc r0q
dec r0q
imul r0q, 5
  • Dòng đầu tiên lưu giá trị tức thời (immediate value) 3 vào thanh ghi r0 dưới dạng quadword
    • Giá trị tức thời là giá trị được lưu ngay trong mã assembly chứ không phải lấy từ bộ nhớ
  • Trong cú pháp Intel, toán hạng nguồn ở bên phải được chuyển sang toán hạng đích ở bên trái
    • Có thể đọc như r0q = 3
    • Thứ tự này tương tự cách memcpy hoạt động
  • Hậu tố q trong r0q cho biết thanh ghi đang được dùng dưới dạng quadword
  • Các bước tiếp theo diễn ra như sau
    • inc làm giá trị thành 4
    • dec đưa giá trị trở lại 3
    • imul nhân với 5 nên cuối cùng r0q là 15
  • Các lệnh dễ đọc như mov, inc được gọi là mnemonic
    • Assembler sẽ chuyển chúng thành mã máy
    • Chữ hoa MOV, INC và chữ thường mov, inc là như nhau
    • Trong FFmpeg, mnemonic được viết thường còn chữ hoa được dành cho macro

Ví dụ hàm SIMD đầu tiên

%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
  • Hàm này cộng dữ liệu từ srcsrc2 bằng SIMD rồi ghi lại kết quả vào vị trí src
  • %include "x86inc.asm" chèn vào các helper, tên định nghĩa sẵn và macro do cộng đồng x264, FFmpeg, dav1d phát triển
  • SECTION .text chỉ phần chứa mã thực thi
    • Dữ liệu hằng có thể đặt trong phần .data
  • ;static void add_values(uint8_t *src, const uint8_t * src2) là chú thích cho thấy dạng tham số của hàm C
    • Trong assembly, dấu chấm phẩy ; là chú thích, tương tự // trong C
  • INIT_XMM sse2 thiết lập việc dùng thanh ghi XMM và tập lệnh sse2
    • paddb là lệnh sse2
  • cglobal add_values, 2, 2, 2, src, src2 định nghĩa hàm C add_values
    • Hàm có 2 tham số
    • Có 2 GPR được dùng trong hàm, bao gồm cả tham số
    • Số thanh ghi XMM sử dụng là 2
    • Hai mục cuối là nhãn tham số hàm src, src2
    • Mã cũ có thể dùng trực tiếp GPR như r0, r1 mà không cần nhãn tham số

load, packed add, store

movu m0, [srcq]
movu m1, [src2q]
  • movu là dạng rút gọn của movdqu, nghĩa là move double quad unaligned
  • Alignment sẽ được nói ở bài sau; ở đây chỉ cần hiểu nó là lệnh di chuyển 128-bit từ [srcq]
  • Trong mov, dấu ngoặc vuông có nghĩa là dereference địa chỉ
    • Tương tự *src trong C
    • Thao tác này là load
  • Hậu tố q chỉ kích thước con trỏ
    • Trên hệ 64-bit, nó biểu thị 8 byte, tức kích thước con trỏ trong C
    • x86asm sẽ dùng 32-bit trên hệ 32-bit
    • Bản thân thao tác load thực tế là 128-bit
  • Thay vì dùng tên đầy đủ như xmm0, thanh ghi vector được tham chiếu qua dạng trừu tượng m0
    • Điều này gắn với cách dùng cùng một đoạn mã cho nhiều kích thước thanh ghi SIMD khác nhau ở các bài sau
paddb m0, m1
  • paddb cộng các phần tử byte tương ứng trong mỗi thanh ghi
  • Tiền tố p nghĩa là packed, dùng để phân biệt lệnh vector với lệnh scalar
  • Hậu tố b cho biết đây là phép cộng theo đơn vị byte
  • Khi cộng hai thanh ghi chứa 16 byte, các phần tử tương ứng sẽ được cộng với nhau như a+q, b+r, c+s
movu [srcq], m0
RET
  • movu [srcq], m0 ghi dữ liệu kết quả trở lại địa chỉ mà con trỏ srcq trỏ tới
    • Thao tác này là store
  • RET là macro biểu thị việc trả về khỏi hàm
  • Gần như mọi hàm assembly trong FFmpeg đều chỉnh sửa dữ liệu nhận qua tham số thay vì trả về giá trị
  • Trong bài tập, phần tiếp theo sẽ tạo function pointer cho các hàm assembly khả dụng và sử dụng chúng

1 bình luận

 
GN⁺ 2025-02-23
Các ý kiến trên Hacker News
  • Tài liệu khác cùng chủ đề: https://blogs.gnome.org/rbultje/2017/07/14/writing-x86-simd-...

    Nhìn các bình luận ở đây, quan điểm về mức độ hữu ích của SIMD viết tay trải từ “hoàn toàn không rõ ràng” đến “thiết yếu cho nhiệm vụ”, nhưng có vẻ phe “hoàn toàn không rõ ràng” đông hơn, nên tôi sẽ giải thích một chút về phía thiết yếu cho nhiệm vụ
    FFmpeg là một ví dụ rõ ràng vì tần suất được dùng, nhưng tôi nghĩ dav1d, một bộ giải mã AV1 production đa dụng, là ví dụ dễ định lượng hơn về tác động của SIMD viết tay
    dav1d được dùng gần như everywhere, từ các trình duyệt lớn đến hệ điều hành Android, đã thay thế libgav1, và lý do lớn cho thành công của nó là tốc độ cực cao, điều có được vì một phần đáng kể codebase là SIMD viết tay
    Việc các ngôn ngữ như Zig có hỗ trợ SIMD tích hợp là tốt, nhưng trong một số trường hợp sử dụng đáng để khảo sát chênh lệch hiệu năng tiềm tàng, việc tự viết trực tiếp trở nên cần thiết. Một số dòng mã trong dav1d được chạy hàng nghìn tỷ lần mỗi ngày nên phải nhanh nhất có thể, và chênh lệch giữa SIMD viết tay với SIMD do trình biên dịch sinh ra có thể lên tới 50% tùy trường hợp, nên điều này rất quan trọng
    Tôi cũng có tham gia ở mức nào đó vào việc viết mã sẽ được chạy cực kỳ nhiều theo cách tương tự, nên tôi nghĩ các tài liệu như FFmpeg Assembly Language School khá quan trọng để những kỹ thuật như thế này không biến mất

    • Một điểm thú vị của dav1d là vì được viết bằng assembly, nó có thể dùng quy ước gọi hàm riêng
      Quy ước gọi hàm thậm chí có thể khác nhau theo từng method, nên số lần lưu và nạp trên stack ít hơn nhiều so với mã do trình biên dịch sinh ra phải tuân theo quy ước gọi hàm chung của nền tảng
    • Tôi cũng thuộc phe thiết yếu cho nhiệm vụ, nhưng cũng có phản ví dụ thú vị. Nếu tập trung vào các chi tiết nhỏ hoặc sa vào độ phức tạp ngẫu nhiên, có thể sẽ khó nhìn thấy hơn các tối ưu hóa thuật toán, và nếu có quá nhiều mã riêng cho từng nền tảng khiến ma sát khi thay đổi tăng lên, sẽ khó thoát khỏi cực tiểu cục bộ
      Ví dụ, phép nhân ma trận mới của chúng tôi nhanh hơn một thư viện nổi tiếng dùng cho suy luận LLM, và có trường hợp vẫn như vậy dù đối thủ dùng AMX còn chúng tôi dùng AVX512BF16. Lý do có vẻ là nút thắt ở threading, hoặc cũng có thể do nguyên nhân khác, nhưng khi có JIT xen vào thì rất khó xác định
      Nếu phải tự viết các kernel riêng cho từng nền tảng, kết quả như vậy đã không xuất hiện. Thời gian trong ngày là có hạn, và nhờ viết một implementation duy nhất bằng Highway, chúng tôi có thể khám phá không gian thiết kế rộng hơn, bao gồm một bộ auto-tuner chọn không chỉ loại kernel mới và kích thước block mà cả chiến lược song song hóa cùng các tham số của nó
      Ở bước thứ hai có thể tinh chỉnh thủ công một phần, nhưng tôi muốn việc khám phá rộng hơn diễn ra trước khi vi tối ưu hóa phân bổ thanh ghi và quy ước gọi hàm
    • Tôi tò mò Zig cung cấp gì với hỗ trợ SIMD tích hợp ngoài việc overload các phép toán số học vụn vặt. 90% tính hữu dụng của SIMD nằm ngoài các phép toán đơn giản như vậy
      Tôi thích Zig, nhưng tôi hiểu rằng trong hầu hết trường hợp vẫn phải dùng các hàm intrinsic theo từng CPU như C/C++
      GCC và Clang hỗ trợ thuộc tính vector_size và overload toán tử số học cho các kiểu “đã vector hóa” đó, ngoài ra còn nhiều thứ hơn nữa. Trên thực tế, các intrinsic như _mm256_mul_ps cũng được triển khai kiểu #define _mm256_mul_ps(a,b) (__m256)((v8sf)(a) * (v8sf)(b))
      Mức hữu dụng của các tính năng này lớn hơn rất nhiều so với những gì có thể làm trong Zig
    • Nhận xét chính xác. Ở phía ngược lại là encoder, SIMD viết tay cũng được dùng rất nhiều vì những lý do tương tự
      Ở phía encoder, đặc biệt thường phải “cấu trúc hóa” bài toán để có thể loại bỏ vòng lặp và load từ sớm, mà trình biên dịch không thể sinh ra loại mã tự động vector hóa như vậy
  • Trước đây chúng tôi từng tạo khá nhiều phiên bản SIMD cho các hàm lõi, nhưng giờ thì hầu như không làm nữa. Một cách đáng thử là tách đoạn mã đó ra và chạy trên Compiler Explorer [0] tuyệt vời
    Rồi xem phần mã được sinh ra
    Ngày nay tự động vector hóa thường tạo ra các phiên bản SIMD của hàm khá tốt, và nhiều khi chỉ cần đưa “gợi ý” cho trình biên dịch. Ví dụ như chỉ rõ căn chỉnh, hoặc cung cấp trực tiếp kiểu nguồn/đích vector
    Có thể làm được rất nhiều nếu “định kiểu” mã C trong khi nghĩ xem trình biên dịch có thể làm gì. Chẳng hạn dùng thêm biến trung gian và chia thật nhỏ các phép toán mong muốn
    Ngay cả trong trường hợp tệ nhất là trình biên dịch không đủ thông minh, bạn vẫn có thể dựa trên assembly được sinh ra để chỉnh sửa mà không phải tự viết phần boilerplate
    Trong phần lớn trường hợp, hàm C kết quả được vector hóa tốt bằng hoặc tốt hơn bản tôi viết tay; trong nhiều trường hợp khác thì cũng “đủ gần” để không tạo khác biệt lớn. Ngoài ra, mã đó cũng có khả năng được vector hóa tốt sang WASM hay NEON mà không cần phiên bản tường minh
    [0] https://godbolt.org/

    • Chúng tôi cũng làm hơi tương tự. Với những phần tách riêng rất hiếm, chẳng hạn upload/download và chuyển đổi ảnh không được driver GPU hỗ trợ hoặc không đủ lớn để khởi chạy một tác vụ GPU, ban đầu chúng tôi viết bằng C, rồi dùng chú thích cho trình biên dịch như căn chỉnh hoặc alias con trỏ được phép để sinh ra mã mong muốn
      Cả GCC và Clang đều hỗ trợ một số phần mở rộng vector, nên có thể triển khai tương đối di động những thứ khó diễn đạt rõ ràng bằng C “thuần” sao cho con người dễ đọc và luôn tạo ra mã như kỳ vọng giữa các phiên bản trình biên dịch, như load/store phân tán, shuffle, masking phần tử trong một thanh ghi đơn
      Nhưng vì còn phải hỗ trợ các trình biên dịch và nền tảng khác, trong bản build thực tế cuối cùng chúng tôi lại lấy assembly được sinh ra từ tệp nguồn đó để dùng
    • Một phản ví dụ: tôi thường xuyên gặp những trường hợp vụn vặt mà trình biên dịch không tự động vector hóa tốt: https://gcc.godbolt.org/z/rjEqzf1hh
      Đây là phép cộng bão hòa byte không dấu. Cả x86-64 và ARM64 đều hỗ trợ trực tiếp bằng một lệnh duy nhất là PADDUSB và UQADD.16B
      Nhưng mọi trình biên dịch đều xử lý phần mô tả trực quan này rất tệ, hoặc không vector hóa được, hoặc sinh ra mã vector hóa lớn và chậm hơn mức cần thiết
      Dù đây là một primitive vector hóa cơ bản và đơn giản mà vẫn vậy. Việc khiến trình biên dịch dùng các lệnh phức tạp hơn như dịch phải bão hòa có làm tròn và thu hẹp (UQRSHRN) thì khó hoặc gần như bất khả thi
    • Vấn đề là phải xem đầu ra của trình biên dịch và so sánh với kỳ vọng. Có thể phải chỉnh sửa một chút cho đến khi khớp với thứ bạn sẽ tự viết
      Thường thì tự viết thẳng còn nhanh hơn
    • Theo kinh nghiệm của tôi, tự động vector hóa là một tối ưu hóa mong manh, âm thầm thất bại trong đủ loại điều kiện, nên tôi không muốn phụ thuộc vào nó
    • Tôi không biết cách khiến trình biên dịch tạo ra pshufb rộng hơn 16 byte trong trường hợp thông thường
      Ngay cả với độ rộng 16 byte, nếu dùng định nghĩa thực sự của pshufb thì cũng không nhận được pshufb; còn nếu dùng phiên bản có hành vi không xác định thì lại có pshufb
  • Tôi là tác giả của bài giảng này
    Có thể hỏi tôi bất cứ điều gì

    • Là người dùng ARM Mac, tôi tò mò không biết cần bao nhiêu công sức để đảm bảo loại mã tối ưu hóa này hoạt động giống nhau trên mọi nền tảng? Có lẽ phải có kiểm thử rất kỹ và thuật toán thay thế
      Nếu có nhiều assembly như vậy thì việc FFmpeg chạy được trên Mac của tôi trông như một phép màu. Nó được port thủ công à?
    • Với tư cách một người chỉ vừa đủ đọc được assembly nhưng chưa có cảm giác phân rã ý tưởng thành assembly để viết, tôi có câu hỏi. Anh có gợi ý cách học hoặc cải thiện kỹ năng này không?
      Tôi tò mò là đến thời điểm nào thì ta nhận ra “cái này có thể nhanh hơn nếu viết bằng assembly”. Nếu tìm được một hàm sẽ thực sự nhanh khi làm bằng assembly thì anh viết thế nào? Lấy đầu ra mà trình biên dịch đã chuyển thành assembly để bắt đầu, hay viết từ đầu? Điều đó có quan trọng không?
    • Tôi tò mò anh nhìn nhận thế nào về các tập lệnh SIMD độ rộng biến thiên như ARM SVE hay phần mở rộng RISC-V V
      Về khả năng sử dụng cho nhà phát triển và hiệu năng mã, chúng so với SIMD truyền thống ra sao? Chúng ta có đang tiến tới một thế giới có ít loại tập lệnh SIMD cần lập trình hơn không?
    • FFmpeg tạo bảng SEH cho các hàm assembly trên Windows như thế nào? Tôi tò mò liệu x86asm.inc xử lý việc đó, hay là không cần quan tâm
    • Từ góc nhìn của người từng viết mã tối ưu hóa x86 chuyên nghiệp vào thập niên 90, đến năm 2025 vẫn phải làm thủ công sao?
      Không thể chỉ viết test rồi để LLM thử 10.000 thuật toán và profile kết quả sao?
      Hay dù đưa 10.000 seed ngẫu nhiên thì LLM vẫn khó tìm được nghiệm tối ưu?
      Tôi chỉ hỏi vì tò mò. Tối ưu hóa x86 bằng tay không dễ. Để suy nghĩ, bạn phải giữ tất cả thanh ghi trong đầu và cân nhắc các tổ hợp, đồng thời biết mỗi tổ hợp lệnh sẽ mất bao lâu. Ngoài ra, một số lệnh còn có những ngoại lệ kỳ lạ mà con người khó tính đến, khiến chúng có lúc chậm hơn hoặc nhanh hơn rất nhiều
  • Muốn hỏi những người đã thực sự thử: khi học hoặc triển khai assembly, có niềm vui nào giống như LISP hay RISC-V không, hay là học nó để làm việc khác, giống như học COBOL vì một số tác vụ hệ thống cụ thể?
    Tôi luôn thấy hứng thú, nhưng trong công việc hằng ngày thì không có lý do rõ ràng để đào sâu. Tò mò không biết có đáng đầu tư thời gian chỉ để cho vui không

    • Vì muốn học thêm, tôi đã làm 27 chương đầu của tutorial này và thấy rất vui: https://mariokartwii.com/armv8/
      Giờ tôi khá thích viết code assembly. Sau tutorial đó thì cũng không làm nhiều, ngoài việc tạo một thư viện mảng có thể gọi từ C
      Tôi nghĩ nó thú vị vì ở mức này gần như không còn nhiều ma thuật sót lại. Bạn thực sự nói chính xác điều gì phải xảy ra, và những gì thấy được phần lớn sẽ đúng như kết quả
      Tôi cũng hiểu linking tốt hơn nhiều, và nó giúp tôi hiểu cả những thứ trước đây ở mức cao thì biết nhưng chi tiết còn mơ hồ
      Giờ tôi cũng muốn xem tutorial FFmpeg này. Cái này là x86 chứ không phải ARM
    • Học dù chỉ một ngôn ngữ assembly cũng rất đáng giá. Vì nó giúp bạn chạm tới hình thức nguyên thủy nhất của lập trình thực tế
      Cũng có những mô hình lý thuyết đơn giản hơn như máy Turing hay lambda calculus, nhưng các kiến trúc mà lập trình viên thực sự làm việc với vẫn có một mức độ “dễ chịu” nhất định
      Không phải thứ đáng sợ. Assembly không hẳn phức tạp mà là dài dòng. Mọi việc đều lặp đi lặp lại nạp và lưu, nạp và lưu vô số lần
      Nếu thêm một chút macro và kiểm tra lúc build, hoặc đặt nó trong bối cảnh một hệ thống Forth bao quanh trình thông dịch “chạy các khối assembly” để cho phép phát triển tương tác và scripting, thì nó cũng không quá xa C, đồng thời loại bỏ phần ma thuật của compiler
      Tôi cũng khuyên nên tiếp cận theo kiểu retro. Một máy 8-bit trong emulator sẽ giới hạn mô hình làm việc trong một phạm vi nhỏ và được tài liệu hóa tốt; nhờ các ràng buộc, việc suy nghĩ làm nhiều thứ hơn bằng assembly trở nên có giá trị. Khi tài nguyên dồi dào trên các kiến trúc từ 32-bit trở đi thì trường hợp này không thường gặp
      Những người viết assembly trong công việc có thể có sở thích cụ thể hơn, nhưng với người mới thì thứ cần nhất là môi trường có tài liệu và ví dụ tốt. Rosetta Code có những ví dụ assembly khá hay để học
    • Một điểm thú vị là nó bậc cao hơn bạn tưởng. Vì chip thực tế làm những việc như dự đoán nhánh và pipelining, mà con người chỉ kiểm soát được rất ít
      Tôi nhớ trong một lớp đại học, chúng tôi từng thi xem ai tạo được chương trình assembly có hiệu năng tốt nhất cho một tác vụ cụ thể. Mọi người đều thử nhiều biến thể unroll vòng lặp để vắt kiệt hiệu năng và tránh dự đoán nhánh tệ
      Không biết đêm trước hạn nộp tôi có đạt Ballmer Peak không, nhưng tôi đã thử một cấu hình mà hầu hết người khác bỏ lỡ và thắng sít sao
      Ngoài ra còn có niềm vui lớn khi xem https://github.com/chrislgarry/Apollo-11 rồi đùa rằng “đây là hệ thống Unix mà, cái này tôi biết!”. Cảm giác kỳ diệu khi có thể đọc được ngôn ngữ đã đưa loài người lên Mặt Trăng thì không mất đi
      Trả lời ngắn gọn là: có
    • Việc học assembly có ý nghĩa sâu sắc với tôi. Trong 30 năm code tôi chưa từng thực sự dùng nó, nhưng nó giúp hoàn thiện bức tranh từ transistor đến lập trình bậc cao
      Khoảnh khắc hiểu được transistor, cổng logic, kiến trúc CPU và lập trình bậc cao khớp với nhau như thế nào đáng giá công sức bỏ ra, dù bạn hoàn toàn không dùng assembly trong nghề nghiệp
    • Trong khoảng 25 năm qua, tôi đã chìm sâu vào assembly vì thấy nó vui
      Thỉnh thoảng nó cũng hữu ích, nhưng niềm vui lớn nằm ở việc đặt từng byte cuối cùng vào đúng chỗ, đào bới một binary mà hàng chục năm không ai nhìn tới, hoặc tạo ra những emulator trước đây tưởng là không thể
      Đây là một trong số ít lĩnh vực mà tôi vẫn cảm thấy ma thuật như hồi mới bắt đầu
  • Cá nhân tôi không thấy có nhiều giá trị trong việc tự viết assembly thay vì dùng intrinsic, nhưng đọc assembly thì thật sự hữu ích
    Tôi thường dùng Compiler Explorer(https://godbolt.org/) để xem assembly được sinh ra và hiểu các tối ưu hóa mà compiler thực hiện khi tối ưu hiệu năng

    • Điều đó mâu thuẫn trực tiếp với nội dung bài viết
      “Mục đích là để xử lý multimedia nhanh hơn. Khi viết code assembly, việc đạt tốc độ nhanh hơn hơn 10 lần là rất phổ biến, và điều này đặc biệt quan trọng khi muốn phát video theo thời gian thực mà không bị giật”
  • Thấy vui khi có tham chiếu K&R. Đó là cuốn sách tôi đã mua để học C và lập trình nói chung
    Ban đầu tôi thử lấy C++ làm ngôn ngữ đầu tiên, nhưng vì cứ tò mò bên trong đang xảy ra gì nên nó cảm giác quá trừu tượng và khó học

  • Tài liệu này vừa đúng thứ cần. Tôi từng biết assembly x86 thời 386, nhưng trên các processor cao cấp hơn thì nó quá phức tạp
    Tôi đã muốn học thêm về SIMD trên CPU hiện đại, và đây có vẻ là một tài liệu tuyệt vời

  • Câu “lưu ý rằng hậu tố q chỉ kích thước con trỏ *(*tức là trong C, trên hệ thống 64-bit, *sizeof(*src) == 8, và x86asm đủ thông minh để dùng 32-bit trên hệ thống 32-bit) nhưng thao tác load thực tế là 128-bit” gây khó hiểu
    i.e có lẽ nên là i.e.,, còn *(* nghĩa là gì? Chẳng phải chỉ nên là dấu mở ngoặc thôi sao?
    Tôi cũng không biết trong ngữ cảnh nào *sizeof(*src) có thể được xem là hợp lệ. Theo tôi biết thì sizeof không tạo ra con trỏ
    Cảm giác như trong câu đó có các dấu sao được rải ngẫu nhiên, hoặc đã bị lỗi khi cố trộn dấu sao biểu thị italic với cú pháp C

    • Đúng vậy. Có vẻ như có gì đó sai trong chính Markdown hoặc trong quá trình chuyển đổi tài liệu gốc sang Markdown
    • Hai dấu sao đầu có vẻ được dùng như một cặp chú thích chân trang
    • Chẳng phải nó trả về kích thước của con trỏ sao? Có vẻ chỉ được dùng để xử lý khác biệt kiến trúc
  • Không quan tâm đến cách chia phần, chỉ muốn nói rằng hướng dẫn này thật sự hay
    Ước gì đã có tài liệu như thế này hồi trước, khi tôi quan tâm đến những thứ rất low-level

  • Assembly nhanh hơn C 10 lần ư? Ở một thời điểm nào đó chắc chắn điều đó từng đúng, nhưng bây giờ vẫn vậy sao? Trình biên dịch thật sự trì trệ đến mức không thể tiệm cận assembly viết tay sao?

    • C dùng intrinsic có thể tiến rất gần đến hiệu năng của assembly thuần
      Các nhà phát triển FFmpeg có tiếng là khá phản đối intrinsic, và nếu tôi nhớ không nhầm thì họ không cho phép đưa vào codebase ngay cả khi hiệu năng tốt ngang assembly tương đương. Dù vậy, ngay cả theo ước tính của bài viết, chênh lệch giữa intrinsic và assembly cũng chỉ khoảng 10–15%
      Nếu so sánh assembly được tối ưu kỹ lưỡng với C ngây thơ, trong những trường hợp có thể vector hóa nhưng trình biên dịch không tận dụng được, có thể thấy chênh lệch 10 lần. Chuyện này khá phổ biến, vì tự động vector hóa nhìn chung vẫn còn khá tệ khi vượt ra ngoài các trường hợp vụn vặt
      Tuy nhiên, việc mã do chuyên gia viết áp đảo mã ngây thơ thì không có gì đáng ngạc nhiên
    • Đây không phải là vấn đề trình biên dịch bị trì trệ. Trình biên dịch không biết được những thông tin mà người viết assembly dùng để thiết kế
      Nói đơn giản hơn, trình biên dịch C không thể chỉ nhìn một triển khai C bình thường mà suy luận rằng tác giả đang muốn biểu diễn một phép toán cụ thể bằng các intrinsic SIMD hiệu quả hơn. Vì nó không có quyền truy cập vào ý đồ toán học của người viết
      Cũng có các cân nhắc theo từng đích. Trình biên dịch tất yếu là trình biên dịch đa dụng. Các bài toán về tài nguyên, chẳng hạn như cấp phát thanh ghi, là NP-complete và tương đương với bài toán cái ba lô. Hầu như không ai muốn trình biên dịch dành hàng giờ để tìm cách sinh assembly tối ưu tuyệt đối. Việc có thể biết tĩnh tính tối ưu đó hay không lại là chuyện khác
    • Đây là về mã được vector hóa ở mức cao, dùng mọi thủ thuật có thể để khai thác CPU đến tối đa
      Trình biên dịch thông minh với mã thông thường, nhưng codec không phải là mã thông thường. Tôi không phải lập trình viên FFmpeg, nhưng có nền tảng từng làm việc với âm thanh
    • Trình biên dịch C vẫn còn khá kém ở tự động vector hóa
      Với các bài toán áp dụng được SIMD, có thể kỳ vọng hợp lý mức nhanh hơn khoảng 2 đến 16 lần so với triển khai scalar ngây thơ
    • Có lẽ sẽ có những thứ rất ngách như vậy
      Tôi không thể viết assembly tốt hơn C 10 lần, nhưng cũng sẽ không giả định rằng không ai làm được