Hiệu năng đường ống Linux bị suy giảm
(qsantos.fr)- Khi ghi dữ liệu vào đường ống Linux bằng
write, tốc độ chậm hơn rất nhiều so với ghi bộ nhớ đơn thuần; trong môi trường thử nghiệm, ghi vào bộ đệm bộ nhớ đạt 167GB/s trong khi ghi vào pipe chỉ ở mức 17GB/s - Nút thắt không thể giải thích chỉ bằng một lần sao chép dữ liệu; chi phí cấp phát trang, khóa và routine sao chép của kernel bên trong
pipe_writecùng cộng dồn lại vmsplicegắn bộ đệm không gian người dùng vào pipe mà không sao chép sang kernel, qua đó tránh các đường đi tốn kém như__alloc_pages,_raw_spin_lock_irq,copy_user_enhanced_fast_string- Trong ví dụ thông lượng Fizz Buzz, cách dùng
vmspliceđạt 60.8GiB/s trên một lõi, 208.3GiB/s trên nhiều lõi, và một ví dụvmspliceở thí nghiệm khác ghi nhận 210GB/s - Do có các đính chính, cách diễn giải về mức phạt do không dùng SIMD không còn đáng tin cậy; giao tiếp liên tiến trình cũng không thể chỉ dừng ở cache L1 nên khó kỳ vọng 167GB/s là thông lượng pipe thực tế
Điểm xuất phát: khoảng cách lớn do vmsplice tạo ra
- Một số chương trình dùng system call
vmspliceđể di chuyển dữ liệu qua pipe nhanh hơn - Trong cuộc thi thông lượng Fizz Buzz trên Code Golf StackExchange, các lời giải chia thành hai nhóm lớn
- Các lời giải không dùng
vmsplicechỉ ở mức vài GiB mỗi giây; lời giải của neil đạt 8.4GiB/s - Các lời giải dùng
vmsplicetăng lên tới 15.5GiB/s của tkluck, 60.8GiB/s của ais523, và 208.3GiB/s đa lõi của david
- Các lời giải không dùng
- Chỉ riêng tác dụng giảm sao chép giữa không gian kernel và không gian người dùng là chưa đủ để giải thích mức chênh gần 7 lần ở bài đo đơn lõi
- Trong thử nghiệm riêng, lời giải của ais523 đạt 96.4GiB/s, còn lời giải của david đạt 277GB/s khi dùng 7 lõi, khoảng 40GB/s mỗi lõi
Mốc chuẩn: ghi bộ nhớ trong không gian người dùng
- Một chương trình Rust lặp lại việc sao chép bộ đệm 32KiB trong không gian người dùng mà không gọi system call đã đạt 167GB/s trong môi trường thử nghiệm
- Mức này được xem là tương ứng với tốc độ ghi vào cache L1 của CPU được dùng
- Hệ thống thử nghiệm dùng Ryzen 9 7950X3D, DDR5 6000T/s, Debian 12, Linux 6.1.0-18-amd64
- Các tùy chọn giảm thiểu CPU bị tắt bằng
mitigations=off
- Trong profiling bằng
ftrace, phần lớn thời gian nằm ở__memset_avx512_unaligned_erms - Tuy vậy, các đính chính đã giới hạn cách diễn giải này
- Lệnh tại điểm ngắt là
rep stos, không phải lệnh AVX-512 - Ngay cả khi giới hạn chỉ dùng AVX2 và SSE2, thông lượng vẫn là 167GB/s vì trong mọi trường hợp đều dùng
rep stos
- Lệnh tại điểm ngắt là
Chi phí thực tế khi ghi vào pipe bằng write
- Khi ghi bộ đệm cùng kích thước vào pipe bằng
stdout.write()và tiêu thụ bằngpv >/dev/null, thông lượng giảm xuống còn 17GB/s - Kết quả profiling cho thấy phần lớn thời gian bị tiêu tốn trong system call
write, đặc biệt 95% nằm trongpipe_write - Bên trong
pipe_write, việc chuẩn bị trang, khóa và sao chép cùng tạo ra chi phí__alloc_pages: 36% tổng thời gian, dùng để chuẩn bị các trang bộ nhớ mới cho pipe__mutex_lock.constprop.0: 25% tổng thời gian, là chi phí khóa cho thao tác ghi pipe_raw_spin_lock_irq: 5% tổng thời gian, cũng là chi phí khóa liên quan đến ghi pipecopy_user_enhanced_fast_string: khoảng 20% tổng thời gian, sao chép dữ liệu từ không gian người dùng sang phía kernel
- Vì
pvtiêu thụ dữ liệu bằng cách dùngspliceđể chuyển các trang sang/dev/null, nên khó tái sử dụng lặp đi lặp lại cùng vài trang trong vòng lặp
Routine sao chép của kernel và cách diễn giải đã được đính chính
- Khi dịch ngược
copy_user_enhanced_fast_string, việc sao chép bộ đệm lớn được thực hiện bằng lệnhREP MOV - Hàm này không được viết bằng C mà là mã assembly của Linux kernel, nên đây là đường đi có chủ đích chứ không phải do thiếu tối ưu hóa từ compiler
- Trong thí nghiệm ban đầu, việc gọi trực tiếp
rep movsbở không gian người dùng đạt 80GB/s, từ đó dẫn đến cách diễn giải rằng routine sao chép của kernel chậm hơn khoảng 2 lần - Sau đó, các đính chính cho thấy điều kiện thí nghiệm đã khác
- Hai bộ đệm 32KiB làm bão hòa cache dữ liệu L1
- Khi dùng bộ đệm 16KiB, hiệu năng tăng lên 153GB/s
- Vì vậy, cách diễn giải ban đầu rằng việc không dùng lệnh vector trong sao chép của kernel gây mức phạt lớn là không còn đáng tin cậy
- Dù vậy, overhead quản lý bộ nhớ trong lúc ghi pipe vẫn là một yếu tố lớn
Các đường đi kernel mà vmsplice tránh được
vmsplicechuyển toàn bộ bộ đệm ở không gian người dùng vào pipe mà không sao chép sang kernel, nhờ đó giảm chi phí dùng pipe- Ví dụ
./writetrongpipes-speed-testcủa Francesco được dùng như một ví dụ tối giản liên tục ghi'X' - Ví dụ này ghi nhận 210GB/s, nhưng vì nó lặp lại việc đưa cùng một bộ đệm vào
vmsplice, nên khác với công việc tạo dữ liệu thông thường- Nếu không phải là một luồng byte hằng, cần phải nạp dữ liệu mới vào bộ đệm
- Khi đó, giới hạn ghi bộ nhớ đơn thuần 167GB/s lại trở nên liên quan
- Ngay cả trong đường đi
vmsplice,__mutex_lock.constprop.0vẫn chiếm 37% thời gian - Tuy nhiên, các thành phần
__alloc_pages,_raw_spin_lock_irq,copy_user_enhanced_fast_stringxuất hiện ở đườngwritethì không còn thấy nữa - Thay vào đó, các đường đi chính là
add_to_pipe,import_iovec,iov_iter_get_pages2, cho thấyvmsplicetránh được những phần tốn kém củawrite
Kết luận còn lại và những điểm cần lưu ý
- Theo thí nghiệm, đường ghi vào pipe Linux bằng
writechậm hơn khoảng 10 lần so với ghi bộ nhớ đơn thuần - Kết luận ban đầu cho rằng chi phí khóa và chi phí lưu/khôi phục ngữ cảnh SIMD trong lúc ghi pipe là lớn, và
splicecùngvmspliceđã tránh được điều đó - Sau đính chính, kết luận cần được nhìn nhận dè dặt hơn
- Overhead quản lý bộ nhớ của kernel vẫn là yếu tố quan trọng làm giảm hiệu năng pipe
- Cách diễn giải rằng việc không dùng lệnh vector gây mức phạt lớn như dự đoán là không chính xác
- Giao tiếp liên tiến trình không thể chỉ diễn ra trong cache L1, nên 167GB/s khó đạt được như thông lượng thực tế khi tính cả thao tác đọc pipe
- Một số sai sót quan trọng đã được đính chính và độ tin cậy của kết quả có thể bị giới hạn, vì vậy nên hiểu các con số này theo hướng chỉ báo xu hướng
- Muốn tăng thông lượng pipe, không chỉ cần giảm số lượng system call mà còn phải xem cả các đường đi nội bộ trong kernel mà
writeđi qua và cách xử lý bộ đệm
1 bình luận
Các ý kiến trên Hacker News
Có một dự án phụ đang cố xử lý vấn đề này: https://lwn.net/Articles/976836/
Ý tưởng là tạo một system call để lấy ring buffer cho mọi file descriptor được hỗ trợ. Pipe cũng được bao gồm; nếu cả hai đầu đều hỗ trợ dùng ring buffer, chúng có thể ánh xạ cùng một ring buffer để thực hiện I/O không sao chép, và trong một số trường hợp thậm chí không cần gọi kernel lần nào. Họ đang tìm người cùng tham gia.
Khi ring buffer đầy hoặc rỗng thì có thể sleep/poll, còn ngoài các trường hợp đó thì hoạt động không khóa và không có overhead system call.
ringbuffer_wait()cũng có thể báo hiệu bằng cách làm chopoll()thấy nó ở trạng thái có thể đọc.Chỉ cần một hiccup tức thời, nếu không có đủ buffering thì pipe có thể rối tung hoàn toàn, và kích thước buffer cần thiết lại khác nhau tùy hệ thống.
Lý do
JMPkhông đơn giản làRETlà vì tùy chọn CONFIG_RETHUNK. Những gì thấy trong phần disassembly củaobjdumplà kết quả của việcRETbị thay bằngJMP __x86_return_thunk.https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/lib/ret...
Các lệnh NOP ở đầu và cuối hàm không phải dành cho ftrace, mà đến từ các macro ASM_CLAC/ASM_STAC. Các macro này chừa chỗ để lúc runtime điền lệnh CLAC/STAC nếu phát hiện X86_FEATURE_SMAP. Cả hai lệnh đều dài 3 byte, nên số NOP cũng tương ứng.
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/kernel/...
Gọi pipe của Linux là “chậm” cũng giống như gọi Toyota Corolla là “chậm”. Nếu không phải use case cực đoan thì nó đủ nhanh.
Bạn đang đua xe à? Và còn là thể loại mà tốc độ quan trọng hơn kỹ thuật? Vậy thì mua xe nhanh hơn. Còn không thì cứ tiếp tục đi Corolla.
Vì vậy, chỉ một cải thiện hiệu suất rất nhỏ cũng đáng bỏ nhiều thời gian tối ưu xét về mặt kinh tế.
Tôi đã dùng pipe cho nhiều mục đích hơn 10 năm nhưng chưa từng bị giới hạn bởi tốc độ pipe; thường thì bottleneck có lẽ là các công cụ như tar, gzip, find, grep, nc. Tất nhiên, so với việc chúng làm thì các công cụ này cũng khá nhanh.
FFMpeg không thể đưa SDK Non-Free vào source, còn lưu RGBA thuần ra file thì phi thực tế đến mức vô lý. Vì vậy pipe là cách duy nhất, và đây là lý do chính đáng để cần pipe thông lượng cao.
https://www.toyota.com/grcorolla/
Những chiếc xe này có kỹ thuật và hiệu năng rất ấn tượng, và cũng giống như một cách hack để lách các quy định khiến GR Yaris — mẫu xe vốn được nhắm tới — khó được đưa vào thị trường Mỹ. Tôi nghĩ nó có đủ bối cảnh về kỹ thuật/hiệu năng/hacking/thị trường để người HN đón nhận nhẹ nhàng. Hơn nữa, chủ tịch công ty vẫn còn tự lái và trực tiếp điều khiển nó.
Dù không liên quan trực tiếp đến ý chính của bài, trên các CPU hiện đại,
rep movsbnhanh ngang với phiên bản vector hóa nhanh nhất. Lý do là ta biết CPU có tăng tốc cho lệnh nàyTên hàm kernel
copy_user_enhanced_fast_stringcũng ngụ ý điều đó. Các tính năng CPU liên quan là ERMS (Enhanced Repeat Move String, làmrep movsbnhanh hơn khi độ dài vượt một mức nhất định) và FSRM (Fast Short Repeat Move String, làm các phép sao chép ngắn cũng nhanh hơn)rep movsbnhanh đến một ngưỡng nào đó, nhưng sau ngưỡng ấy thì store thông thường hoặc non-temporal store nhanh hơnTất cả các ngưỡng đều được mô tả tại https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch...
Và các giá trị này cũng không cố định; Noah Goldstein vẫn cập nhật chúng hằng năm
memcpyngắn, tôi vẫn đang chờ đến lúcrep movsbvàrep stosbđủ nhanh để có thể xóa luôn phiên bản vòng lặp C đơn giảnmemcpy()độ dài biến đổi, giống như cách chúng inlinememcpyđộ dài cố địnhCó một điểm về AVX512 mà tôi không thấy trong bài: ngoài overhead của
xsave/xrstor, AVX512 tiêu thụ nhiều điện năng và gây ra CPU frequency scaling. Xem [1], [2] để biết chi tiết và thấy việc này có thể tinh vi đến mức nào[1] https://www.intel.com/content/dam/www/central-libraries/us/e...
[2] https://www.intel.com/content/www/us/en/developer/articles/t...
Gần như mọi hình thức giao tiếp liên tiến trình đều “chậm”. Đó là lựa chọn trả chi phí hiệu năng để đổi lấy an toàn
Đặc biệt, không có nhiều lý do để giao tiếp liên tiến trình nhanh nhất lại chậm hơn một lời gọi hàm dài
Lại đang hứng cú ôm nghẹt thở của Hacker News. Nhờ cache trang của WordPress nên đỡ hơn lần trước, nhưng trang vẫn có thể mất vài giây để tải, mong mọi người thông cảm
Tôi vốn không hiểu rõ vì sao
splicephải chậm đến vậy. Bài có nêu nguyên nhân nó chậm hơnvmsplicelà do cấp phát buffer và dùng lệnh scalar, nhưng tôi không hiểu vì sao những thứ đó lại cần thiếtTại sao không thể triển khai lại
spliceđơn giản giống nhưvmsplice? Chắc hẳn có lý do hay nào đó mà tôi đã bỏ lỡvmsplicekhông hoạt động với mọi loại file descriptorSẽ rất thú vị nếu xem cả phiên bản dùng io_uring. Có thể chia sẻ trước buffer với kernel để tránh một phần việc sao chép, và cũng có thể tránh overhead của system call. Tuy nhiên trong trường hợp này, phần sau có vẻ gần như không đáng kể
Một tuyên bố khá táo bạo đối với một blog mất khoảng 20 giây để tải
Bản thân bài viết có vẻ rất hay, và có nhiều điều để học về những gì diễn ra bên trong