Trình biên dịch cho lập trình viên
Đo tận mắt điều trình biên dịch làm với mã nguồn C của bạn: từ tiền xử lý, tối ưu, sinh assembly, tới liên kết và nạp — mọi thứ chạy thật trong container.
45/45 phần đã đăng
Trình biên dịch
1
8 dòng nở thành 750, và printf lặng lẽ biến thành puts — bốn giai đoạn sau một lệnh gcc
Một lệnh gcc chạy qua bốn bước: tiền xử lý, biên dịch, hợp dịch, liên kết. Đo từng bước, và đọc assembly mới thấy trình dịch lặng lẽ đổi printf thành puts. Đo thật trong container.
03/09/2026
· 8 phút đọc
2
SQ(i++) làm i tăng hai lần, SQ(1+2) ra 5 chứ không phải 9 — vì macro chỉ là dán văn bản
Macro trông như hàm nhưng chỉ là dán văn bản: SQ(i++) khiến i tăng hai lần, SQ_BAD(1+2) ra 5 chứ không phải 9. Và một #include kéo theo 37 file. Đo thật bằng gcc -E trong container.
03/09/2026
· 8 phút đọc
3
Có include guard rồi mà một header vẫn biến mất không dấu vết — vì nó khóa theo tên, không theo file
Include guard chống được include lặp, nhưng nó khóa theo TÊN macro chứ không theo file — hai header trùng tên guard làm một cái biến mất im lặng. Đo thật bằng gcc -E trong container.
03/09/2026
· 8 phút đọc
4
Hai file bất đồng kiểu một biến, cả trình dịch lẫn linker đều không bắt — và hai số 42 trùng nhau suýt lừa tôi
Mỗi .c dịch một mình, không thấy các file khác; linker khớp theo tên chứ không theo kiểu. Một lệch kiểu xuyên file lọt lưới cả trình dịch lẫn linker — chỉ -flto bắt được. Đo thật trong container.
03/09/2026
· 9 phút đọc
5
Thiếu định nghĩa dịch vẫn xong — chỉ hỏng lúc link; và extern kèm khởi tạo lén thành định nghĩa
Khai báo đủ để dịch một file, định nghĩa mới đủ để liên kết — nên thiếu định nghĩa chỉ hỏng lúc link, không phải lúc compile. Và extern kèm khởi tạo lại là định nghĩa, gây multiple definition. Đo thật trong container.
03/09/2026
· 8 phút đọc
6
Benchmark báo -O2 nhanh 490 lần — nhưng đó là công việc bị xóa sạch, không phải tăng tốc
Cú nhảy tốc độ lớn nhất là -O0 sang -O1 (4,2x), sau đó thường phẳng. Và một benchmark không dùng kết quả cho -O2 nhanh 490 lần — nhưng đó là công việc bị xóa, không phải tăng tốc. Đo thật trong container.
03/09/2026
· 8 phút đọc
7
Nội tuyến làm hàm nhỏ nhanh 2,46 lần — và một lệnh grep cẩu thả suýt làm tôi báo cáo sai
Nội tuyến dán thân hàm vào chỗ gọi, bỏ lời gọi và cho tối ưu xuyên ranh giới — hàm nhỏ nhanh 2,46 lần. Nhưng từ khóa inline chỉ là gợi ý, và một grep cẩu thả suýt làm tôi báo sai. Đo thật trong container.
03/09/2026
· 8 phút đọc
8
Tôi viết memset để xóa mật khẩu — trình biên dịch xóa luôn cái memset, bí mật còn nguyên trong RAM
Trình dịch xóa mọi mã không có tác dụng quan sát được — kể cả memset(pw,0,n) bạn viết để xóa mật khẩu, vì buffer không đọc lại sau đó. Bí mật còn nguyên trên stack. Đo thật bằng assembly, và cách xóa an toàn thật.
03/09/2026
· 8 phút đọc
9
Trình biên dịch tính sẵn 60*60*24 thành 86400 — nhưng nó cũng nướng luôn cả bug tràn số của bạn
60*60*24 được tính sẵn thành 86400 lúc dịch, không nhân lúc chạy. Nhưng trình dịch gấp bằng đúng số học của chương trình: 1000000*1000000 ra -727379968, và 0.1+0.2 không bằng 0.3. Đo thật trong container.
03/09/2026
· 8 phút đọc
10
Mở vòng lặp nhanh 3,5 lần — nhưng không phải nhờ bớt nhánh như sách vở nói
-funroll-loops làm vòng lặp nhanh 3,5 lần — nhưng không phải nhờ bớt nhánh như sách vở nói. Đọc assembly thấy nó phá chuỗi phụ thuộc bằng 4 ô tích lũy song song. Đo thật trong container.
03/09/2026
· 8 phút đọc
11
Vector hóa SIMD
-O3 tự vector hóa vòng cộng dồn int nhưng TỪ CHỐI cộng dồn float — vì đổi thứ tự cộng float đổi kết quả IEEE. Bật -ffast-math thì nhanh 4.3 lần. Đo thật bằng lệnh NEON trong assembly.
03/09/2026
· 7 phút đọc
12
objdump báo 'bl 0' — tôi tưởng gọi địa chỉ 0 sẽ crash, hóa ra đó là chỗ trống chờ linker
objdump -d tháo mã máy thật ra ba cột: địa chỉ, byte, lệnh. Nhưng tháo một .o chưa liên kết thấy 'bl 0 <puts>' — số 0 là chỗ trống chờ trình liên kết điền, không phải địa chỉ thật. Đo thật trong container.
03/09/2026
· 8 phút đọc
13
Đối số thứ 3 lại nằm ở thanh ghi x1 chứ không phải x2 — trực giác 'đếm theo vị trí' sai hoàn toàn
Đối số thứ 3 trong f(int a, double b, int c) nằm ở thanh ghi x1 chứ không phải x2 — vì int và float dùng hai dải thanh ghi riêng, đếm độc lập. Đo thật bằng assembly aarch64 trong container.
03/09/2026
· 8 phút đọc
14
-fomit-frame-pointer không bỏ khung ngăn xếp — tôi đọc sai cái tên, prologue nói thật
-fomit-frame-pointer không bỏ khung ngăn xếp — nó chỉ bỏ con trỏ khung x29, tiết kiệm một lệnh và giải phóng một thanh ghi. Khung vẫn được cấp. Đo thật bằng prologue/epilogue trong assembly.
03/09/2026
· 8 phút đọc
15
Tràn thanh ghi làm chậm 12 lần — tôi suýt tin, rồi nhận ra mình đổi hai biến cùng lúc
Khi số giá trị sống cùng lúc vượt số thanh ghi, trình dịch phải đổ bớt ra stack (spill). Nhưng áp lực đến từ giá trị sống đồng thời, không phải số biến khai báo — 40 biến dùng tuần tự vẫn 0 tràn. Và chi phí tràn thật, khi cô lập đúng, chỉ ~3%. Đo thật trong container.
03/09/2026
· 8 phút đọc
16
Gọi một hàm C từ C++ mà không liên kết được — nm phơi ra thủ phạm trong một dòng
nm liệt kê ký hiệu với mã loại T/D/U. C dùng tên thô; C++ nhét chữ ký vào tên (mangling) để cho phép nạp chồng — nên gọi hàm C từ C++ mà quên extern C thì không liên kết được. Đo thật trong container.
03/09/2026
· 8 phút đọc
17
Liên kết tĩnh cho file to gấp 12 lần — và khởi động chậm hơn 1,75 lần, ngược hẳn câu tôi vẫn tin
Liên kết tĩnh cho file to gấp 12 lần — và khởi động CHẬM hơn 1,75 lần, ngược với câu 'tĩnh nhanh hơn'. Vì bản tĩnh phải nạp 522KB mã còn bản động dùng libc đã thường trú. Đo thật trong container.
03/09/2026
· 8 phút đọc
18
Hàm sqrt được liên kết vào chương trình nhưng không bao giờ được phân giải — vì tôi chưa gọi nó
ld.so không phân giải mọi ký hiệu lúc khởi động — lazy binding chỉ phân giải một hàm khi nó được gọi lần đầu, và hàm không gọi thì không bao giờ được đụng tới. Đo thật bằng PLT/GOT và LD_DEBUG trong container.
03/09/2026
· 8 phút đọc
19
Đảo chỗ hai file trên dòng lệnh, chương trình từ dịch được thành 'undefined reference'
Đảo chỗ -lfoo với file .o có thể biến một chương trình dịch được thành 'undefined reference' — nhưng chỉ với thư viện tĩnh .a, không phải .so. Đo thật và giải thích cơ chế rút thành viên một lượt trong container.
03/09/2026
· 8 phút đọc
20
Một mảng 4MB toàn số 0 miễn phí trên đĩa — thêm đúng một số 1, file phình 4MB
int arr[1000000]={0} không tốn byte nào trong file (nằm .bss), nhưng ={1} tốn đúng 4MB (nhảy sang .data). Một phần tử khác 0 kéo cả mảng từ miễn phí sang tốn đĩa. Đo bằng size và nm trong container.
03/09/2026
· 8 phút đọc
21
Khai mảng toàn cục 400MB: file thực thi vẫn 70KB, và lúc chạy chỉ tốn 1MB RAM thật
Một mảng toàn cục 400MB làm file thực thi nhỏ như cũ (nằm .bss) — và bất ngờ hơn, lúc chạy nó chỉ tốn 1MB RAM thật cho tới khi bạn chạm vào từng trang. Kích thước file, VSZ và RSS là ba con số khác nhau. Đo thật trong container.
03/09/2026
· 8 phút đọc
22
Cùng một dòng ép con trỏ: -O0 ra 1073741824, -O2 ra 1 — và cách đúng lại miễn phí
Một đoạn type-pun bằng ép con trỏ cho 1073741824 ở -O0 nhưng 1 ở -O2 — vì -O2 giả định int* và float* không trỏ cùng ô. Đó là hành vi không xác định. Cách đúng là memcpy, và gcc gấp nó thành một lệnh. Đo thật trong container.
03/09/2026
· 8 phút đọc
23
Tôi viết đoạn check để bắt tràn — trình biên dịch xóa sạch nó vì 'tràn không được phép xảy ra'
Một đoạn kiểm tra tràn if(x+100 < x) bị -O2 xóa sạch — vì trình dịch suy tràn signed là UB nên 'không bao giờ xảy ra'. Cái check để bắt bug lại bị compile đi mất, và -fwrapv cứu. Đo thật trong container.
03/09/2026
· 8 phút đọc
24
next() + next() ra 21 mọi lần chạy — nhưng con số ổn định đó là một cái bẫy
next()+next() cho 21 trên gcc/aarch64, nhất quán mọi lần — nhưng thứ tự đó không được chuẩn bảo đảm, và i=i++ + ++i là UB. Nhất quán trên một máy không phải 'được định nghĩa'. Đo thật trong container.
03/09/2026
· 8 phút đọc
25
Vòng while(!done) treo vĩnh viễn ở -O2 — volatile cứu được, nhưng đừng tưởng nó làm biến an toàn đa luồng
Một vòng while(!done) treo vô hạn ở -O2 vì trình dịch đọc done một lần rồi cache; volatile ép đọc lại mỗi vòng. Nhưng volatile chỉ đảm bảo truy cập xảy ra, không phải nguyên tử hay thread-safe. Đo bằng assembly trong container.
03/09/2026
· 8 phút đọc
26
Một từ khóa cắt vòng lặp còn 4 lệnh thay vì 7 — nhưng phá lời hứa thì kết quả ra 15 thay vì 20
restrict hứa hai con trỏ không trùng nhau, cho trình dịch bỏ một lần nạp lại — 4 lệnh thay vì 7. Nhưng đó là lời hứa: truyền con trỏ trùng thì kết quả sai (15 thay vì 20), là hành vi không xác định. Đo bằng assembly trong container.
03/09/2026
· 8 phút đọc
27
Năm trường cộng lại 15 byte, struct chiếm 32 — đổi thứ tự khai, còn một nửa
sizeof một struct không phải tổng kích thước các trường: struct trộn kiểu cộng lại 15 byte mà sizeof=32 vì đệm căn lề. Sắp lại đúng năm trường đó theo lớn->nhỏ còn 16. Đo sizeof và offsetof thật trong container.
03/09/2026
· 8 phút đọc
28
Tám cờ một bit, tôi chắc chiếm 1 byte — đo ra 4, và bố cục bit là cái bẫy còn to hơn
Struct 8 cờ 1-bit tôi tưởng chiếm 1 byte, đo ra 4 — vì kiểu cơ sở là int. Và thứ tự bit trong đơn vị là tùy trình dịch, nên đừng dùng trường bit để khớp bố cục nhị phân ngoài. Đo sizeof, byte thô và assembly trong container.
03/09/2026
· 11 phút đọc
29
Trình biên dịch gộp phép tính lặp từ -O1 — nhưng một phép ghi qua con trỏ đủ để nó bó tay
Trình biên dịch tính (a*b) một lần cho cả hai chỗ dùng, ngay từ -O1. Nhưng một phép ghi qua con trỏ có thể trùng buộc nó phải tính lại — và restrict gỡ đúng chỗ đó. Đo bằng assembly thật, kèm một lần công cụ đếm của tôi nói dối.
03/09/2026
· 9 phút đọc
30
Thêm một cờ, vòng lặp nhanh 2,9 lần — nhưng tôi suýt kể sai lý do
Thêm -flto làm một vòng lặp gọi hàm xuyên file nhanh 2,9 lần. Tôi tưởng nhờ bỏ phí gọi hàm — đọc assembly mới thấy cái lợi thật là vector hóa được mở khóa. Đo thật, đọc objdump trong container.
03/09/2026
· 10 phút đọc
31
PGO nhanh hơn -O2 rõ rệt — cho tới khi tôi thấy hồ sơ chưa từng được nạp
PGO trông nhanh hơn -O2 hẳn 1,3 lần — cho tới khi tôi nhận ra file hồ sơ chưa từng được đọc, và cái nhanh thật ra đến từ mấy cờ mà -fprofile-use tự bật. Đo thật, và một bài học về đo nhầm đại lượng.
03/09/2026
· 9 phút đọc
32
Tôi gắn UNLIKELY để tăng tốc — nó chẳng đổi gì, còn gắn nhầm hướng thì chậm gần gấp đôi
Thêm __builtin_expect(UNLIKELY) vào nhánh hiếm, tôi mong nhanh hơn. Đo ra bằng y hệt bản không hint — vì trình dịch đã đoán đúng sẵn. Cái duy nhất đổi thời gian lại là một hint SAI. Đo thật, đọc assembly trong container.
03/09/2026
· 8 phút đọc
33
Một dòng khai báo biến vòng lặp 371 ms thành 0 ms — nhưng đánh nhầm nó thì kết quả sai âm thầm
Gắn ((const)) cho một hàm bất biến biến vòng lặp 371 ms thành 0 ms vì trình dịch kéo lời gọi ra ngoài. Nhưng đánh nhầm const cho hàm đọc bộ nhớ khiến nó trả về giá trị cũ, không báo lỗi. Đo cả tốc độ lẫn tính đúng trong container.
03/09/2026
· 10 phút đọc
34
Cùng một hàm đệ quy: -O0 tràn ngăn xếp và chết, -O2 chạy gọn 50 triệu tầng
Một hàm đệ quy đuôi tràn ngăn xếp ở -O0 nhưng chạy 50 triệu tầng ở -O2. Và gcc còn khử được cả 'n + f(n-1)' bằng cách tự thêm biến tích lũy — nhiều hơn tôi tưởng. Nhưng TCO là tối ưu, không phải bảo đảm của C. Đo thật trong container.
03/09/2026
· 9 phút đọc
35
Tôi dựng bẫy để bắt fast-math làm sai — hóa ra bản IEEE strict mới sai nặng
Định bắt quả tang -ffast-math làm sai số. Đo ra ngược hẳn: bản IEEE strict kẹt ở 2^24 (16777216) sai nặng, fast-math gần đúng hơn và nhanh 4 lần. Nhưng nó âm thầm xóa lá chắn NaN. Đo thật, đọc assembly trong container.
03/09/2026
· 9 phút đọc
36
Đoạn kiểm tra tràn của tôi báo 'an toàn' đúng lúc số đã âm — và nó chết ngay ở -O0
Kiểm tra tràn signed 'x+100 < x' bị trình dịch xóa thành return 0 — ngay ở -O0, không đợi -O2 như tôi tưởng. Còn unsigned thì quấn vòng đoán được, giữ nguyên phép so sánh. Đọc assembly thật để thấy vì sao bạn không được phát hiện tràn bằng chính phép tràn.
03/09/2026
· 8 phút đọc
37
Tôi viết SIMD bằng tay để thắng trình biên dịch — nó ra bằng y hệt, cùng một lệnh vector
Viết tay SIMD bằng intrinsic để mong thắng trình dịch. Với phép cộng từng phần tử, nó bằng đúng bản để -O3 tự lo — cùng một lệnh fadd. Intrinsic chỉ thắng ở chỗ trình dịch từ chối vector hóa. Đo thật, đọc objdump trong container.
03/09/2026
· 8 phút đọc
38
Bộ đo thời gian viết bằng asm luôn báo 0 — thiếu một từ khóa, cả hàm sụp thành return 0
Một routine đọc bộ đếm chu kỳ hai lần rồi tính hiệu, viết bằng asm không volatile, luôn trả 0 — trình dịch gộp hai lần đọc rồi gấp thành return 0. Với nó, asm non-volatile là hàm thuần. Đọc objdump trong container để thấy tận mắt.
03/09/2026
· 8 phút đọc
39
Đánh dấu hàm ((hot)), đo ra 38,33 so 38,35 ms — tôi suýt kết 'vô dụng', rồi readelf lên tiếng
Gắn ((hot)) rồi đo vòng lặp: 38,33 so 38,35 ms, bằng nhau. Nhưng readelf cho thấy hàm đã nhảy sang section .text.hot đúng như thiết kế. Lợi ích là cache lệnh, chỉ lộ ở quy mô lớn — và bài học về đo đúng đại lượng.
03/09/2026
· 8 phút đọc
40
Tôi chắc switch dày đặc là bảng nhảy O(1) — đọc assembly bốn ví dụ, không cái nào là bảng nhảy
Niềm tin sách giáo khoa: switch dày đặc biên dịch thành jump table. Tôi đo bốn ví dụ dày đặc — gcc gấp thành công thức, thành bảng tra cứu, thành chuỗi so sánh. Bảng nhảy thật chỉ hiện ở 20 case. Đọc objdump trong container.
03/09/2026
· 8 phút đọc
41
Ép nội tuyến một hàm to: mã phình 14 lần, tốc độ tăng đúng 1% — ngưỡng của gcc là bạn, không phải thù
Hàm to gọi từ một chỗ vẫn được nội tuyến bất kể lớn; gọi từ nhiều chỗ thì gcc giữ lời gọi. Tôi ép always_inline vượt ngưỡng — đổi 14 lần kích thước mã lấy vỏn vẹn 1% tốc độ. Đo bằng objdump thật trong container.
03/09/2026
· 8 phút đọc
42
Tôi chắc -Wall sẽ bắt lỗi nguy hiểm nhất — nó im re, và lý do nằm ngay trong tên cảnh báo
-Wall -Wextra bắt gọn bốn lỗi mà mã vẫn dịch trơn, exit 0. Nhưng lỗi dùng biến chưa khởi tạo — thứ nguy hiểm nhất — lọt qua cả -O0 lẫn -O2, kể cả khi bật thẳng -Wmaybe-uninitialized. Đo thật trong container, và bài học về giới hạn của phân tích tĩnh.
03/09/2026
· 8 phút đọc
43
Lỗi ghi lố mảng chạy trơn, exit 0 — rồi ASan hét lên, nhưng suýt nữa cũng im lặng vì một lý do bất ngờ
AddressSanitizer bắt cái lỗi ghi ngoài mảng mà -Wall bỏ lọt và chạy thường không sập. Nhưng tôi tưởng nó bắt ngay — ở -O1 trình dịch gấp lỗi đi, chẳng có truy cập thật để mà bắt. Đo thật trong container.
03/09/2026
· 8 phút đọc
44
Cùng một dòng C, một máy ra 200 còn máy kia ra -56 — biên dịch chéo phơi ra điều tôi tưởng không thể
Tôi tưởng biên dịch chéo chỉ đổi lệnh, không đổi kết quả. Rồi from_char(200) trả 200 trên aarch64 nhưng -56 trên x86-64 — vì dấu của char do kiến trúc quy định, không phải ngôn ngữ. Đo thật trên hai đích, đọc cả hai bản assembly.
03/09/2026
· 8 phút đọc
45
clang thắng ba vòng lặp, tôi định khoe — rồi đọc assembly thấy mình sai cả lý do
Đo sáu vòng lặp bằng gcc và clang: clang nhanh hơn 2-4 lần ở vài vòng, hòa ở số khác. Tôi định kết 'clang nhanh hơn' và giải thích nó thắng popcount nhờ lệnh cnt. Đọc mã máy ra ngược hẳn — gcc mới dùng cnt mà vẫn chậm. Bài cuối sê-ri, và cái bẫy tôi rơi vào cả hai chân.
03/09/2026
· 8 phút đọc