Tôi đang tự hỏi bao nhiêu về Ulrich Drepper's Điều mà mọi lập trình viên nên biết về bộ nhớ từ năm 2007 vẫn còn hiệu lực. Ngoài ra, tôi không thể tìm thấy phiên bản mới hơn 1.0 hoặc errata.
Tôi đang tự hỏi bao nhiêu về Ulrich Drepper's Điều mà mọi lập trình viên nên biết về bộ nhớ từ năm 2007 vẫn còn hiệu lực. Ngoài ra, tôi không thể tìm thấy phiên bản mới hơn 1.0 hoặc errata.
Câu trả lời:
Theo như tôi nhớ nội dung của Drepper mô tả các khái niệm cơ bản về bộ nhớ: cách bộ đệm CPU hoạt động, bộ nhớ vật lý và ảo là gì và cách nhân Linux xử lý sở thú đó. Có thể có các tham chiếu API lỗi thời trong một số ví dụ, nhưng nó không thành vấn đề; điều đó sẽ không ảnh hưởng đến sự liên quan của các khái niệm cơ bản.
Vì vậy, bất kỳ cuốn sách hoặc bài viết mô tả một cái gì đó cơ bản không thể được gọi là lỗi thời. "Điều mà mọi lập trình viên nên biết về bộ nhớ" chắc chắn đáng để đọc, nhưng, tôi không nghĩ nó dành cho "mọi lập trình viên". Nó phù hợp hơn cho các hệ thống / nhúng / kernel.
Hướng dẫn ở dạng PDF có tại https://www.akkadia.org/drepper/cpumemory.pdf .
Nó vẫn còn tuyệt vời và rất được khuyến khích (bởi tôi, và tôi nghĩ bởi các chuyên gia điều chỉnh hiệu suất khác). Sẽ thật tuyệt nếu Ulrich (hoặc bất kỳ ai khác) đã viết một bản cập nhật năm 2017, nhưng đó sẽ là rất nhiều công việc (ví dụ: chạy lại các điểm chuẩn). Xem thêm các liên kết tối ưu hóa hiệu suất x86 và SSE / asm (và C / C ++) khác trongx86 thẻ wiki . (Bài viết của Ulrich không phải là x86 cụ thể, nhưng hầu hết (tất cả) điểm chuẩn của anh ấy là trên phần cứng x86.)
Các chi tiết phần cứng cấp thấp về cách thức hoạt động của DRAM và cache vẫn được áp dụng . DDR4 sử dụng các lệnh tương tự như được mô tả cho DDR1 / DDR2 (cụm đọc / ghi). Các cải tiến DDR3 / 4 không phải là những thay đổi cơ bản. AFAIK, tất cả các công cụ độc lập với vòm vẫn được áp dụng chung, ví dụ như đối với AArch64 / ARM32.
Xem thêm các Latency Platforms ràng buộc phần của câu trả lời này để biết chi tiết quan trọng về tác động của bộ nhớ / L3 độ trễ về single-ren băng thông: bandwidth <= max_concurrency / latency
, và điều này thực sự là nút cổ chai chính cho băng thông đơn ren trên một CPU nhiều lõi hiện đại như một Xeon . Nhưng một máy tính để bàn Skylake lõi tứ có thể tiến gần đến mức tối đa băng thông DRAM chỉ với một luồng. Liên kết đó có một số thông tin rất tốt về các cửa hàng NT so với các cửa hàng bình thường trên x86. Tại sao Skylake lại tốt hơn Broadwell-E cho thông lượng bộ nhớ đơn luồng? là một bản tóm tắt.
Do đó, đề xuất của Ulrich trong 6.5.8 Sử dụng tất cả Băng thông về việc sử dụng bộ nhớ từ xa trên các nút NUMA khác cũng như của riêng bạn, là phản tác dụng trên phần cứng hiện đại, nơi bộ điều khiển bộ nhớ có nhiều băng thông hơn một lõi có thể sử dụng. Cũng có thể bạn có thể tưởng tượng một tình huống có lợi ích ròng khi chạy nhiều luồng đói bộ nhớ trên cùng một nút NUMA để liên lạc giữa các luồng có độ trễ thấp, nhưng chúng sử dụng bộ nhớ từ xa cho các công cụ không nhạy cảm băng thông cao. Nhưng điều này khá tối nghĩa, thông thường chỉ cần phân chia các luồng giữa các nút NUMA và để chúng sử dụng bộ nhớ cục bộ. Băng thông trên lõi rất nhạy cảm với độ trễ do giới hạn đồng thời tối đa (xem bên dưới), nhưng tất cả các lõi trong một ổ cắm thường có thể nhiều hơn bão hòa bộ điều khiển bộ nhớ trong ổ cắm đó.
Một điều quan trọng đó là thay đổi là prefetch phần cứng là nhiều hơn trên Pentium 4 và có thể nhận ra mô hình truy cập strided lên đến một sải chân khá lớn, và nhiều dòng cùng một lúc (ví dụ như một forward / backward mỗi 4k trang). Hướng dẫn tối ưu hóa của Intel mô tả một số chi tiết về trình tải trước CTNH ở các cấp độ bộ đệm khác nhau cho kiến trúc vi mô gia đình Sandybridge của họ. Ivybridge và sau đó có tìm nạp trước phần cứng trang tiếp theo, thay vì chờ lỗi bộ nhớ cache trong trang mới để kích hoạt khởi động nhanh. Tôi giả sử AMD có một số thứ tương tự trong hướng dẫn tối ưu hóa của họ. Xin lưu ý rằng hướng dẫn sử dụng của Intel cũng chứa đầy những lời khuyên cũ, một số trong đó chỉ tốt cho P4. Các phần dành riêng cho Sandybridge tất nhiên là chính xác cho SnB, nhưng vdkhông ghép các uops hợp nhất đã thay đổi trong HSW và hướng dẫn không đề cập đến nó .
Lời khuyên thông thường hiện nay là xóa tất cả tìm nạp trước SW khỏi mã cũ và chỉ xem xét đưa nó trở lại nếu cấu hình hiển thị lỗi bộ nhớ cache (và bạn không bão hòa băng thông bộ nhớ). Tìm nạp trước cả hai mặt của bước tiếp theo của tìm kiếm nhị phân vẫn có thể giúp ích. ví dụ: một khi bạn quyết định xem phần tử nào tiếp theo, hãy tìm nạp trước các phần tử 1/4 và 3/4 để chúng có thể tải song song với tải / kiểm tra giữa.
Theo tôi, đề xuất sử dụng một luồng tìm nạp riêng (6.3.4) là hoàn toàn lỗi thời và chỉ tốt cho Pentium 4. P4 có siêu phân luồng (2 lõi logic chia sẻ một lõi vật lý), nhưng không đủ bộ đệm theo dõi (và / hoặc tài nguyên thực thi ngoài đơn hàng) để đạt được thông lượng chạy hai luồng tính toán đầy đủ trên cùng một lõi. Nhưng các CPU hiện đại (Sandybridge-gia đình và Ryzen) mạnh hơn nhiều và nên chạy một luồng thực sự hoặc không sử dụng siêu phân luồng (để nguyên lõi logic khác để luồng xử lý độc lập có toàn bộ tài nguyên thay vì phân vùng ROB).
Phần mềm tìm nạp trước luôn luôn "dễ vỡ" : số điều chỉnh ma thuật phù hợp để tăng tốc phụ thuộc vào chi tiết của phần cứng và có thể tải hệ thống. Quá sớm và nó đã bị trục xuất trước khi tải nhu cầu. Quá muộn và không có ích. Bài viết trên blog này hiển thị mã + biểu đồ cho một thử nghiệm thú vị trong việc sử dụng tính năng tìm nạp trước SW trên Haswell để tìm nạp trước phần không tuần tự của một vấn đề. Xem thêm Làm thế nào để sử dụng đúng hướng dẫn tìm nạp? . NT prefetch rất thú vị, nhưng thậm chí còn dễ vỡ hơn bởi vì việc trục xuất sớm từ L1 có nghĩa là bạn phải tìm mọi cách để đến L3 hoặc DRAM, không chỉ L2. Nếu bạn cần mỗi lần giảm hiệu suất cuối cùng và bạn có thể điều chỉnh cho một máy cụ thể, thì tìm nạp trước SW đáng để xem xét để truy cập tuần tự, nhưng nóvẫn có thể bị chậm lại nếu bạn có đủ công việc ALU để làm trong khi sắp bị tắc nghẽn bộ nhớ.
Kích thước dòng bộ đệm vẫn là 64 byte. (L1D đọc / ghi băng thông là rất cao, và CPU hiện đại có thể làm 2 tải vector mỗi đồng hồ + 1 cửa hàng vector nếu nó tất cả lượt truy cập trong L1D. Xem Làm thế nào có thể bộ nhớ cache được nhanh vậy? .) Với AVX512, kích thước dòng = chiều rộng vector, để bạn có thể tải / lưu trữ toàn bộ dòng bộ đệm trong một hướng dẫn. Do đó, mọi tải / lưu trữ bị sai lệch đều vượt qua ranh giới dòng bộ đệm, thay vì mọi thứ khác cho 256b AVX1 / AVX2, thường không làm chậm việc lặp lại trên một mảng không có trong L1D.
Các hướng dẫn tải không được chỉ định có hình phạt bằng 0 nếu địa chỉ được căn chỉnh trong thời gian chạy, nhưng trình biên dịch (đặc biệt là gcc) tạo mã tốt hơn khi tự động kiểm tra nếu chúng biết về bất kỳ đảm bảo căn chỉnh nào. Trên thực tế, các ops không được phân bổ thường nhanh, nhưng việc chia trang vẫn bị tổn thương (tuy nhiên ít hơn nhiều trên Skylake; chỉ có ~ 11 chu kỳ thêm độ trễ so với 100, nhưng vẫn bị phạt thông lượng).
Như Ulrich dự đoán, mọi hệ thống đa ổ cắm là NUMA ngày nay: bộ điều khiển bộ nhớ tích hợp là tiêu chuẩn, tức là không có Northbridge bên ngoài. Nhưng SMP không còn có nghĩa là đa ổ cắm, vì CPU đa lõi đang lan rộng. Các CPU Intel từ Nehalem đến Skylake đã sử dụng bộ đệm L3 lớn bao gồm như một điểm dừng cho sự gắn kết giữa các lõi. CPU AMD thì khác, nhưng tôi không rõ về chi tiết.
Skylake-X (AVX512) không còn có L3 bao gồm, nhưng tôi nghĩ vẫn còn một thư mục thẻ cho phép nó kiểm tra những gì được lưu trong bộ nhớ cache ở bất cứ đâu trên chip (và nếu có) mà không thực sự phát sóng đến tất cả các lõi. Thật không may, SKX sử dụng lưới thay vì bus vòng , thường có độ trễ thậm chí còn tệ hơn cả Xeons nhiều lõi trước đây.
Về cơ bản, tất cả các lời khuyên về tối ưu hóa vị trí bộ nhớ vẫn được áp dụng, chỉ là chi tiết về chính xác những gì xảy ra khi bạn không thể tránh bỏ lỡ bộ nhớ cache hoặc tranh chấp khác nhau.
6.4.2 Nguyên tử ops : điểm chuẩn hiển thị vòng lặp CAS-retry tệ hơn gấp 4 lần so với phân xử bằng phần cứng lock add
có thể vẫn phản ánh trường hợp tranh chấp tối đa . Nhưng trong các chương trình đa luồng thực sự, đồng bộ hóa được giữ ở mức tối thiểu (vì nó đắt tiền), do đó sự tranh chấp thấp và vòng lặp CAS-retry thường thành công mà không phải thử lại.
C ++ 11 std::atomic
fetch_add
sẽ biên dịch thành lock add
(hoặc lock xadd
nếu giá trị trả về được sử dụng), nhưng thuật toán sử dụng CAS để thực hiện điều gì đó không thể thực hiện được bằng lệnh lock
ed thường không phải là thảm họa. Sử dụng C ++ 11std::atomic
hoặc C11 stdatomic
thay vì gcc di sản __sync
built-in hoặc các phiên bản mới hơn __atomic
được xây dựng-in , trừ khi bạn muốn kết hợp truy cập nguyên tử và phi nguyên tử để cùng một vị trí ...
8.1 DWCAS ( cmpxchg16b
) : Bạn có thể dỗ gcc phát ra nó, nhưng nếu bạn muốn tải hiệu quả chỉ bằng một nửa đối tượng, bạn cần những bản union
hack xấu xí : Làm cách nào tôi có thể triển khai bộ đếm ABA với c ++ 11 CAS? . (Đừng nhầm lẫn DWCAS với DCAS của 2 vị trí bộ nhớ riêng biệt . Việc mô phỏng nguyên tử không khóa của DCAS là không thể với DWCAS, nhưng bộ nhớ giao dịch (như x86 TSX) có thể thực hiện được.)
8.2.4 Bộ nhớ giao dịch : Sau khi một vài lần khởi động sai (được phát hành sau đó bị vô hiệu hóa bởi bản cập nhật vi mã do lỗi hiếm khi được kích hoạt), Intel đã làm việc bộ nhớ giao dịch trong Broadwell mô hình muộn và tất cả các CPU Skylake. Thiết kế vẫn là những gì David Kanter mô tả cho Haswell . Có một cách khóa hình elip để sử dụng nó để tăng tốc mã sử dụng (và có thể quay lại) một khóa thông thường (đặc biệt là với một khóa duy nhất cho tất cả các thành phần của một container nên nhiều luồng trong cùng một phần quan trọng thường không va chạm ) hoặc để viết mã biết về giao dịch trực tiếp.
7.5 Hugepages : các ôm trong suốt ẩn danh hoạt động tốt trên Linux mà không cần phải sử dụng hugetlbfs theo cách thủ công. Thực hiện phân bổ> = 2MiB với căn chỉnh 2MiB (ví dụ: posix_memalign
hoặcaligned_alloc
không thực thi yêu cầu ISO C ++ 17 ngu ngốc để thất bại khi size % alignment != 0
).
Một phân bổ ẩn danh được liên kết 2MiB sẽ sử dụng các ôm theo mặc định. Một số khối lượng công việc (ví dụ: tiếp tục sử dụng phân bổ lớn trong một thời gian sau khi thực hiện chúng) có thể được hưởng lợi từ
echo always >/sys/kernel/mm/transparent_hugepage/defrag
việc lấy kernel để chống phân mảnh bộ nhớ vật lý bất cứ khi nào cần, thay vì quay lại trang 4k. (Xem tài liệu kernel ). Ngoài ra, sử dụng madvise(MADV_HUGEPAGE)
sau khi thực hiện phân bổ lớn (tốt nhất vẫn là với căn chỉnh 2MiB).
Phụ lục B: Oprofile : Linux perf
hầu hết đã thay thế oprofile
. Đối với các sự kiện chi tiết cụ thể cho các kiến trúc vi mô nhất định, hãy sử dụng ocperf.py
trình bao bọc . ví dụ
ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out
Đối với một số ví dụ về việc sử dụng nó, hãy xem MOV của x86 có thực sự "miễn phí" không? Tại sao tôi không thể tái tạo điều này cả? .
Từ cái nhìn nhanh qua của tôi, nó trông khá chính xác. Một điều cần chú ý, là phần về sự khác biệt giữa bộ điều khiển bộ nhớ "tích hợp" và "bên ngoài". Kể từ khi phát hành CPU Intel dòng i7 đều được tích hợp và AMD đã sử dụng bộ điều khiển bộ nhớ tích hợp kể từ khi chip AMD64 được phát hành lần đầu tiên.
Vì bài viết này được viết, không có nhiều thay đổi, tốc độ tăng cao hơn, bộ điều khiển bộ nhớ đã thông minh hơn rất nhiều (i7 sẽ trì hoãn ghi vào RAM cho đến khi cảm thấy như thay đổi), nhưng không thay đổi nhiều . Ít nhất là không theo bất kỳ cách nào mà một nhà phát triển phần mềm sẽ quan tâm.