Làm thế nào để một mã viết sử dụng tốt nhất bộ đệm CPU để cải thiện hiệu suất?


159

Điều này nghe có vẻ giống như một câu hỏi chủ quan, nhưng những gì tôi đang tìm kiếm là những trường hợp cụ thể, mà bạn có thể đã gặp phải liên quan đến vấn đề này.

  1. Làm thế nào để tạo mã, bộ nhớ cache hiệu quả / thân thiện với bộ đệm (nhiều lần truy cập bộ đệm hơn, càng ít bộ nhớ cache càng tốt)? Từ cả hai quan điểm, bộ đệm dữ liệu & bộ đệm chương trình (bộ đệm hướng dẫn), tức là những thứ trong mã của một người, liên quan đến cấu trúc dữ liệu và cấu trúc mã, nên chú ý để làm cho bộ đệm có hiệu quả.

  2. Có bất kỳ cấu trúc dữ liệu cụ thể nào mà người ta phải sử dụng / tránh hoặc có một cách cụ thể để truy cập các thành viên của cấu trúc đó, v.v ... để làm cho bộ đệm mã hiệu quả.

  3. Có bất kỳ cấu trúc chương trình nào (if, for, switch, break, goto, ...), dòng mã (cho bên trong một if, nếu bên trong một for, v.v ...) người ta nên theo dõi / tránh trong vấn đề này không?

Tôi mong muốn được nghe những kinh nghiệm cá nhân liên quan đến việc tạo mã hiệu quả cho bộ đệm nói chung. Nó có thể là bất kỳ ngôn ngữ lập trình nào (C, C ++, hội, ...), bất kỳ mục tiêu phần cứng nào (ARM, Intel, PowerPC, ...), bất kỳ HĐH nào (Windows, Linux, S ymbian, ...), v.v. .

Sự đa dạng sẽ giúp tốt hơn để hiểu sâu sắc về nó.


1
Là một phần giới thiệu, bài nói chuyện này cung cấp một cái nhìn tổng quan tốt về youtu.be/BP6NxVxDQIs
schoetbi

URL rút gọn ở trên dường như không còn hoạt động nữa, đây là URL đầy đủ để nói chuyện: youtube.com/watch?v=BP6NxVxDQIs
Abhinav Upadhyay

Câu trả lời:


119

Bộ nhớ cache ở đó để giảm số lần CPU sẽ chờ đợi yêu cầu bộ nhớ được thực hiện (tránh độ trễ bộ nhớ ) và như một hiệu ứng thứ hai, có thể để giảm tổng lượng dữ liệu cần truyền (bảo quản băng thông bộ nhớ ).

Các kỹ thuật để tránh bị ảnh hưởng bởi độ trễ tìm nạp bộ nhớ thường là điều đầu tiên cần xem xét và đôi khi giúp ích cho một chặng đường dài. Băng thông bộ nhớ hạn chế cũng là một yếu tố hạn chế, đặc biệt đối với các ứng dụng đa lõi và đa luồng trong đó nhiều luồng muốn sử dụng bus bộ nhớ. Một bộ kỹ thuật khác nhau giúp giải quyết vấn đề sau.

Cải thiện địa phương không gian có nghĩa là bạn đảm bảo rằng mỗi dòng bộ đệm được sử dụng đầy đủ một khi nó đã được ánh xạ tới bộ đệm. Khi chúng tôi đã xem xét các tiêu chuẩn chuẩn khác nhau, chúng tôi đã thấy rằng một phần lớn đáng ngạc nhiên trong số đó không sử dụng 100% các dòng bộ đệm được tìm nạp trước khi các dòng bộ đệm được gỡ bỏ.

Cải thiện việc sử dụng dòng bộ đệm giúp ở ba khía cạnh:

  • Nó có xu hướng phù hợp với dữ liệu hữu ích hơn trong bộ đệm, về cơ bản làm tăng kích thước bộ đệm hiệu quả.
  • Nó có xu hướng phù hợp với dữ liệu hữu ích hơn trong cùng một dòng bộ đệm, làm tăng khả năng dữ liệu được yêu cầu có thể được tìm thấy trong bộ đệm.
  • Nó làm giảm yêu cầu băng thông bộ nhớ, vì sẽ có ít lần tải hơn.

Các kỹ thuật phổ biến là:

  • Sử dụng các loại dữ liệu nhỏ hơn
  • Sắp xếp dữ liệu của bạn để tránh các lỗ căn chỉnh (sắp xếp các thành viên cấu trúc của bạn bằng cách giảm kích thước là một cách)
  • Cảnh giác với bộ cấp phát bộ nhớ động tiêu chuẩn, có thể giới thiệu các lỗ hổng và phân tán dữ liệu của bạn xung quanh trong bộ nhớ khi nó nóng lên.
  • Hãy chắc chắn rằng tất cả các dữ liệu liền kề thực sự được sử dụng trong các vòng lặp nóng. Mặt khác, xem xét việc phá vỡ cấu trúc dữ liệu thành các thành phần nóng và lạnh, để các vòng nóng sử dụng dữ liệu nóng.
  • tránh các thuật toán và cơ sở dữ liệu thể hiện các mẫu truy cập không thường xuyên và ưu tiên các cơ sở dữ liệu tuyến tính.

