Mã thân thiện với bộ nhớ cache của bộ nhớ cache là gì?


738

Sự khác biệt giữa " mã không thân thiện với bộ đệm " và " thân thiện với bộ đệm " là gì?

Làm thế nào tôi có thể chắc chắn rằng tôi viết mã hiệu quả bộ nhớ cache?


28
Điều này có thể cung cấp cho bạn một gợi ý: stackoverflow.com/questions/9936132/ trộm
Robert Martin

4
Cũng cần lưu ý về kích thước của một dòng bộ đệm. Trên các bộ xử lý hiện đại, nó thường là 64 byte.
John Dibling

3
Đây là một bài viết rất tốt. Các nguyên tắc áp dụng cho các chương trình C / C ++ trên mọi HĐH (Linux, MaxOS hoặc Windows): lwn.net/Articles/255364
paulsm4

4
Câu hỏi liên quan: stackoverflow.com/questions/8469427/
Matt

Câu trả lời:


965

Sơ bộ

Trên các máy tính hiện đại, chỉ các cấu trúc bộ nhớ mức thấp nhất (các thanh ghi ) có thể di chuyển dữ liệu xung quanh trong các chu kỳ đồng hồ đơn. Tuy nhiên, các thanh ghi rất đắt tiền và hầu hết các lõi máy tính có ít hơn vài chục thanh ghi (tổng số từ vài trăm đến một nghìn byte ). Ở đầu kia của phổ bộ nhớ ( DRAM ), bộ nhớ rất rẻ (nghĩa là rẻ hơn hàng triệu lần ) nhưng phải mất hàng trăm chu kỳ sau khi yêu cầu nhận dữ liệu. Để thu hẹp khoảng cách giữa siêu nhanh, đắt và siêu chậm và rẻ là những bộ nhớ cache, được đặt tên là L1, L2, L3 với tốc độ và chi phí giảm. Ý tưởng là hầu hết các mã thực thi sẽ thường xuyên gặp phải một tập hợp các biến nhỏ và phần còn lại (một bộ biến lớn hơn nhiều) không thường xuyên. Nếu bộ xử lý không thể tìm thấy dữ liệu trong bộ đệm L1, thì nó sẽ tìm trong bộ đệm L2. Nếu không có thì bộ đệm L3 và nếu không có, bộ nhớ chính. Mỗi "lần bỏ lỡ" này đều tốn kém về thời gian.

(Tương tự bộ nhớ cache là bộ nhớ hệ thống, vì bộ nhớ hệ thống lưu trữ đĩa quá cứng. Lưu trữ đĩa cứng siêu rẻ nhưng rất chậm).

Bộ nhớ đệm là một trong những phương pháp chính để giảm tác động của độ trễ . Để diễn giải Herb Sutter (cfr. Link bên dưới): tăng băng thông là dễ dàng, nhưng chúng ta không thể mua cách thoát khỏi độ trễ .

Dữ liệu luôn được truy xuất thông qua hệ thống phân cấp bộ nhớ (nhỏ nhất == nhanh nhất đến chậm nhất). Một bộ nhớ cache hit / bỏ lỡ thường đề cập đến một hit / bỏ lỡ ở mức cao nhất của bộ nhớ cache trong CPU - bởi mức cao nhất tôi có nghĩa là lớn nhất == chậm nhất. Tốc độ nhấn bộ nhớ cache rất quan trọng đối với hiệu suất vì mọi bộ nhớ cache đều dẫn đến việc tìm nạp dữ liệu từ RAM (hoặc tệ hơn ...) mất rất nhiều thời gian (hàng trăm chu kỳ cho RAM, hàng chục triệu chu kỳ cho ổ cứng). Để so sánh, việc đọc dữ liệu từ bộ đệm (mức cao nhất) thường chỉ mất một vài chu kỳ.

Trong các kiến ​​trúc máy tính hiện đại, nút cổ chai hiệu năng đang khiến CPU chết (ví dụ: truy cập RAM hoặc cao hơn). Điều này sẽ chỉ trở nên tồi tệ hơn theo thời gian. Việc tăng tần số bộ xử lý hiện không còn phù hợp để tăng hiệu suất. Vấn đề là truy cập bộ nhớ. Do đó, những nỗ lực thiết kế phần cứng trong CPU hiện đang tập trung rất nhiều vào việc tối ưu hóa bộ nhớ cache, tìm nạp trước, đường ống dẫn và đồng thời. Chẳng hạn, các CPU hiện đại dành khoảng 85% chết cho bộ nhớ cache và tới 99% cho việc lưu trữ / di chuyển dữ liệu!

Có khá nhiều điều để nói về chủ đề này. Dưới đây là một vài tài liệu tham khảo tuyệt vời về bộ nhớ cache, phân cấp bộ nhớ và lập trình phù hợp:

Các khái niệm chính cho mã thân thiện với bộ đệm

