Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 36.228 phút đọc

Mức tối ưu và LTO

Sau bài này bạn sẽ làm được

  • Phân biệt sáu mức tối ưu và khi nào dùng cái nào
  • Đo được rằng -O2 nhanh hơn -O0 trên mã thật
  • Giải thích vì sao -O3 không phải luôn nhanh hơn -O2
  • Biết -march=native đánh đổi gì

Tối ưu là chỗ dễ mê tín nhất trong C. Bài này thay việc đoán bằng việc đo: một vòng lặp thật, năm mức tối ưu, và những con số cho thấy khi nào mỗi mức đáng dùng.

#Sáu mức

MứcÝ nghĩaDùng khi
-O0Không tối ưu, mặc địnhGỡ lỗi: mã khớp nguồn từng dòng
-O1Tối ưu cơ bản, dịch nhanhMuốn nhanh hơn O0 mà vẫn dịch nhanh
-O2Tối ưu đầy đủ, mặc định phát hànhBản phát hành, gần như luôn
-O3Thêm vector hoá và nội tuyến mạnhChỉ khi đo được là nhanh hơn O2
-OsTối ưu kích thướcHệ nhúng, bộ nhớ hạn chế
-OgTối ưu nhưng vẫn gỡ lỗi đượcGỡ lỗi mà O0 quá chậm

#Đo trên mã thật

cham.c
#include <stdio.h>
#include <stdlib.h>

static long tinh(long n) {
    long s = 0;
    for (long i = 0; i < n; ++i)
        for (long j = 0; j < 1000; ++j)
            s += (i * j) % 7;
    return s;
}

int main(int argc, char **argv) {
    long n = (argc > 1) ? atol(argv[1]) : 50000;
    printf("%ld\n", tinh(n));
    return 0;
}
terminal
# Dich moi muc, chay 3 lan, lay lan nhanh nhat, n = 100000
for o in O0 O1 O2 O3 Os; do gcc -std=c11 -$o cham.c -o cham_$o.exe; done
# do bang thoi gian tuong (best of 3)
-O0: 300 ms
-O1: 203 ms
-O2: 181 ms
-O3: 162 ms
-Os: 295 ms
terminal
# Vi sao O2 nhanh: xem tinh() con bao nhieu lenh
gcc -std=c11 -O0 -S cham.c -o - | sed -n '/^tinh:/,/ret/p' | grep -c '^\s\+[a-z]'
39
gcc -std=c11 -O2 -S cham.c -o - | sed -n '/^tinh:/,/ret/p' | grep -c '^\s\+[a-z]'
0

#O3 không luôn nhanh hơn

Đoán
# "O3 la cao nhat nen chac nhanh nhat" gcc -O3 -march=native -funroll-all-loops -ffast-math app.c -o app # Chong het co le vao, khong do, va co the: # - cham hon O2 # - file to gap doi # - -ffast-math lam ket qua so thuc SAI # - -march=native lam binary khong chay may khac
Đo
# Do tren dung tai cong viec that for o in O2 O3; do gcc -std=c17 -$o app.c -o app_$o echo "== -$o ==" for r in 1 2 3; do time ./app_$o du_lieu_that.dat > /dev/null done done # Roi giu cai nhanh hon TREN MAY DICH VU THAT, khong phai may ban.

#LTO

Tối ưu lúc liên kết
Link Time Optimization. Bình thường mỗi tệp .c được tối ưu riêng, nên trình biên dịch không nội tuyến được một hàm nằm ở tệp khác. LTO hoãn phần tối ưu tới bước liên kết, khi đã thấy toàn bộ chương trình, nên nó tối ưu xuyên tệp.
# Bat LTO o CA buoc dich LAN buoc lien ket
gcc -O2 -flto -c a.c -o a.o
gcc -O2 -flto -c b.c -o b.o
gcc -O2 -flto a.o b.o -o app

# Hoac mot lenh:
gcc -O2 -flto a.c b.c -o app

/* Loi ich: mot ham nho trong a.c goi tu b.c gio noi tuyen duoc,
   nhu the ca chuong trinh nam trong mot file.

   Chi phi:
     - lien ket cham hon nhieu, doi khi rat nhieu
     - thong bao loi kho doc hon
     - can CUNG co -O va -flto o ca hai buoc

   Dung LTO cho ban PHAT HANH, khong cho ban phat trien hang ngay.
   Do truoc: voi nhieu du an LTO tiet kiem 5-15%, dang gia;
   voi mot so du an gan nhu khong doi. */

#march=native

gcc -O2 -march=native app.c -o app

/* -march=native cho phep trinh bien dich dung MOI lenh CPU cua
   MAY DANG DICH: AVX, AVX2, FMA, va cac phan mo rong khac.

   Loi: nhanh hon voi ma so hoc day, vi vector hoa dung lenh rong.

   Nguy: binary CHI CHAY tren CPU co cung tap lenh. Dem sang
   may cu hon, no bao "Illegal instruction" va sap.

   Ba lua chon: */

-march=native      /* nhanh nhat, chi chay may nay */
-march=x86-64-v2   /* mot muc co so hop ly, chay hau het may tu 2010 */
-march=x86-64      /* an toan nhat, chay moi may 64 bit */
(khong dat)        /* = -march tuong ung -mtune generic, an toan */

/* Quy tac:
   - Binary phat hanh cho nguoi khac: DUNG -march=native.
     Dung muc co so nhu x86-64-v2, hoac khong dat gi.
   - Binary chi chay tren may dich vu ban kiem soat: -march=native
     hoac -march ung voi dung dong CPU do.
   - Do luong: -march=native khong phai luc nao cung nhanh hon
     dang ke. Do truoc khi danh doi tinh di dong. */

Tự làm thử

  1. Đo cham.c ở năm mức trên máy bạn và ghi lại các con số.
  2. So assembly của tinh() ở -O0 và -O2, đếm số lệnh.
  3. Tìm một chương trình mà -O3 chậm hơn hoặc bằng -O2.
  4. Dựng một dự án hai tệp có và không có -flto, đo chênh lệch.
  5. Dịch với -Ofast và tìm một phép tính số thực cho kết quả khác -O2.
  6. So kích thước binary giữa -O2, -O3, và -Os.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Gỡ lỗi dùng -O0 hoặc -Og; phát hành dùng -O2 và đo trước khi lên -O3.
  • Bước nhảy lớn nhất là O0 tới O2; -O2 nội tuyến hàm nên vòng lặp có thể biến mất khỏi hàm gốc.
  • -O3 có thể chậm hơn do phình mã, vector hoá sai chỗ, hoặc áp lực thanh ghi; luôn đo.
  • LTO tối ưu xuyên tệp, dùng cho bản phát hành, cần cùng phiên bản công cụ ở cả hai bước.
  • -march=native nhanh hơn nhưng khoá binary vào một dòng CPU; đừng dùng cho binary phát hành hay trong CI.