Chúng ta cũng nên lưu ý rằng có nhiều cách khác để che giấu độ trễ bộ nhớ hơn là sử dụng bộ nhớ cache.

CPU hiện đại: s thường có một hoặc nhiều bộ nạp trước phần cứng . Họ luyện tập các lỗi trong bộ nhớ cache và cố gắng phát hiện thường xuyên. Chẳng hạn, sau một vài lần bỏ lỡ các dòng bộ đệm tiếp theo, trình tải trước hw sẽ bắt đầu tìm nạp các dòng bộ đệm vào bộ đệm, dự đoán nhu cầu của ứng dụng. Nếu bạn có một mẫu truy cập thường xuyên, trình tải trước phần cứng thường làm rất tốt. Và nếu chương trình của bạn không hiển thị các mẫu truy cập thông thường, bạn có thể cải thiện mọi thứ bằng cách tự thêm các hướng dẫn tìm nạp trước .

Sắp xếp lại các hướng dẫn theo cách mà những lỗi luôn bị bỏ lỡ trong bộ đệm xảy ra gần nhau, CPU đôi khi có thể chồng lấp các lần tìm nạp này để ứng dụng chỉ duy trì một lần chạm trễ ( song song mức bộ nhớ ).

Để giảm áp suất bus bộ nhớ tổng thể, bạn phải bắt đầu giải quyết cái được gọi là địa phương tạm thời . Điều này có nghĩa là bạn phải sử dụng lại dữ liệu trong khi dữ liệu vẫn chưa bị xóa khỏi bộ đệm.

Hợp nhất các vòng lặp chạm vào cùng một dữ liệu ( hợp nhất vòng lặp ) và sử dụng các kỹ thuật viết lại được gọi là ốp lát hoặc chặn tất cả các nỗ lực để tránh các bộ nhớ bổ sung đó.

Mặc dù có một số quy tắc đối với bài tập viết lại này, bạn thường phải xem xét cẩn thận các phụ thuộc dữ liệu mang theo vòng lặp, để đảm bảo rằng bạn không ảnh hưởng đến ngữ nghĩa của chương trình.

Những điều này là những gì thực sự được đền đáp trong thế giới đa lõi, nơi bạn thường không thấy nhiều cải tiến thông lượng sau khi thêm luồng thứ hai.


5
Khi chúng tôi đã xem xét các tiêu chuẩn chuẩn khác nhau, chúng tôi đã thấy rằng một phần lớn đáng ngạc nhiên trong số đó không sử dụng 100% các dòng bộ đệm được tìm nạp trước khi các dòng bộ đệm được gỡ bỏ. Tôi có thể hỏi loại công cụ định hình nào cung cấp cho bạn loại thông tin này không, và bằng cách nào?
Năng lượng rồng

"Sắp xếp dữ liệu của bạn để tránh các lỗ căn chỉnh (sắp xếp các thành viên cấu trúc của bạn bằng cách giảm kích thước là một cách)" - tại sao trình biên dịch không tự tối ưu hóa điều này? tại sao trình biên dịch không thể luôn luôn "sắp xếp các thành viên bằng cách giảm kích thước"? lợi thế để giữ cho các thành viên chưa được sắp xếp là gì?
javapowered

Tôi biết không phải nguồn gốc, nhưng đối với một người, thứ tự thành viên là rất quan trọng trong giả sử giao tiếp mạng, nơi bạn có thể muốn gửi toàn bộ cấu trúc từng byte qua web.
Kobrar

1
@javapowered Trình biên dịch có thể làm điều đó tùy thuộc vào ngôn ngữ, mặc dù tôi không chắc liệu có ai trong số họ làm không. Lý do bạn không thể làm điều đó trong C là vì nó hoàn toàn hợp lệ để giải quyết các thành viên theo địa chỉ cơ sở + bù thay vì theo tên, điều đó có nghĩa là sắp xếp lại các thành viên sẽ phá vỡ hoàn toàn chương trình.
Dan Bechard

56

Tôi không thể tin rằng không có nhiều câu trả lời cho điều này. Dù sao, một ví dụ kinh điển là lặp lại một mảng nhiều chiều "từ trong ra ngoài":

pseudocode
for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[j][i]