Một khía cạnh rất quan trọng của mã thân thiện với bộ đệm là tất cả về nguyên tắc cục bộ , mục tiêu của nó là đặt dữ liệu liên quan gần trong bộ nhớ để cho phép bộ nhớ đệm hiệu quả. Về bộ đệm của CPU, điều quan trọng là phải biết về các dòng bộ đệm để hiểu cách thức hoạt động của nó: Làm thế nào để các dòng bộ đệm hoạt động?

Các khía cạnh cụ thể sau đây có tầm quan trọng cao để tối ưu hóa bộ nhớ đệm:

  1. Địa phương tạm thời : khi một vị trí bộ nhớ nhất định được truy cập, có khả năng cùng một vị trí được truy cập lại trong tương lai gần. Lý tưởng nhất, thông tin này sẽ vẫn được lưu trữ tại thời điểm đó.
  2. Địa phương không gian : điều này đề cập đến việc đặt dữ liệu liên quan gần nhau. Bộ nhớ đệm xảy ra ở nhiều cấp độ, không chỉ trong CPU. Ví dụ, khi bạn đọc từ RAM, thông thường một khối bộ nhớ lớn hơn được tìm nạp so với những gì được yêu cầu cụ thể vì rất thường xuyên chương trình sẽ yêu cầu dữ liệu đó sớm. Bộ nhớ cache theo cùng một dòng suy nghĩ. Cụ thể đối với bộ nhớ CPU, khái niệm về các dòng bộ đệm là quan trọng.

Sử dụng phù hợp hộp đựng

Một ví dụ đơn giản về thân thiện với bộ đệm so với bộ đệm không thân thiện là 's std::vectorso với std::list. Các yếu tố của một std::vectorđược lưu trữ trong bộ nhớ kề nhau, và như việc truy cập như vậy họ là nhiều hơn bộ nhớ cache thân thiện trừ các bộ phận truy cập trong một std::list, mà các cửa hàng nội dung của nó ở khắp mọi nơi. Điều này là do địa phương không gian.

Một minh họa rất hay về điều này được đưa ra bởi Bjarne Stroustrup trong clip youtube này (cảm ơn @Mohammad Ali Baydoun cho liên kết!).

Đừng bỏ qua bộ đệm trong cấu trúc dữ liệu và thiết kế thuật toán

Bất cứ khi nào có thể, hãy cố gắng điều chỉnh cấu trúc dữ liệu và thứ tự tính toán theo cách cho phép sử dụng tối đa bộ đệm. Một kỹ thuật phổ biến trong vấn đề này là chặn bộ đệm (phiên bản Archive.org) , cực kỳ quan trọng trong điện toán hiệu năng cao (ví dụ như ATLAS ).

Biết và khai thác cấu trúc ngầm của dữ liệu

Một ví dụ đơn giản khác, mà nhiều người trong lĩnh vực đôi khi quên là cột chính (ví dụ. ,) so với thứ tự hàng lớn (ví dụ ,) để lưu trữ các mảng hai chiều. Ví dụ, hãy xem xét ma trận sau:

1 2
3 4

Theo thứ tự chính hàng, điều này được lưu trữ trong bộ nhớ như 1 2 3 4; theo thứ tự chính cột, điều này sẽ được lưu trữ dưới dạng 1 3 2 4. Dễ dàng thấy rằng các triển khai không khai thác thứ tự này sẽ nhanh chóng gặp phải các vấn đề về bộ đệm (dễ dàng tránh được!). Thật không may, tôi thấy những thứ như thế này rất thường xuyên trong miền của tôi (máy học). @MatteoItalia đã cho thấy ví dụ này chi tiết hơn trong câu trả lời của mình.

Khi tìm nạp một phần tử nhất định của ma trận từ bộ nhớ, các phần tử gần nó cũng sẽ được tìm nạp và được lưu trữ trong một dòng bộ đệm. Nếu thứ tự được khai thác, điều này sẽ dẫn đến việc truy cập bộ nhớ ít hơn (vì một vài giá trị tiếp theo cần thiết cho các tính toán tiếp theo đã có trong một dòng bộ đệm).

Để đơn giản, giả sử bộ đệm bao gồm một dòng bộ đệm duy nhất có thể chứa 2 phần tử ma trận và khi một phần tử nhất định được lấy từ bộ nhớ, thì phần tiếp theo cũng vậy. Giả sử chúng tôi muốn lấy tổng số trên tất cả các phần tử trong ma trận 2x2 ví dụ ở trên (hãy gọi nó M):

Khai thác thứ tự (ví dụ: thay đổi chỉ mục cột đầu tiên trong ):

M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses

Không khai thác thứ tự (ví dụ: thay đổi chỉ mục hàng đầu tiên trong ):

M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses

Trong ví dụ đơn giản này, việc khai thác thứ tự tăng gấp đôi tốc độ thực hiện (vì truy cập bộ nhớ đòi hỏi nhiều chu kỳ hơn so với tính toán tổng). Trong thực tế, sự khác biệt hiệu suất có thể lớn hơn nhiều .

Tránh các chi nhánh không thể đoán trước

