Phần 1 đo được cs gần một triệu mỗi giây làm CPU bận 44% mà không làm gì hữu ích. Bài này đo giá của một lần chuyển.
Phép thử
Hai tiến trình ném một byte qua lại bằng hai ống. Mỗi vòng là hai lần chuyển ngữ cảnh. Đo 150.000 vòng.
| Vị trí | µs mỗi lần chuyển (3 lần đo) |
|---|---|
| Cùng một nhân (CPU0 ↔ CPU0) | 0,721 / 0,647 / 0,628 |
| Hai nhân khác (CPU0 ↔ CPU1) | 12,442 / 13,214 / 12,128 |
| Hai nhân xa (CPU0 ↔ CPU8) | 12,312 / 12,180 / 12,026 |
| Nhân tự chọn (không ghim) | 12,445 / 12,162 / 12,250 |
Chênh 19 lần giữa cùng nhân và khác nhân.
Và dòng cuối là điều đáng chú ý: bộ lập lịch mặc định trải hai tiến trình ra hai nhân khác nhau, tức là chọn đúng trường hợp đắt.
Vì sao khác biệt lớn đến vậy
Chuyển trên cùng một nhân chỉ là: lưu thanh ghi, đổi con trỏ ngăn xếp, nạp thanh ghi của tiến trình kia. Bộ nhớ đệm CPU vẫn còn nóng vì cả hai vừa chạy ở đó.
Chuyển qua nhân khác phải thêm hai việc đắt:
- Gửi một ngắt liên nhân để đánh thức nhân kia. Đây là một tín hiệu phần cứng, không phải lời gọi hàm.
- Kéo lại các dòng bộ nhớ đệm. Dữ liệu trong ống nằm ở bộ đệm của nhân này; nhân kia phải lấy qua hệ thống nhất quán bộ đệm.
Gần hết 12 µs nằm ở hai việc đó.
Luồng không rẻ hơn tiến trình
hai TIẾN TRÌNH, không ghim 12,442 µs
hai LUỒNG cùng tiến trình 12,586 / 12,345 / 11,803 µs
Bằng nhau.
Điều này đi ngược lời khuyên quen thuộc "dùng luồng thay tiến trình vì chuyển ngữ cảnh rẻ hơn". Việc đổi không gian địa chỉ — nạp lại bảng trang, xả TLB — là có thật, nhưng nó nhỏ so với chi phí đánh thức qua nhân đang chi phối.
"Luồng rẻ hơn tiến trình" chỉ đúng khi cả hai chạy trên cùng một nhân. Ở đó việc đổi bảng trang mới trở thành phần đáng kể.
Một lưu ý về môi trường đo
Số liệu đo trong máy ảo Docker trên macOS. Chi phí ngắt liên nhân ở đó cao hơn máy vật lý, nên con số 12 µs lớn hơn điển hình.
Cái mang đi được là tỉ lệ và thứ tự: cùng nhân rẻ hơn khác nhân một bậc, và bộ lập lịch mặc định không chọn giúp bạn. Hãy đo lại trên chính máy chủ của bạn — chương trình đo chỉ vài chục dòng và trong phần cuối bài.
Điều này nghĩa là gì cho mã của bạn
Mọi lần chờ và đánh thức đều tốn. Khoá, hàng đợi, biến điều kiện, epoll — mỗi lần một luồng ngủ rồi được đánh thức là một lần chuyển ngữ cảnh. Với 100.000 lần mỗi giây và 12 µs mỗi lần, đó là 1,2 giây CPU mỗi giây — tức hơn một nhân bị đốt hoàn toàn.
Đây chính là điều đo được ở phần 1: cs = 964.760 làm sy = 44%.
Nhiều luồng hơn không có nghĩa nhanh hơn. Nếu các luồng tranh nhau một khoá, thêm luồng chỉ thêm số lần chuyển. Bộ đếm cs là cách nhanh nhất phát hiện: nó tăng mà thông lượng không tăng nghĩa là bạn đang trả tiền cho việc chuyển qua chuyển lại.
Xử lý theo lô cắt được chi phí này. Đọc 100 mục trong một lần thức thay vì thức 100 lần. Đây là cùng một nguyên tắc với đường ống trong sê-ri Redis, chỉ ở tầng thấp hơn.
Ghim CPU có thể giúp — hoặc hại. Hai tiến trình trao đổi dữ liệu dày đặc chạy cùng nhân sẽ nhanh hơn nhiều. Nhưng chúng chia nhau một nhân, nên nếu cả hai cần CPU thật thì tổng thông lượng giảm. Chỉ đáng làm khi phần trao đổi chi phối, và luôn phải đo.
Xem hệ thống đang chuyển bao nhiêu
grep ctxt /proc/stat
Số tích luỹ từ lúc khởi động. Đọc hai lần cách nhau một giây rồi lấy hiệu:
A=$(awk '/^ctxt/{print $2}' /proc/stat); sleep 1
B=$(awk '/^ctxt/{print $2}' /proc/stat); echo $((B-A))
Và cho từng tiến trình:
grep -E 'voluntary_ctxt_switches|nonvoluntary_ctxt_switches' /proc/<pid>/status
Phân biệt này quan trọng:
voluntary— tiến trình tự nhường CPU vì phải chờ: đọc đĩa, chờ mạng, chờ khoá. Cao nghĩa là chương trình dành nhiều thời gian chờ.nonvoluntary— bộ lập lịch cưỡng chế lấy CPU vì hết lượt. Cao nghĩa là có tranh chấp CPU: quá nhiều tiến trình sẵn sàng chạy so với số nhân.
Hai con số này chỉ đúng hướng khắc phục khác hẳn nhau, và chúng miễn phí để đọc.
Thử ba mươi giây
Đo chính máy của bạn bằng chương trình trong bài:
cat > /tmp/ctx.c <<'EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sched.h>
#include <sys/wait.h>
static double now(){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return t.tv_sec+t.tv_nsec/1e9;}
static void pin(int c){cpu_set_t s;CPU_ZERO(&s);CPU_SET(c,&s);sched_setaffinity(0,sizeof(s),&s);}
int main(int argc,char**argv){
int N=atoi(argv[1]), cp=atoi(argv[2]), cc=atoi(argv[3]);
int a[2],b[2]; if(pipe(a)||pipe(b))return 1;
pid_t p=fork(); char c=1;
if(p==0){ if(cc>=0)pin(cc);
for(int i=0;i<N;i++){ if(read(a[0],&c,1)!=1)_exit(1); if(write(b[1],&c,1)!=1)_exit(1);} _exit(0);}
if(cp>=0)pin(cp);
double t0=now();
for(int i=0;i<N;i++){ write(a[1],&c,1); read(b[0],&c,1);}
double d=now()-t0; waitpid(p,NULL,0);
printf("cha=%d con=%d : %.3f us moi lan chuyen\n", cp, cc, d*1e6/(2.0*N));
return 0;
}
EOF
gcc -O2 -o /tmp/ctx /tmp/ctx.c
/tmp/ctx 150000 0 0 # cung nhan
/tmp/ctx 150000 0 1 # khac nhan
/tmp/ctx 150000 -1 -1 # nhan tu chon
Tỉ lệ giữa dòng một và dòng hai là con số đáng nhớ về máy chủ của bạn. Và dòng ba cho biết bộ lập lịch mặc định đang chọn phía nào.
Phần sau: bộ lập lịch CFS — nó chia CPU giữa các tiến trình theo quy tắc nào.