Lý do đây là bộ đệm không hiệu quả là vì các CPU hiện đại sẽ tải dòng bộ đệm với các địa chỉ bộ nhớ "gần" từ bộ nhớ chính khi bạn truy cập vào một địa chỉ bộ nhớ. Chúng tôi đang lặp qua các hàng "j" (bên ngoài) trong mảng trong vòng lặp bên trong, do đó, với mỗi chuyến đi qua vòng bên trong, dòng bộ đệm sẽ gây ra bị xóa và được tải với một dòng địa chỉ gần [ j] [i] mục. Nếu điều này được thay đổi thành tương đương:

for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[i][j]

Nó sẽ chạy nhanh hơn nhiều.


9
trở lại trường đại học, chúng tôi đã có một bài tập về nhân ma trận. Hóa ra trước tiên, việc chuyển đổi ma trận "cột" nhanh hơn và nhân các hàng theo hàng thay vì hàng bằng cols vì lý do chính xác đó.
ykaganovich

11
trên thực tế, hầu hết các trình biên dịch hiện đại có thể tìm ra điều này bởi các phần tử của nó (đã bật tối ưu hóa)
Ricardo Nolde

1
@ykaganovich Đó cũng là ví dụ trong bài viết của Ulrich Dreppers: lwn.net/Articles/255364
Simon Stender Boisen

Tôi không chắc chắn điều này luôn đúng - nếu toàn bộ mảng nằm trong bộ đệm L1 (thường là 32k!) Thì cả hai đơn hàng sẽ có cùng số lần truy cập và lỗi bộ nhớ cache. Có lẽ tôi đoán trước bộ nhớ có thể có một số tác động tôi đoán. Hạnh phúc khi được sửa chữa tất nhiên.
Matt Parkins

Ai sẽ chọn phiên bản đầu tiên của mã này nếu thứ tự không quan trọng?
silver_rocket

45

Các quy tắc cơ bản thực sự khá đơn giản. Trường hợp trở nên khó khăn là cách họ áp dụng cho mã của bạn.

Bộ đệm hoạt động theo hai nguyên tắc: Địa phương tạm thời và địa phương không gian. Ý tưởng trước đây là nếu gần đây bạn đã sử dụng một đoạn dữ liệu nhất định, có thể bạn sẽ cần lại nó sớm. Điều thứ hai có nghĩa là nếu gần đây bạn đã sử dụng dữ liệu tại địa chỉ X, có thể bạn sẽ sớm cần địa chỉ X + 1.

Bộ nhớ cache cố gắng thực hiện điều này bằng cách ghi nhớ các khối dữ liệu được sử dụng gần đây nhất. Nó hoạt động với các dòng bộ đệm, thường có kích thước 128 byte hoặc hơn, vì vậy ngay cả khi bạn chỉ cần một byte duy nhất, toàn bộ dòng bộ đệm chứa nó sẽ bị kéo vào bộ đệm. Vì vậy, nếu bạn cần byte sau đây, nó sẽ có trong bộ đệm.

Và điều này có nghĩa là bạn sẽ luôn muốn mã của riêng mình khai thác hai dạng địa phương này càng nhiều càng tốt. Đừng nhảy qua bộ nhớ. Làm nhiều việc nhất có thể trên một khu vực nhỏ, sau đó chuyển sang khu vực tiếp theo và làm càng nhiều công việc càng tốt.

Một ví dụ đơn giản là truyền tải mảng 2D mà câu trả lời của 1800 cho thấy. Nếu bạn duyệt qua một hàng tại một thời điểm, bạn sẽ đọc bộ nhớ theo tuần tự. Nếu bạn thực hiện theo cách khôn ngoan, bạn sẽ đọc một mục, sau đó chuyển đến một vị trí hoàn toàn khác (bắt đầu của hàng tiếp theo), đọc một mục và nhảy lại. Và khi cuối cùng bạn quay lại hàng đầu tiên, nó sẽ không còn trong bộ đệm.

Áp dụng tương tự cho mã. Nhảy hoặc các nhánh có nghĩa là sử dụng bộ đệm kém hiệu quả hơn (vì bạn không đọc hướng dẫn tuần tự, mà nhảy đến một địa chỉ khác). Tất nhiên, các câu lệnh if nhỏ có thể sẽ không thay đổi bất cứ điều gì (bạn chỉ bỏ qua một vài byte, vì vậy bạn sẽ vẫn ở trong vùng được lưu trong bộ nhớ cache), nhưng các lệnh gọi hàm thường ngụ ý rằng bạn đang nhảy sang một cách hoàn toàn khác địa chỉ có thể không được lưu trữ. Trừ khi nó được gọi gần đây.

Hướng dẫn sử dụng bộ đệm thường ít hơn một vấn đề. Những gì bạn thường cần phải lo lắng là bộ đệm dữ liệu.