Các kiến ​​trúc hiện đại có các đường ống và trình biên dịch đang trở nên rất tốt trong việc sắp xếp lại mã để giảm thiểu độ trễ do truy cập bộ nhớ. Khi mã quan trọng của bạn chứa các nhánh (không thể đoán trước), rất khó hoặc không thể tìm nạp trước dữ liệu. Điều này sẽ gián tiếp dẫn đến bỏ lỡ bộ nhớ cache nhiều hơn.

Điều này được giải thích rất tốt ở đây (nhờ @ 0x90 cho liên kết): Tại sao xử lý một mảng được sắp xếp nhanh hơn xử lý một mảng chưa được sắp xếp?

Tránh các chức năng ảo

Trong ngữ cảnh của , virtualcác phương thức thể hiện một vấn đề gây tranh cãi liên quan đến lỗi bộ nhớ cache (tồn tại sự đồng thuận chung mà chúng nên tránh khi có thể về mặt hiệu suất). Các chức năng ảo có thể gây ra lỗi nhớ cache trong quá trình tra cứu, nhưng điều này chỉ xảy ra nếu chức năng cụ thể không được gọi thường xuyên (nếu không nó có thể bị lưu vào bộ đệm), do đó, điều này được coi là không có vấn đề. Để tham khảo về vấn đề này, hãy xem: Chi phí hiệu năng của việc có một phương thức ảo trong lớp C ++ là bao nhiêu?

Những vấn đề chung

Một vấn đề phổ biến trong các kiến ​​trúc hiện đại với bộ đệm đa bộ xử lý được gọi là chia sẻ sai . Điều này xảy ra khi mỗi bộ xử lý riêng lẻ đang cố gắng sử dụng dữ liệu trong vùng nhớ khác và cố lưu trữ nó trong cùng một dòng bộ đệm . Điều này khiến dòng bộ đệm - chứa dữ liệu mà bộ xử lý khác có thể sử dụng - bị ghi đè nhiều lần. Hiệu quả, các chủ đề khác nhau làm cho nhau chờ đợi bằng cách gây ra lỗi nhớ cache trong tình huống này. Xem thêm (cảm ơn @Matt cho liên kết): Làm thế nào và khi nào để căn chỉnh kích thước dòng bộ đệm?

Một triệu chứng cực đoan của bộ nhớ đệm kém trong bộ nhớ RAM (có lẽ không phải là ý của bạn trong ngữ cảnh này) được gọi là đập . Điều này xảy ra khi quá trình liên tục tạo ra lỗi trang (ví dụ: truy cập bộ nhớ không có trong trang hiện tại) yêu cầu truy cập đĩa.


27
có lẽ bạn có thể mở rộng câu trả lời một chút bằng cách giải thích rằng, dữ liệu mã đã đọc cũng có thể quá cục bộ (ví dụ: chia sẻ sai)
TemplateRex

2
Có thể có nhiều mức bộ nhớ cache như các nhà thiết kế chip nghĩ là hữu ích. Nói chung họ đang cân bằng tốc độ so với kích thước. Nếu bạn có thể làm cho bộ đệm L1 của mình lớn như L5 và nhanh như vậy, bạn sẽ chỉ cần L1.
Rafael Baptista

24
Tôi nhận thấy các bài viết thỏa thuận trống rỗng không được chấp thuận trên StackOverflow nhưng đây thực sự là câu trả lời rõ ràng nhất, tốt nhất mà tôi từng thấy cho đến nay. Làm tốt lắm, Marc.
Jack Aidley

2
@JackAidley cảm ơn lời khen của bạn! Khi tôi thấy lượng chú ý mà câu hỏi này nhận được, tôi đoán rằng nhiều người có thể quan tâm đến một lời giải thích có phần rộng rãi. Tôi rất vui vì nó hữu ích.
Marc Claesen

1
Những gì bạn đã không đề cập là các cấu trúc dữ liệu thân thiện với bộ đệm được thiết kế để phù hợp với một dòng bộ đệm và được căn chỉnh theo bộ nhớ để sử dụng tối ưu các dòng bộ đệm. Câu trả lời tuyệt vời mặc dù! tuyệt vời.
Matt

140

Ngoài câu trả lời của @Marc Claesen, tôi nghĩ rằng một ví dụ cổ điển mang tính hướng dẫn về mã không thân thiện với bộ đệm là mã quét một mảng hai chiều C (ví dụ: hình ảnh bitmap) thay vì theo hàng.

Các phần tử liền kề trong một hàng cũng liền kề trong bộ nhớ, do đó truy cập chúng theo thứ tự có nghĩa là truy cập chúng theo thứ tự bộ nhớ tăng dần; cái này thân thiện với bộ đệm, vì bộ đệm có xu hướng tìm nạp trước các khối bộ nhớ liền kề.

Thay vào đó, việc truy cập các phần tử như vậy là không thân thiện với bộ đệm, vì các phần tử trên cùng một cột nằm cách xa nhau trong bộ nhớ (đặc biệt, khoảng cách của chúng bằng với kích thước của hàng), vì vậy khi bạn sử dụng mẫu truy cập này, bạn đang nhảy xung quanh trong bộ nhớ, có khả năng lãng phí nỗ lực của bộ nhớ cache khi truy xuất các phần tử gần đó trong bộ nhớ.