Trong một cấu trúc hoặc lớp, tất cả các thành viên được đặt liên tục, đó là tốt. Trong một mảng, tất cả các mục được đặt liên tục là tốt. Trong các danh sách được liên kết, mỗi nút được phân bổ tại một vị trí hoàn toàn khác nhau, điều này là xấu. Con trỏ nói chung có xu hướng trỏ đến các địa chỉ không liên quan, điều này có thể sẽ dẫn đến việc bỏ lỡ bộ đệm nếu bạn bỏ qua nó.

Và nếu bạn muốn khai thác nhiều lõi, điều đó có thể thực sự thú vị, vì thông thường, chỉ có một CPU có thể có bất kỳ địa chỉ cụ thể nào trong bộ đệm L1 của nó tại một thời điểm. Vì vậy, nếu cả hai lõi liên tục truy cập vào cùng một địa chỉ, điều đó sẽ dẫn đến việc bỏ lỡ bộ nhớ cache liên tục, vì chúng đang chiến đấu với địa chỉ đó.


4
+1, lời khuyên tốt và thiết thực. Một bổ sung: Địa phương thời gian và địa phương không gian kết hợp đề xuất, ví dụ, đối với ma trận ops, có thể nên chia chúng thành các ma trận nhỏ hơn hoàn toàn phù hợp với một dòng bộ đệm hoặc có hàng / cột phù hợp với các dòng bộ đệm. Tôi nhớ làm điều đó để hình dung ra multidim. dữ liệu. Nó cung cấp một số cú đá nghiêm trọng trong quần. Bạn nên nhớ rằng bộ nhớ cache chứa nhiều hơn một 'dòng';)
AndreasT

1
Bạn nói rằng chỉ có 1 CPU có thể có một địa chỉ nhất định trong bộ đệm L1 tại một thời điểm - tôi giả sử bạn có nghĩa là các dòng bộ đệm chứ không phải địa chỉ. Ngoài ra, tôi đã nghe nói về các vấn đề chia sẻ sai khi ít nhất một trong số các CPU đang ghi, nhưng không phải nếu cả hai chỉ đọc. Vì vậy, bằng cách 'truy cập', bạn thực sự có nghĩa là viết?
Joseph Garvin

2
@JosephGarvin: vâng, ý tôi là viết. Bạn đã đúng, nhiều lõi có thể có cùng một dòng bộ đệm trong bộ đệm L1 của chúng cùng một lúc, nhưng khi một lõi ghi vào các địa chỉ này, nó sẽ bị vô hiệu trong tất cả các bộ đệm L1 khác, và sau đó chúng phải tải lại trước khi chúng có thể làm được bất cứ điều gì với nó. Xin lỗi vì từ ngữ không chính xác (sai). :)
jalf

44

Tôi khuyên bạn nên đọc bài viết gồm 9 phần Những điều mà mọi lập trình viên nên biết về bộ nhớ của Ulrich Drepper nếu bạn quan tâm đến cách bộ nhớ và phần mềm tương tác. Nó cũng có sẵn dưới dạng PDF 104 trang .

Các phần đặc biệt liên quan đến câu hỏi này có thể là Phần 2 (bộ nhớ CPU) và Phần 5 (Những gì lập trình viên có thể làm - tối ưu hóa bộ đệm).


16
Bạn nên thêm một bản tóm tắt các điểm chính từ bài viết.
Azmisov

Đọc tuyệt vời, nhưng một cuốn sách khác PHẢI được đề cập ở đây là Hennessy, Patterson, Computer Architecture, A Quantitiative Access , có sẵn trong phiên bản thứ 5 của nó cho đến ngày hôm nay.
Haymo Kutschbach

15

Ngoài các mẫu truy cập dữ liệu, một yếu tố chính trong mã thân thiện với bộ đệm là kích thước dữ liệu . Ít dữ liệu hơn có nghĩa là nhiều hơn phù hợp với bộ đệm.

Đây chủ yếu là một yếu tố với cấu trúc dữ liệu phù hợp với bộ nhớ. Sự khôn ngoan "thông thường" nói rằng các cấu trúc dữ liệu phải được căn chỉnh tại các ranh giới từ vì CPU chỉ có thể truy cập toàn bộ các từ và nếu một từ chứa nhiều hơn một giá trị, bạn phải thực hiện thêm công việc (đọc-sửa-ghi thay vì viết đơn giản) . Nhưng cache có thể hoàn toàn vô hiệu hóa lập luận này.

Tương tự, một mảng boolean Java sử dụng toàn bộ byte cho mỗi giá trị để cho phép hoạt động trực tiếp trên các giá trị riêng lẻ. Bạn có thể giảm kích thước dữ liệu xuống 8 lần nếu bạn sử dụng các bit thực tế, nhưng sau đó việc truy cập vào các giá trị riêng lẻ trở nên phức tạp hơn nhiều, đòi hỏi các thao tác thay đổi bit và mặt nạ ( BitSetlớp thực hiện điều này cho bạn). Tuy nhiên, do hiệu ứng bộ đệm, điều này vẫn có thể nhanh hơn đáng kể so với sử dụng boolean [] khi mảng lớn. IIRC Tôi đã từng đạt được tốc độ tăng theo hệ số 2 hoặc 3 theo cách này.


9

Cấu trúc dữ liệu hiệu quả nhất cho bộ đệm là một mảng. Bộ nhớ cache hoạt động tốt nhất, nếu cấu trúc dữ liệu của bạn được đặt tuần tự khi CPU đọc toàn bộ dòng bộ đệm (thường là 32 byte trở lên) cùng một lúc từ bộ nhớ chính.

Bất kỳ thuật toán nào truy cập bộ nhớ theo thứ tự ngẫu nhiên đều lưu trữ bộ đệm vì nó luôn cần các dòng bộ đệm mới để chứa bộ nhớ được truy cập ngẫu nhiên. Mặt khác, một thuật toán chạy liên tục qua một mảng là tốt nhất bởi vì:

  1. Nó cho CPU cơ hội đọc trước, ví dụ như đặt thêm bộ nhớ vào bộ đệm, sẽ được truy cập sau. Đọc trước này cho một hiệu suất rất lớn.

  2. Chạy một vòng lặp chặt chẽ trên một mảng lớn cũng cho phép CPU lưu mã thực thi mã trong vòng lặp và trong hầu hết các trường hợp cho phép bạn thực hiện thuật toán hoàn toàn từ bộ nhớ đệm mà không phải chặn truy cập bộ nhớ ngoài.


@Grover: Về điểm của bạn 2. vì vậy người ta có thể nói rằng nếu vòng lặp chặt chẽ trong vòng, một hàm được gọi cho mỗi số vòng lặp, thì nó sẽ tìm nạp mã mới hoàn toàn và gây ra lỗi bộ nhớ cache, thay vào đó nếu bạn có thể đặt hàm này làm mã trong vòng lặp for, không có chức năng gọi, nó sẽ nhanh hơn do ít nhớ cache hơn?
Goldenmean

1
Có và không. Các chức năng mới sẽ được tải trong bộ đệm. Nếu có đủ dung lượng bộ đệm, thì ở lần lặp thứ hai, nó sẽ có chức năng đó trong bộ đệm nên không có lý do gì để tải lại nó. Vì vậy, nó là một hit trong cuộc gọi đầu tiên. Trong C / C ++, bạn có thể yêu cầu trình biên dịch đặt các hàm ngay cạnh nhau bằng các phân đoạn thích hợp.
Grover

Thêm một lưu ý: Nếu bạn gọi ra khỏi vòng lặp và không đủ không gian bộ đệm, chức năng mới sẽ được tải vào bộ đệm bất kể. Nó thậm chí có thể xảy ra rằng vòng lặp ban đầu sẽ bị loại khỏi bộ đệm. Trong trường hợp này, cuộc gọi sẽ phải chịu tới ba hình phạt cho mỗi lần lặp: Một để tải mục tiêu cuộc gọi và một lần khác để tải lại vòng lặp. Và thứ ba nếu đầu vòng lặp không nằm trong cùng dòng bộ đệm với địa chỉ trả lại cuộc gọi. Trong trường hợp đó, nhảy đến đầu vòng lặp cũng cần truy cập bộ nhớ mới.
Grover

8

Một ví dụ tôi thấy được sử dụng trong một công cụ trò chơi là để di chuyển dữ liệu ra khỏi các đối tượng và vào các mảng riêng của chúng. Một đối tượng trò chơi thuộc đối tượng vật lý cũng có thể có rất nhiều dữ liệu khác được đính kèm. Nhưng trong vòng cập nhật vật lý, tất cả các động cơ quan tâm là dữ liệu về vị trí, tốc độ, khối lượng, hộp giới hạn, v.v. Vì vậy, tất cả những thứ đó được đặt vào các mảng riêng và tối ưu hóa càng nhiều càng tốt cho SSE.

Vì vậy, trong vòng lặp vật lý, dữ liệu vật lý được xử lý theo thứ tự mảng bằng toán học vectơ. Các đối tượng trò chơi đã sử dụng ID đối tượng của họ làm chỉ mục vào các mảng khác nhau. Nó không phải là một con trỏ bởi vì các con trỏ có thể bị vô hiệu nếu các mảng phải được di dời.

Theo nhiều cách, điều này đã vi phạm các mẫu thiết kế hướng đối tượng nhưng nó làm cho mã nhanh hơn rất nhiều bằng cách đặt dữ liệu gần nhau cần được vận hành trong cùng một vòng.