Và tất cả những gì nó cần để làm hỏng hiệu suất là đi từ

// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
    for(unsigned int x=0; x<width; ++x)
    {
        ... image[y][x] ...
    }
}

đến

// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
    for(unsigned int y=0; y<height; ++y)
    {
        ... image[y][x] ...
    }
}

Hiệu ứng này có thể khá ấn tượng (một số bậc về tốc độ) trong các hệ thống có bộ nhớ nhỏ và / hoặc làm việc với các mảng lớn (ví dụ: 10+ megapixel hình ảnh 24 bpp trên các máy hiện tại); vì lý do này, nếu bạn phải thực hiện nhiều lần quét dọc, thường thì tốt hơn là xoay hình ảnh 90 độ trước và thực hiện các phân tích khác nhau sau đó, hạn chế mã không thân thiện với bộ đệm chỉ để xoay.


Err, nên là x <chiều rộng?
mowwwalker

13
Trình chỉnh sửa hình ảnh hiện đại sử dụng các ô xếp làm bộ nhớ trong, ví dụ: các khối 64x64 pixel. Điều này thân thiện với bộ nhớ cache hơn nhiều đối với các hoạt động cục bộ (đặt dab, chạy bộ lọc mờ) vì các pixel lân cận nằm gần bộ nhớ theo cả hai hướng, hầu hết thời gian.
MAXY

Tôi đã thử định thời gian cho một ví dụ tương tự trên máy của mình và tôi thấy rằng thời gian là như nhau. Có ai khác đã thử thời gian nó?
gsingh2011

@ I3arnon: không, đầu tiên là thân thiện với bộ đệm, vì thông thường trong các mảng C được lưu theo thứ tự chính hàng (tất nhiên nếu hình ảnh của bạn vì một lý do nào đó được lưu trữ theo thứ tự chính của cột thì điều ngược lại là đúng).
Matteo Italia

1
@Gauthier: vâng, đoạn đầu tiên là đoạn hay; Tôi nghĩ rằng khi tôi viết bài này, tôi đã nghĩ dọc theo dòng chữ "Tất cả những gì cần thiết [để phá hỏng hiệu năng của một ứng dụng đang hoạt động] là đi từ ... đến ..."
Matteo Italia

88

Tối ưu hóa việc sử dụng bộ nhớ cache phần lớn đến từ hai yếu tố.

Địa phương của tài liệu tham khảo

Yếu tố đầu tiên (mà những người khác đã ám chỉ) là địa phương tham chiếu. Địa phương của tài liệu tham khảo thực sự có hai chiều: không gian và thời gian.

  • Không gian

Kích thước không gian cũng có hai điều: thứ nhất, chúng tôi muốn đóng gói thông tin của chúng tôi dày đặc, vì vậy nhiều thông tin sẽ phù hợp với bộ nhớ hạn chế đó. Điều này có nghĩa (ví dụ) rằng bạn cần một sự cải thiện lớn về độ phức tạp tính toán để chứng minh các cấu trúc dữ liệu dựa trên các nút nhỏ được nối bởi các con trỏ.

Thứ hai, chúng tôi muốn thông tin sẽ được xử lý cùng nhau cũng nằm cùng nhau. Bộ đệm thông thường hoạt động theo "dòng", có nghĩa là khi bạn truy cập một số thông tin, thông tin khác tại các địa chỉ gần đó sẽ được tải vào bộ đệm với phần chúng tôi chạm vào. Ví dụ: khi tôi chạm vào một byte, bộ đệm có thể tải 128 hoặc 256 byte gần byte đó. Để tận dụng điều đó, bạn thường muốn dữ liệu được sắp xếp để tối đa hóa khả năng bạn cũng sẽ sử dụng dữ liệu khác được tải cùng một lúc.

Đối với một ví dụ thực sự tầm thường, điều này có thể có nghĩa là tìm kiếm tuyến tính có thể cạnh tranh hơn nhiều với tìm kiếm nhị phân hơn bạn mong đợi. Khi bạn đã tải một mục từ một dòng bộ đệm, sử dụng phần còn lại của dữ liệu trong dòng bộ đệm đó gần như miễn phí. Một tìm kiếm nhị phân trở nên nhanh hơn đáng kể chỉ khi dữ liệu đủ lớn để tìm kiếm nhị phân làm giảm số lượng dòng bộ đệm bạn truy cập.

  • Thời gian

Thứ nguyên thời gian có nghĩa là khi bạn thực hiện một số thao tác trên một số dữ liệu, bạn muốn (càng nhiều càng tốt) để thực hiện tất cả các thao tác trên dữ liệu đó cùng một lúc.

Vì bạn đã gắn thẻ này là C ++, tôi sẽ chỉ ra một ví dụ cổ điển về thiết kế tương đối không bộ nhớ cache : std::valarray. valarrayquá tải toán tử số học nhất, vì vậy tôi có thể (ví dụ) nói a = b + c + d;(trong đó a, b, cdtất cả đều valarrays) để làm Ngoài yếu tố khôn ngoan của những mảng.