Ví dụ này có lẽ đã lỗi thời vì tôi hy vọng hầu hết các trò chơi hiện đại đều sử dụng công cụ vật lý dựng sẵn như Havok.


2
+1 Không hoàn toàn lỗi thời. Đây là cách tốt nhất để sắp xếp dữ liệu cho các công cụ trò chơi - tạo các khối dữ liệu liền kề và thực hiện tất cả các loại hoạt động nhất định (nói AI) trước khi chuyển sang tiếp theo (nói về vật lý) để tận dụng khoảng cách / vị trí bộ đệm của bộ đệm tài liệu tham khảo.
Kỹ sư

Tôi đã thấy ví dụ chính xác này trong một video ở đâu đó vài tuần trước, nhưng từ đó đã mất liên kết đến nó / không thể nhớ làm thế nào để tìm thấy nó. Bạn có nhớ nơi bạn đã thấy ví dụ này không?
sẽ

@will: Không, tôi không nhớ chính xác nơi này.
Zan Lynx

Đây là ý tưởng của một hệ thống thành phần thực thể (ECS: en.wikipedia.org/wiki/Entity_component_system ). Lưu trữ dữ liệu dưới dạng cấu trúc của mảng chứ không phải là mảng cấu trúc truyền thống hơn mà các thực tiễn OOP khuyến khích.
BuschnicK

7

Chỉ có một bài viết chạm vào nó, nhưng một vấn đề lớn xuất hiện khi chia sẻ dữ liệu giữa các quy trình. Bạn muốn tránh việc có nhiều quá trình cố gắng sửa đổi cùng một dòng bộ đệm. Một cái gì đó để ý ở đây là chia sẻ "sai", trong đó hai cấu trúc dữ liệu liền kề chia sẻ một dòng bộ đệm và sửa đổi một dòng không hợp lệ cho dòng kia cho bộ đệm. Điều này có thể khiến các dòng bộ đệm di chuyển qua lại một cách không cần thiết giữa các bộ đệm của bộ xử lý chia sẻ dữ liệu trên hệ thống đa bộ xử lý. Một cách để tránh nó là căn chỉnh và đệm cấu trúc dữ liệu để đặt chúng trên các dòng khác nhau.


7

Một nhận xét cho "ví dụ cổ điển" của người dùng 1800 THÔNG TIN (quá dài cho một nhận xét)

Tôi muốn kiểm tra sự khác biệt về thời gian cho hai lệnh lặp ("outter" và "bên trong"), vì vậy tôi đã thực hiện một thử nghiệm đơn giản với một mảng 2D lớn:

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

và trường hợp thứ hai với các forvòng lặp hoán đổi.

Phiên bản chậm hơn ("x đầu tiên") là 0,88 giây và phiên bản nhanh hơn là 0,06 giây. Đó là sức mạnh của bộ nhớ đệm :)

Tôi đã sử dụng gcc -O2và vẫn còn các vòng lặp không được tối ưu hóa. Nhận xét của Ricardo rằng "hầu hết các trình biên dịch hiện đại có thể tìm ra điều này bởi các phần tử của nó" không giữ


Không chắc chắn tôi nhận được điều này. Trong cả hai ví dụ, bạn vẫn đang truy cập từng biến trong vòng lặp for. Tại sao một cách nhanh hơn so với cách khác?
ed-

cuối cùng trực quan để tôi hiểu nó ảnh hưởng như thế nào :)
Laie

@EdwardCorlew Đó là vì thứ tự mà chúng được truy cập. Đơn hàng đầu tiên nhanh hơn vì nó truy cập dữ liệu tuần tự. Khi mục nhập đầu tiên được yêu cầu, bộ đệm L1 tải toàn bộ dòng bộ đệm, bao gồm int được yêu cầu cộng với 15 dòng tiếp theo (giả sử dòng bộ đệm 64 byte), do đó, không có gian hàng CPU nào chờ đợi cho 15 tiếp theo Thứ tự đầu tiên chậm hơn vì phần tử được truy cập không tuần tự và có lẽ N đủ lớn để bộ nhớ được truy cập luôn nằm ngoài bộ đệm L1 và do đó mọi thao tác đều bị trì hoãn.
Matt Parkins

4

Tôi có thể trả lời (2) bằng cách nói rằng trong thế giới C ++, các danh sách được liên kết có thể dễ dàng giết chết bộ đệm CPU. Mảng là một giải pháp tốt hơn nếu có thể. Không có kinh nghiệm về việc liệu điều tương tự có áp dụng cho các ngôn ngữ khác hay không, nhưng thật dễ để tưởng tượng những vấn đề tương tự sẽ phát sinh.