Vấn đề với điều này là nó đi qua một cặp đầu vào, đặt kết quả tạm thời, đi qua một cặp đầu vào khác, v.v. Với rất nhiều dữ liệu, kết quả từ một tính toán có thể biến mất khỏi bộ đệm trước khi nó được sử dụng trong lần tính toán tiếp theo, vì vậy chúng tôi kết thúc việc đọc (và viết) dữ liệu nhiều lần trước khi chúng tôi nhận được kết quả cuối cùng. Nếu mỗi phần tử của kết quả cuối cùng sẽ là một cái gì đó giống như (a[n] + b[n]) * (c[n] + d[n]);, chúng tôi thường thích đọc từng a[n], b[n], c[n]d[n]một lần, thực hiện tính toán, viết kết quả, increment nvà lặp lại 'til chúng tôi đã hoàn tất. 2

Chia sẻ dòng

Yếu tố chính thứ hai là tránh chia sẻ dòng. Để hiểu điều này, có lẽ chúng ta cần sao lưu và xem xét một chút về cách tổ chức bộ nhớ cache. Hình thức đơn giản nhất của bộ đệm là ánh xạ trực tiếp. Điều này có nghĩa là một địa chỉ trong bộ nhớ chính chỉ có thể được lưu trữ ở một vị trí cụ thể trong bộ đệm. Nếu chúng ta đang sử dụng hai mục dữ liệu ánh xạ đến cùng một vị trí trong bộ đệm, thì nó hoạt động rất tệ - mỗi lần chúng ta sử dụng một mục dữ liệu, mục kia phải được xóa khỏi bộ đệm để nhường chỗ cho mục kia. Phần còn lại của bộ đệm có thể trống, nhưng các mục đó sẽ không sử dụng các phần khác của bộ đệm.

Để ngăn chặn điều này, hầu hết các bộ nhớ cache là cái được gọi là "tập hợp liên kết". Ví dụ: trong bộ đệm kết hợp bộ 4 chiều, mọi mục từ bộ nhớ chính có thể được lưu trữ tại bất kỳ 4 vị trí khác nhau trong bộ đệm. Vì vậy, khi bộ đệm sẽ tải một mục, nó sẽ tìm 3 mục được sử dụng gần đây nhất trong số bốn mục đó, xóa nó vào bộ nhớ chính và tải mục mới vào vị trí của nó.

Vấn đề có lẽ khá rõ ràng: đối với bộ đệm được ánh xạ trực tiếp, hai toán hạng xảy ra ánh xạ tới cùng một vị trí bộ đệm có thể dẫn đến hành vi xấu. Bộ đệm kết hợp bộ N-way tăng số lượng từ 2 lên N + 1. Việc tổ chức bộ đệm thành nhiều "cách" cần nhiều mạch hơn và thường chạy chậm hơn, vì vậy (ví dụ) bộ đệm kết hợp được đặt theo kiểu 8192 cũng hiếm khi là một giải pháp tốt.

Cuối cùng, yếu tố này khó kiểm soát hơn trong mã di động. Kiểm soát của bạn về nơi dữ liệu của bạn được đặt thường khá hạn chế. Tệ hơn, ánh xạ chính xác từ địa chỉ đến bộ đệm khác nhau giữa các bộ xử lý tương tự. Tuy nhiên, trong một số trường hợp, có thể đáng làm những việc như phân bổ bộ đệm lớn và sau đó chỉ sử dụng các phần của những gì bạn đã phân bổ để đảm bảo chống lại dữ liệu chia sẻ cùng một dòng bộ đệm (mặc dù bạn có thể cần phải phát hiện bộ xử lý chính xác và hành động phù hợp để làm điều này).

  • Chia sẻ sai

Có một mục khác, liên quan được gọi là "chia sẻ sai". Điều này phát sinh trong một hệ thống đa bộ xử lý hoặc đa lõi, trong đó hai (hoặc nhiều) bộ xử lý / lõi có dữ liệu riêng biệt, nhưng nằm trong cùng một dòng bộ đệm. Điều này buộc hai bộ xử lý / lõi phải phối hợp truy cập dữ liệu của chúng, mặc dù mỗi bộ có mục dữ liệu riêng biệt. Đặc biệt là nếu cả hai sửa đổi dữ liệu xen kẽ, điều này có thể dẫn đến sự chậm chạp lớn vì dữ liệu phải liên tục bị đảo lộn giữa các bộ xử lý. Điều này không thể dễ dàng được chữa khỏi bằng cách tổ chức bộ đệm thành nhiều "cách" hơn hoặc bất cứ điều gì tương tự. Cách chính để ngăn chặn điều đó là đảm bảo rằng hai luồng hiếm khi (tốt nhất là không bao giờ) sửa đổi dữ liệu có thể nằm trong cùng một dòng bộ đệm (có cùng cảnh báo về khó kiểm soát địa chỉ mà dữ liệu được phân bổ).


  1. Những người biết rõ về C ++ có thể tự hỏi liệu điều này có mở để tối ưu hóa thông qua một cái gì đó giống như các mẫu biểu thức hay không. Tôi khá chắc chắn câu trả lời là có, nó có thể được thực hiện và nếu có, nó có thể sẽ là một chiến thắng khá đáng kể. Tuy nhiên, tôi không biết bất kỳ ai đã làm như vậy, và cho biết ít valarraysử dụng như thế nào , tôi ít nhất sẽ hơi ngạc nhiên khi thấy bất cứ ai làm như vậy.

  2. Trong trường hợp bất kỳ ai tự hỏi làm thế nào valarray(được thiết kế đặc biệt cho hiệu năng) có thể sai đến mức này, thì có một điều: nó thực sự được thiết kế cho các máy như Crays cũ, sử dụng bộ nhớ chính nhanh và không có bộ đệm. Đối với họ, đây thực sự là một thiết kế gần như lý tưởng.

  3. Có, tôi đang đơn giản hóa: hầu hết các bộ nhớ cache không thực sự đo chính xác mục được sử dụng gần đây nhất, nhưng chúng sử dụng một số heuristic có ý định gần với nó mà không phải giữ dấu thời gian đầy đủ cho mỗi lần truy cập.


1
Tôi thích những thông tin bổ sung trong câu trả lời của bạn, đặc biệt là valarrayví dụ.
Marc Claesen

1
+1 Cuối cùng: một mô tả đơn giản về sự kết hợp tập hợp! EDIT hơn nữa: Đây là một trong những câu trả lời nhiều thông tin nhất về SO. Cảm ơn bạn.
Kỹ sư

32

Chào mừng bạn đến với thế giới của Thiết kế hướng dữ liệu. Câu thần chú cơ bản là Sắp xếp, Loại bỏ Chi nhánh, Batch, Loại bỏ virtualcác cuộc gọi - tất cả các bước hướng tới địa phương tốt hơn.

Vì bạn đã gắn thẻ câu hỏi với C ++, đây là C ++ Bullshit điển hình bắt buộc . Cạm bẫy của lập trình hướng đối tượng của Tony Albrecht cũng là một giới thiệu tuyệt vời về chủ đề này.


1
ý bạn là gì theo đợt, người ta có thể không hiểu.
0x90

5
Batching: thay vì thực hiện đơn vị công việc trên một đối tượng, hãy thực hiện nó trên một loạt các đối tượng.
arul

AKA chặn, chặn thanh ghi, chặn cache.
0x90

1
Chặn / Không chặn thường đề cập đến cách các đối tượng hành xử trong một môi trường đồng thời.
arul

2
đợt xử lý == vector hóa
Amro

23

Chỉ cần chồng chất lên: ví dụ kinh điển về mã không thân thiện với bộ đệm so với bộ đệm thân thiện với bộ đệm là "chặn bộ đệm" của ma trận nhân lên.

Ma trận ngây thơ nhân lên như sau:

for(i=0;i<N;i++) {
   for(j=0;j<N;j++) {
      dest[i][j] = 0;
      for( k==;k<N;i++) {
         dest[i][j] += src1[i][k] * src2[k][j];
      }
   }
}

Nếu Nlớn, ví dụ: nếu N * sizeof(elemType)lớn hơn kích thước bộ đệm, thì mỗi lần truy cập duy nhất src2[k][j]sẽ là một lỗi bộ đệm.

Có nhiều cách khác nhau để tối ưu hóa điều này cho bộ đệm. Đây là một ví dụ rất đơn giản: thay vì đọc một mục trên mỗi dòng bộ đệm trong vòng lặp bên trong, hãy sử dụng tất cả các mục:

int itemsPerCacheLine = CacheLineSize / sizeof(elemType);