@Andrew: Làm thế nào về cấu trúc. Họ có bộ nhớ cache hiệu quả? Họ có bất kỳ hạn chế kích thước để được bộ nhớ cache hiệu quả?
Goldenmean

Một cấu trúc là một khối bộ nhớ duy nhất, miễn là nó không vượt quá kích thước bộ đệm của bạn, bạn sẽ không thấy tác động. Chỉ khi bạn có một bộ sưu tập các cấu trúc (hoặc các lớp) thì bạn mới thấy các lần truy cập bộ đệm và nó phụ thuộc vào cách bạn tổ chức bộ sưu tập. Một mảng hất các đối tượng lên nhau (tốt) nhưng một danh sách được liên kết có thể có các đối tượng trên khắp không gian địa chỉ của bạn với các liên kết giữa chúng, điều này rõ ràng không tốt cho hiệu năng bộ đệm.
Andrew

Một số cách để sử dụng danh sách được liên kết mà không làm chết bộ đệm, hiệu quả nhất đối với danh sách không lớn, là tạo nhóm bộ nhớ của riêng bạn, nghĩa là - phân bổ một mảng lớn. sau đó thay vì bộ nhớ 'malloc'ing (hoặc' new'ing trong C ++) cho mỗi thành viên danh sách được liên kết nhỏ, có thể được phân bổ ở một vị trí hoàn toàn khác trong bộ nhớ và không gian quản lý lãng phí, bạn cung cấp cho bộ nhớ từ nhóm bộ nhớ của bạn, tăng tỷ lệ cược cao mà các thành viên đóng danh sách một cách hợp lý sẽ nằm trên bộ đệm cùng nhau.
Liran Orevi

Chắc chắn, nhưng đó là rất nhiều công việc nhận std :: list <> et al. để sử dụng các khối bộ nhớ tùy chỉnh của bạn. Khi tôi còn là một chủ hàng trẻ tuổi, tôi hoàn toàn đi theo con đường đó, nhưng những ngày này ... có quá nhiều thứ khác để giải quyết.
Andrew


4

Bộ nhớ cache được sắp xếp theo "dòng bộ đệm" và bộ nhớ (thực) được đọc và ghi vào các khối có kích thước này.

Do đó, các cấu trúc dữ liệu được chứa trong một dòng bộ đệm sẽ hiệu quả hơn.

Tương tự, các thuật toán truy cập các khối bộ nhớ liền kề sẽ hiệu quả hơn các thuật toán nhảy qua bộ nhớ theo thứ tự ngẫu nhiên.

Thật không may, kích thước dòng bộ đệm thay đổi đáng kể giữa các bộ xử lý, vì vậy không có cách nào để đảm bảo rằng cấu trúc dữ liệu tối ưu trên một bộ xử lý sẽ hiệu quả với bất kỳ bộ xử lý nào khác.


không cần thiết. chỉ cần cẩn thận về chia sẻ sai. đôi khi bạn phải chia dữ liệu thành các dòng bộ đệm khác nhau. hiệu quả của bộ đệm luôn phụ thuộc vào cách bạn sử dụng nó.
DAG

4

Để hỏi cách tạo mã, bộ nhớ cache hiệu quả - thân thiện với bộ đệm và hầu hết các câu hỏi khác, thường là hỏi cách Tối ưu hóa chương trình, đó là vì bộ đệm có tác động rất lớn đến hiệu suất mà bất kỳ chương trình tối ưu hóa nào cũng là bộ đệm hiệu quả-bộ nhớ cache thân thiện.

Tôi đề nghị đọc về Tối ưu hóa, có một số câu trả lời tốt trên trang web này. Về mặt sách, tôi khuyên dùng trên Hệ thống máy tính: Phối cảnh của lập trình viên có một số văn bản hay về cách sử dụng đúng bộ đệm.

(btw - tệ như bộ nhớ cache có thể xảy ra, còn tệ hơn - nếu một chương trình được phân trang từ ổ cứng ...)


4

Đã có rất nhiều câu trả lời về các lời khuyên chung như lựa chọn cấu trúc dữ liệu, mẫu truy cập, v.v ... Ở đây tôi muốn thêm một mẫu thiết kế mã khác gọi là đường ống phần mềm sử dụng quản lý bộ đệm hoạt động.

Ý tưởng là mượn từ các kỹ thuật đường ống khác, ví dụ đường ống dẫn CPU.

Kiểu mẫu này áp dụng tốt nhất cho các thủ tục

  1. có thể được chia thành nhiều bước phụ hợp lý, S [1], S [2], S [3], ... có thời gian thực hiện gần tương đương với thời gian truy cập RAM (~ 60-70ns).
  2. mất một loạt đầu vào và thực hiện nhiều bước đã nói ở trên để có kết quả.