for(i=0;i<N;i++) {
   for(j=0;j<N;j += itemsPerCacheLine ) {
      for(jj=0;jj<itemsPerCacheLine; jj+) {
         dest[i][j+jj] = 0;
      }
      for( k=0;k<N;k++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

Nếu kích thước dòng bộ đệm là 64 byte và chúng tôi đang hoạt động trên các phao 32 bit (4 byte), thì có 16 mục trên mỗi dòng bộ đệm. Và số lượng bộ nhớ cache bỏ qua chỉ bằng cách chuyển đổi đơn giản này đã giảm khoảng 16 lần.

Các phép biến đổi Fancier hoạt động trên các ô 2D, tối ưu hóa cho nhiều bộ đệm (L1, L2, TLB), v.v.

Một số kết quả của việc "chặn bộ nhớ cache" của Google:

http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf

http://software.intel.com/en-us/articles/cache-blocking-t kỹ thuật

Một hình ảnh động video đẹp của thuật toán chặn bộ nhớ cache được tối ưu hóa.

http://www.youtube.com/watch?v=IFWgwGMMrh0

Ốp lát liên quan rất chặt chẽ:

http://en.wikipedia.org/wiki/Loop_tiling


7
Những người đọc nó cũng có thể quan tâm đến bài viết của tôi về phép nhân ma trận trong đó tôi đã thử nghiệm thuật toán ikj "thân thiện với bộ đệm" và thuật toán ijk không thân thiện bằng cách nhân hai ma trận 2000x2000.
Martin Thoma

3
k==;Tôi đang hy vọng đây là một lỗi đánh máy?
TrebledJ

13

Bộ xử lý ngày nay làm việc với nhiều cấp độ vùng nhớ. Vì vậy, CPU sẽ có một loạt bộ nhớ nằm trên chính chip CPU. Nó có quyền truy cập rất nhanh vào bộ nhớ này. Có nhiều cấp độ bộ đệm khác nhau, mỗi lần truy cập chậm hơn (và lớn hơn) so với lần tiếp theo, cho đến khi bạn vào bộ nhớ hệ thống không có trên CPU và truy cập tương đối chậm hơn nhiều.

Theo logic, theo hướng dẫn của CPU, bạn chỉ cần tham khảo các địa chỉ bộ nhớ trong một không gian địa chỉ ảo khổng lồ. Khi bạn truy cập một địa chỉ bộ nhớ duy nhất, CPU sẽ tìm nạp nó. ngày xưa nó chỉ lấy địa chỉ duy nhất đó. Nhưng hôm nay CPU sẽ lấy một loạt bộ nhớ xung quanh bit bạn yêu cầu và sao chép nó vào bộ đệm. Nó giả định rằng nếu bạn yêu cầu một địa chỉ cụ thể rất có khả năng bạn sẽ sớm yêu cầu một địa chỉ gần đó. Ví dụ: nếu bạn đang sao chép bộ đệm, bạn sẽ đọc và ghi từ các địa chỉ liên tiếp - cái này nối tiếp cái kia.

Vì vậy, hôm nay khi bạn tìm nạp một địa chỉ, nó sẽ kiểm tra cấp độ bộ đệm đầu tiên để xem nó đã đọc địa chỉ đó vào bộ đệm chưa, nếu nó không tìm thấy nó, thì đây là một lỗi bộ nhớ cache và nó phải chuyển sang cấp độ tiếp theo bộ nhớ cache để tìm nó, cho đến khi cuối cùng nó phải đi vào bộ nhớ chính.

Mã thân thiện với bộ nhớ cache cố gắng giữ các truy cập gần nhau trong bộ nhớ để bạn giảm thiểu các lỗi bộ nhớ cache.

Vì vậy, một ví dụ sẽ là tưởng tượng bạn muốn sao chép một bảng 2 chiều khổng lồ. Nó được tổ chức với hàng tiếp cận liên tiếp trong bộ nhớ và một hàng tiếp theo ngay sau đó.

Nếu bạn đã sao chép các thành phần một hàng một lần từ trái sang phải - đó sẽ là bộ đệm thân thiện. Nếu bạn quyết định sao chép từng cột một bảng, bạn sẽ sao chép chính xác cùng một lượng bộ nhớ - nhưng đó sẽ là bộ đệm không thân thiện.


4

Cần phải làm rõ rằng không chỉ dữ liệu phải thân thiện với bộ đệm, nó cũng quan trọng đối với mã. Điều này ngoài việc dự đoán chi nhánh, sắp xếp lại hướng dẫn, tránh phân chia thực tế và các kỹ thuật khác.

Thông thường mã càng dày đặc, càng ít dòng bộ đệm sẽ được yêu cầu để lưu trữ nó. Điều này dẫn đến nhiều dòng bộ đệm có sẵn cho dữ liệu.

Mã không nên gọi các chức năng ở mọi nơi vì chúng thường sẽ yêu cầu một hoặc nhiều dòng bộ đệm của riêng chúng, dẫn đến ít dòng bộ đệm hơn cho dữ liệu.

Một chức năng nên bắt đầu tại một địa chỉ thân thiện với dòng bộ đệm. Mặc dù có các trình chuyển đổi trình biên dịch (gcc) cho điều này, hãy lưu ý rằng nếu các chức năng rất ngắn thì có thể gây lãng phí cho mỗi người khi chiếm toàn bộ một dòng bộ đệm. Ví dụ: nếu ba trong số các hàm được sử dụng thường xuyên nhất nằm trong một dòng bộ đệm 64 byte, thì điều này sẽ ít lãng phí hơn nếu mỗi hàm có một dòng riêng và dẫn đến hai dòng bộ đệm ít có sẵn cho việc sử dụng khác. Giá trị căn chỉnh điển hình có thể là 32 hoặc 16.

Vì vậy, dành một số thời gian thêm để làm cho mã dày đặc. Kiểm tra các cấu trúc khác nhau, biên dịch và xem xét kích thước mã và hồ sơ được tạo.


2

Như @Marc Claesen đã đề cập rằng một trong những cách để viết mã thân thiện với bộ đệm là khai thác cấu trúc mà dữ liệu của chúng tôi được lưu trữ. Ngoài ra, một cách khác để viết mã thân thiện với bộ đệm là: thay đổi cách lưu trữ dữ liệu của chúng tôi; sau đó viết mã mới để truy cập dữ liệu được lưu trữ trong cấu trúc mới này.

Điều này có ý nghĩa trong trường hợp các hệ thống cơ sở dữ liệu tuyến tính hóa các bộ dữ liệu của một bảng và lưu trữ chúng. Có hai cách cơ bản để lưu trữ các bộ dữ liệu của bảng là lưu trữ hàng và lưu trữ cột. Trong cửa hàng hàng như tên cho thấy các bộ dữ liệu được lưu trữ hàng khôn ngoan. Giả sử một bảng có tên Productđang được lưu trữ có 3 thuộc tính tức là int32_t key, char name[56]int32_t pricedo đó, tổng kích thước của một tuple là 64byte.

Chúng ta có thể mô phỏng một thực thi truy vấn lưu trữ hàng rất cơ bản trong bộ nhớ chính bằng cách tạo một mảng các Productcấu trúc có kích thước N, trong đó N là số lượng hàng trong bảng. Bố trí bộ nhớ như vậy cũng được gọi là mảng cấu trúc. Vì vậy, cấu trúc cho Sản phẩm có thể như sau:

struct Product
{
   int32_t key;
   char name[56];
   int32_t price'
}

/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */

Tương tự, chúng ta có thể mô phỏng một thực thi truy vấn lưu trữ cột rất cơ bản trong bộ nhớ chính bằng cách tạo 3 mảng có kích thước N, một mảng cho mỗi thuộc tính của Productbảng. Bố trí bộ nhớ như vậy cũng được gọi là cấu trúc của mảng. Vì vậy, 3 mảng cho mỗi thuộc tính của Sản phẩm có thể như sau:

/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */

Bây giờ sau khi tải cả mảng cấu trúc (Bố cục hàng) và 3 mảng riêng biệt (Bố cục cột), chúng tôi có lưu trữ hàng và lưu trữ cột trên bảng Productcó trong bộ nhớ của chúng tôi.

Bây giờ chúng ta chuyển sang phần mã thân thiện với bộ đệm. Giả sử khối lượng công việc trên bảng của chúng ta sao cho chúng ta có một truy vấn tổng hợp trên thuộc tính price. Nhu la

SELECT SUM(price)
FROM PRODUCT

Đối với cửa hàng hàng, chúng tôi có thể chuyển đổi truy vấn SQL ở trên thành

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + table[i].price;

Đối với lưu trữ cột, chúng ta có thể chuyển đổi truy vấn SQL ở trên thành

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + price[i];

Mã cho kho lưu trữ cột sẽ nhanh hơn mã cho bố cục hàng trong truy vấn này vì nó chỉ yêu cầu một tập hợp các thuộc tính và trong bố cục cột, chúng tôi đang làm điều đó tức là chỉ truy cập vào cột giá.

Giả sử rằng kích thước dòng bộ đệm là 64byte.

Trong trường hợp bố trí hàng khi đọc một dòng bộ đệm, giá trị giá chỉ 1 ( cacheline_size/product_struct_size = 64/64 = 1) tuple được đọc, bởi vì kích thước cấu trúc của chúng tôi là 64 byte và nó lấp đầy toàn bộ dòng bộ đệm của chúng tôi, vì vậy trong mỗi trường hợp, một lỗi bộ nhớ cache xảy ra trong trường hợp của một bố trí hàng.

Trong trường hợp bố trí cột khi đọc một dòng bộ đệm, giá trị của các bộ dữ liệu 16 ( cacheline_size/price_int_size = 64/4 = 16) được đọc, bởi vì 16 giá trị giá liền kề được lưu trữ trong bộ nhớ được đưa vào bộ đệm, do đó, cứ sau mười sáu bộ đệm sẽ bỏ qua bộ đệm trong trường hợp bố trí cột.

Vì vậy, bố cục cột sẽ nhanh hơn trong trường hợp truy vấn đã cho và nhanh hơn trong các truy vấn tổng hợp như vậy trên một tập hợp con các cột của bảng. Bạn có thể tự mình thử nghiệm như vậy bằng cách sử dụng dữ liệu từ điểm chuẩn TPC-H và so sánh thời gian chạy cho cả hai bố cục. Các wikipedia bài viết về hệ thống cơ sở dữ liệu cột định hướng cũng tốt.

Vì vậy, trong các hệ thống cơ sở dữ liệu, nếu biết trước khối lượng công việc truy vấn, chúng ta có thể lưu trữ dữ liệu của mình theo bố cục phù hợp với các truy vấn trong khối lượng công việc và truy cập dữ liệu từ các bố cục này. Trong trường hợp ví dụ trên, chúng tôi đã tạo một bố cục cột và thay đổi mã của chúng tôi để tính tổng để nó trở nên thân thiện với bộ đệm.


1

Xin lưu ý rằng bộ nhớ cache không chỉ lưu trữ bộ nhớ liên tục. Chúng có nhiều dòng (ít nhất là 4) vì vậy bộ nhớ gián đoạn và chồng chéo thường có thể được lưu trữ một cách hiệu quả.

Những gì còn thiếu trong tất cả các ví dụ trên là điểm chuẩn được đo. Có nhiều huyền thoại về hiệu suất. Trừ khi bạn đo nó bạn không biết. Không làm phức tạp mã của bạn trừ khi bạn có một cải tiến được đo .

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.