Hãy lấy một trường hợp đơn giản chỉ có một thủ tục phụ. Thông thường mã sẽ muốn:

def proc(input):
    return sub-step(input))

Để có hiệu suất tốt hơn, bạn có thể muốn chuyển nhiều đầu vào cho hàm theo một đợt để bạn khấu hao phí gọi hàm và cũng tăng địa phương bộ đệm mã.

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

Tuy nhiên, như đã nói trước đó, nếu việc thực hiện bước này gần giống như thời gian truy cập RAM, bạn có thể cải thiện thêm mã thành một cái gì đó như thế này:

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

Luồng thực hiện sẽ như sau:

  1. tìm nạp trước (1) yêu cầu CPU tìm nạp trước [1] vào bộ đệm, trong đó lệnh tìm nạp trước sẽ tự P quay trở lại và trong đầu vào nền [1] sẽ đến bộ đệm sau R chu kỳ.
  2. works_on (0) lạnh nhớ 0 và hoạt động trên đó, mất M
  3. prefetch (2) phát hành một lần tìm nạp khác
  4. works_on (1) nếu P + R <= M, thì đầu vào [1] phải nằm trong bộ đệm đã có trước bước này, do đó tránh bỏ lỡ bộ đệm dữ liệu
  5. hoạt động_on (2) ...

Có thể có nhiều bước liên quan hơn, sau đó bạn có thể thiết kế một đường ống nhiều giai đoạn miễn là thời gian của các bước và độ trễ truy cập bộ nhớ phù hợp, bạn sẽ bị mất ít bộ đệm / mã dữ liệu. Tuy nhiên, quá trình này cần được điều chỉnh với nhiều thử nghiệm để tìm ra nhóm các bước và thời gian tìm nạp đúng. Do nỗ lực cần thiết của nó, nó thấy việc áp dụng nhiều hơn trong xử lý luồng dữ liệu / gói hiệu suất cao. Một ví dụ mã sản xuất tốt có thể được tìm thấy trong thiết kế đường ống DPDK QoS Enqueue: http://dpdk.org/doc/guides/prog_guide/qos_framework.html Chương 21.2.4.3. Đường ống Enqueue.

Thêm thông tin có thể được tìm thấy:

https://software.intel.com/en-us/articles/memory-manloyment-for-optimal-performance-on-intel-xeon-phi-coprocessor-alocate-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

Viết chương trình của bạn để có một kích thước tối thiểu. Đó là lý do tại sao không phải lúc nào cũng nên sử dụng tối ưu hóa -O3 cho GCC. Nó chiếm một kích thước lớn hơn. Thông thường, -Os cũng tốt như -O2. Tất cả phụ thuộc vào bộ xử lý được sử dụng mặc dù. YMMV.

Làm việc với các khối dữ liệu nhỏ tại một thời điểm. Đó là lý do tại sao một thuật toán sắp xếp kém hiệu quả hơn có thể chạy nhanh hơn quicksort nếu bộ dữ liệu lớn. Tìm cách chia nhỏ các tập dữ liệu lớn hơn của bạn thành các tập dữ liệu nhỏ hơn. Những người khác đã đề nghị điều này.

Để giúp bạn khai thác tốt hơn hướng dẫn địa phương / không gian địa phương, bạn có thể muốn nghiên cứu cách mã của bạn được chuyển đổi thành lắp ráp. Ví dụ:

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

Hai vòng lặp tạo ra các mã khác nhau mặc dù chúng chỉ phân tích cú pháp thông qua một mảng. Trong mọi trường hợp, câu hỏi của bạn là rất kiến ​​trúc cụ thể. Vì vậy, cách duy nhất để kiểm soát chặt chẽ việc sử dụng bộ đệm là hiểu cách phần cứng hoạt động và tối ưu hóa mã của bạn cho nó.


Điểm thú vị. Các bộ đệm nhìn về phía trước có đưa ra các giả định dựa trên hướng của một vòng lặp / truyền qua bộ nhớ không?
Andrew

1
Có nhiều cách để thiết kế bộ nhớ dữ liệu đầu cơ. Những người dựa trên sải chân thực hiện đo 'khoảng cách' và 'hướng' của truy cập dữ liệu. Nội dung dựa trên những chuỗi con trỏ đuổi theo. Có nhiều cách khác để thiết kế chúng.
sybreon

1

Bên cạnh việc căn chỉnh cấu trúc và các trường của bạn, nếu cấu trúc của bạn nếu được phân bổ heap, bạn có thể muốn sử dụng các phân bổ hỗ trợ phân bổ được căn chỉnh; như _align_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE); nếu không, bạn có thể có chia sẻ sai ngẫu nhiên; hãy nhớ rằng trong Windows, heap mặc định có căn chỉnh 16 byte.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.