Tại sao có hiệu suất lớn đạt được trong phép nhân mảng 2048x2048 so với 2047x2047?


127

Tôi đang thực hiện một số điểm chuẩn nhân ma trận, như đã đề cập trước đây trong Tại sao MATLAB lại nhân nhanh như vậy?

Bây giờ tôi đã có một vấn đề khác, khi nhân hai ma trận 2048x2048, có một sự khác biệt lớn giữa C # và các ma trận khác. Khi tôi thử nhân các ma trận chỉ 2047x2047, điều đó có vẻ bình thường. Thêm một số người khác cho comparsion quá.

1024x1024 - 10 giây.

1027x1027 - 10 giây.

2047x2047 - 90 giây.

2048x2048 - 300 giây.

2049x2049 - 91 giây. (cập nhật)

2500x2500 - 166 giây

Đó là chênh lệch ba phút rưỡi cho trường hợp 2k x 2k.

sử dụng mảng 2dim

//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];

//Main multiply code
for(int j = 0; j < rozmer; j++)
{
   for (int k = 0; k < rozmer; k++)
   {
     float temp = 0;
     for (int m = 0; m < rozmer; m++)
     {
       temp = temp + matice1[j,m] * matice2[m,k];
     }
     matice3[j, k] = temp;
   }
 }

23
Đây sẽ là một câu hỏi thi tuyệt vời cho lớp lập trình C cấp cao hoặc lớp Thiết kế hệ điều hành ;-)
Dana the Sane

Bạn đã thử kiểm tra cả mảng đa chiều [,] và răng cưa [] [] cũng như 32 và 64 bit chưa? Tôi chỉ thử nghiệm một vài lần nhưng răng cưa có vẻ phù hợp hơn với kết quả của bạn nhưng răng cưa 64 bit rất cao, tôi không biết liệu có bất kỳ phương pháp phỏng đoán nào trong tình huống áp dụng cho tình huống này hay không nếu bộ nhớ cache của nó có liên quan như đề xuất trước đây. Nếu bạn muốn có một giải pháp GPGPU có research.microsoft.com/en-us/projects/accelerator mà nên có tính cạnh tranh với thời gian trong bài viết khác của bạn.
Kris

Một số câu hỏi ngây thơ, nhưng có bao nhiêu ops (thêm / nhân) có liên quan đến việc nhân hai ma trận vuông?
Nick T

vấn đề tương tự ở đây stackoverflow.com/questions/12264970/
Thẻ

Câu trả lời:


61

Điều này có thể có liên quan đến xung đột trong bộ đệm L2 của bạn.

Lỗi bộ nhớ cache trên matice1 không phải là vấn đề vì chúng được truy cập tuần tự. Tuy nhiên, đối với matice2 nếu một cột đầy đủ phù hợp với L2 (nghĩa là khi bạn truy cập matice2 [0, 0], matice2 [1, 0], matice2 [2, 0] ... vv, không có gì bị đuổi) nhớ cache với matice2.

Bây giờ để đi sâu hơn về cách thức hoạt động của bộ nhớ cache, nếu địa chỉ byte của biến của bạn là X, hơn dòng bộ đệm cho nó sẽ là (X >> 6) & (L - 1). Trong đó L là tổng số dòng bộ đệm trong bộ đệm của bạn. L luôn có sức mạnh bằng 2. Sáu xuất phát từ thực tế rằng 2 ^ 6 == 64 byte là kích thước chuẩn của dòng bộ đệm.

Bây giờ điều này có nghĩa là gì? Chà, điều đó có nghĩa là nếu tôi có địa chỉ X và địa chỉ Y và (X >> 6) - (Y >> 6) chia hết cho L (tức là một số công suất lớn bằng 2), chúng sẽ được lưu trong cùng một dòng.

Bây giờ để trở lại vấn đề của bạn, sự khác biệt giữa năm 2048 và 2049 là gì,

khi 2048 là kích thước của bạn:

nếu bạn lấy & matice2 [x, k] và & matice2 [y, k] thì sự khác biệt (& matice2 [x, k] >> 6) - (& matice2 [y, k] >> 6) sẽ chia hết cho 2048 * 4 (kích thước của phao). Vì vậy, một sức mạnh lớn của 2.

Do đó, tùy thuộc vào kích thước L2 của bạn, bạn sẽ có rất nhiều xung đột dòng bộ đệm và chỉ sử dụng một phần nhỏ L2 để lưu trữ một cột, do đó bạn thực sự không thể lưu trữ cột đầy đủ trong bộ đệm của mình, do đó bạn sẽ có hiệu suất kém .

Khi kích thước là 2049, thì sự khác biệt là 2049 * 4 không phải là sức mạnh của 2 do đó bạn sẽ có ít xung đột hơn và cột của bạn sẽ phù hợp với bộ đệm của bạn một cách an toàn.

Bây giờ để kiểm tra lý thuyết này, có một số điều bạn có thể làm:

Phân bổ mảng matice2 của bạn như matice2 [razmor, 4096] và chạy với razmor = 1024, 1025 hoặc bất kỳ kích thước nào, và bạn sẽ thấy hiệu suất rất kém so với những gì bạn đã có trước đây. Điều này là do bạn mạnh mẽ sắp xếp tất cả các cột để xung đột với nhau.

Sau đó thử matice2 [razmor, 4097] và chạy nó với bất kỳ kích thước nào và bạn sẽ thấy hiệu suất tốt hơn nhiều.


Bạn đã phạm sai lầm trong 2 đoạn cuối của bạn? Cả hai trys đều giống hệt nhau. :)
Xèo

Cache associativity cũng đóng một vai trò.
Ben Jackson

20

Có lẽ là một hiệu ứng bộ nhớ đệm. Với kích thước ma trận có sức mạnh lớn bằng hai và kích thước bộ đệm cũng là sức mạnh của hai, bạn chỉ có thể sử dụng một phần nhỏ của bộ đệm L1, làm chậm mọi thứ. Phép nhân ma trận ngây thơ thường bị hạn chế bởi nhu cầu tìm nạp dữ liệu vào bộ đệm. Các thuật toán được tối ưu hóa bằng cách sử dụng ốp lát (hoặc thuật toán quên bộ nhớ cache) tập trung vào việc sử dụng bộ đệm L1 tốt hơn.

Nếu bạn tính thời gian cho các cặp khác (2 ^ n-1,2 ^ n) tôi hy vọng bạn sẽ thấy các hiệu ứng tương tự.

Để giải thích đầy đủ hơn, trong vòng lặp bên trong, nơi bạn truy cập matice2 [m, k], có khả năng matice2 [m, k] và matice2 [m + 1, k] được bù trừ cho nhau bằng 2048 * sizeof (float) và do đó ánh xạ tới cùng một chỉ mục trong bộ đệm L1. Với bộ đệm kết hợp N-way, thông thường bạn sẽ có 1-8 vị trí bộ đệm cho tất cả các vị trí này. Do đó, hầu hết tất cả các truy cập đó sẽ kích hoạt việc xóa bộ đệm L1 và tìm nạp dữ liệu từ bộ đệm chính hoặc bộ nhớ chính chậm hơn.


+1. Âm thanh có khả năng. Người ta phải cẩn thận với sự kết hợp bộ nhớ cache.
Macke

16

Điều này có thể phải làm với kích thước của bộ đệm cpu của bạn. Nếu 2 hàng của ma trận ma trận không khớp, thì bạn sẽ mất thời gian hoán đổi các phần tử từ RAM. Các yếu tố thêm 4095 có thể chỉ đủ để ngăn hàng khớp.

Trong trường hợp của bạn, 2 hàng cho ma trận 2047 2d nằm trong 16KB bộ nhớ (giả sử các loại 32 bit). Ví dụ: nếu bạn có bộ đệm L1 (gần cpu nhất trên bus) là 64KB, thì bạn có thể đặt ít nhất 4 hàng (2047 * 32) vào bộ đệm cùng một lúc. Với các hàng dài hơn nếu có bất kỳ phần đệm nào được yêu cầu đẩy các cặp hàng vượt quá 16KB, thì mọi thứ bắt đầu trở nên lộn xộn. Ngoài ra, mỗi lần bạn 'bỏ lỡ' bộ đệm, hoán đổi dữ liệu từ bộ đệm khác hoặc bộ nhớ chính sẽ làm chậm mọi thứ.

Tôi đoán là phương sai trong thời gian chạy mà bạn nhìn thấy với các ma trận có kích thước khác nhau bị ảnh hưởng bởi hiệu quả của hệ điều hành có thể sử dụng bộ đệm có sẵn (và một số kết hợp chỉ có vấn đề). Tất nhiên đây là một sự đơn giản hóa toàn bộ về phía tôi.


2
nhưng rất có thể anh ta không có 16,7 MB bộ nhớ cache CPU
Marino imić

Tôi đã cập nhật kết quả với 2049x2049 - 91 giây. Nếu đó là "sự cố bộ đệm", thì đây có phải vẫn là 300+ không?
Sói

@Marino câu trả lời đã được cập nhật để đưa nó vào tài khoản.
Dana the Sane

1
Tôi cảm thấy như không có giải thích nào trong số những giải thích này có thể giải quyết thỏa đáng các chi tiết mới liên quan đến các kích cỡ khác nhau và thưa thớt gây ra vấn đề, với những người khác ở giữa không bị ảnh hưởng.
Ken Rockot

2
Tôi không nghĩ rằng lời giải thích này là chính xác. Vấn đề nằm ở việc không sử dụng hết dung lượng bộ nhớ cache do xung đột dòng bộ đệm khi kích thước là sức mạnh 2. Ngoài ra, hệ điều hành thực sự không liên quan gì đến bộ nhớ cache, vì đó không phải là hệ điều hành quyết định bộ đệm và những gì cần phải xóa, tất cả chỉ là trong phần cứng. Hệ điều hành có liên quan đến việc căn chỉnh dữ liệu, nhưng trong trường hợp này là tất cả về cách C # quyết định phân bổ dữ liệu và cách biểu diễn mảng 2D trong bộ nhớ, HĐH không liên quan gì đến nó.
zviadm


5

Cho rằng thời gian đang giảm ở kích thước lớn hơn sẽ không có khả năng xảy ra xung đột bộ đệm, đặc biệt là với quyền hạn 2 cho kích thước ma trận có vấn đề? Tôi không phải là chuyên gia về các vấn đề bộ đệm, nhưng thông tin tuyệt vời về các vấn đề hiệu suất liên quan đến bộ đệm ở đây .


Phần 5 của liên kết về tính kết hợp bộ đệm dường như được áp dụng cụ thể.
Dana the Sane

4

Khi bạn đang truy cập vào matice2mảng theo chiều dọc, nó sẽ được hoán đổi trong và ngoài bộ đệm hơn rất nhiều. Nếu bạn phản chiếu mảng theo đường chéo, để bạn có thể truy cập nó bằng cách sử dụng [k,m]thay vì [m,k], mã sẽ chạy nhanh hơn rất nhiều.

Tôi đã thử nghiệm điều này cho ma trận 1024x1024, và nó nhanh gấp khoảng hai lần. Đối với ma trận 2048x2048, nó nhanh hơn khoảng mười lần.


Điều này không giải thích tại sao năm 2049 nhanh hơn năm 2048.
Macke

@Macke: Đó là bởi vì nó vượt qua một số giới hạn trong bộ nhớ đệm, do đó có rất nhiều lỗi bộ nhớ cache.
Guffa

Tại sao các downvote? Nếu bạn không nói những gì bạn nghĩ là sai, nó không thể cải thiện câu trả lời.
Guffa

Một downvote khác mà không có bất kỳ lời giải thích nào ... Có phải câu trả lời của tôi có quá ít "có lẽ", "đoán" và "nên" trong đó, giống như các câu trả lời nhận được nhiều sự ủng hộ nhất ...?
Guffa

4

Bí danh bộ nhớ cache

Hoặc đập bộ nhớ cache , nếu tôi có thể đặt một thuật ngữ.

Bộ nhớ cache hoạt động bằng cách lập chỉ mục với các bit thứ tự thấp và gắn thẻ với các bit thứ tự cao.

Hình ảnh rằng bộ đệm của bạn có 4 từ và ma trận của bạn là 4 x 4. Khi một cột được truy cập và hàng có sức mạnh hai chiều dài, thì mỗi phần tử cột trong bộ nhớ sẽ ánh xạ tới cùng một phần tử bộ đệm.

Một sức mạnh của hai cộng một thực sự là tối ưu cho vấn đề này. Mỗi thành phần cột mới sẽ ánh xạ tới vị trí bộ đệm tiếp theo chính xác như thể truy cập theo hàng.

Trong cuộc sống thực, một thẻ bao gồm nhiều địa chỉ tăng liên tục sẽ lưu trữ một số thành phần liền kề liên tiếp. Bằng cách bù vào nhóm mà mỗi hàng mới ánh xạ tới, đi qua cột không thay thế mục trước đó. Khi cột tiếp theo được duyệt qua, toàn bộ bộ đệm sẽ được lấp đầy với các hàng khác nhau và mỗi phần hàng phù hợp với bộ đệm sẽ được nhấn cho một số cột.

Vì bộ nhớ cache nhanh hơn rất nhiều so với DRAM (chủ yếu là nhờ vào chip) nên mọi thứ đều ổn.


2

Bạn dường như đã đạt đến giới hạn kích thước bộ đệm, hoặc có thể có một số vấn đề về độ lặp lại trong thời gian của bạn.

Dù vấn đề là gì, bạn chỉ đơn giản là không nên tự mình nhân ma trận trong C # và thay vào đó hãy sử dụng phiên bản BLAS được tối ưu hóa. Kích thước ma trận đó nên được nhân lên dưới một giây trên bất kỳ máy hiện đại nào.


1
Tôi biết về BLAS, nhưng nhiệm vụ không phải là làm cho nó nhanh nhất có thể, mà là viết và kiểm tra nó bằng nhiều ngôn ngữ khác nhau. Đây là một vấn đề rất lạ đối với tôi và Iam thực sự tò mò tại sao kết quả lại giống như vậy.
Sói

3
@Wolf Tôi cảm thấy khó có thể phấn khích về việc thứ gì đó nên mất một giây là mất 90 giây hay 300 giây.
David Heffernan

4
Cách tốt nhất để tìm hiểu làm thế nào một cái gì đó hoạt động là tự viết nó và xem cách bạn có thể cải thiện việc thực hiện của bạn; đây là (hy vọng) những gì Wolf đang làm.
Callum Rogers

@Callum Rogers, đồng ý. Đó là cách tôi học được tầm quan trọng của kích thước bộ đệm trong các hoạt động sao chép tệp.
Kelly S. Pháp

1

Sử dụng hiệu quả hệ thống phân cấp bộ đệm là rất quan trọng. Bạn cần đảm bảo rằng các mảng nhiều chiều có dữ liệu được sắp xếp tốt, có thể được thực hiện bằng cách ốp lát . Để làm điều này, bạn sẽ cần lưu trữ mảng 2D dưới dạng mảng 1D cùng với cơ chế lập chỉ mục. Vấn đề với phương pháp truyền thống là mặc dù hai phần tử mảng liền kề nằm trong cùng một hàng nằm cạnh nhau trong bộ nhớ, hai phần tử liền kề trong cùng một cột sẽ được phân tách bằng các phần tử W trong bộ nhớ, trong đó W là số cột . Ốp lát có thể tạo ra sự khác biệt về hiệu suất của hệ số mười.


Hmm - nhưng một mảng được khai báo là 2D (float [,] matice = new float [rozmer, rozmer];) chỉ được phân bổ trong RAM dưới dạng mảng một chiều và tính toán hàng / sải chân được thực hiện dưới mui xe. Vậy tại sao việc khai báo nó là 1D và thực hiện các phép tính hàng / sải tay thủ công sẽ nhanh hơn? Bạn có nghĩa là sol'n được phân bổ một mảng lớn dưới dạng các mảng nhỏ hơn mà mỗi ô có thể vừa với bộ đệm trong đó mảng lớn sẽ không?
Eric M

1
Nếu thư viện của bạn hoặc bất kỳ công cụ nào bạn đang sử dụng không ốp lát, thì bạn không cần phải làm vậy. Nhưng nếu bạn sử dụng một mảng 2D truyền thống, giả sử C / C ++, thì ốp lát sẽ cải thiện hiệu suất.
Arlen

0

Tôi nghi ngờ đó là kết quả của một thứ gọi là " Lũ lụt tuần tự ". Điều này là bạn đang cố gắng lặp qua danh sách các đối tượng lớn hơn một chút so với kích thước bộ đệm, do đó, mọi yêu cầu đối với danh sách (mảng) phải được thực hiện từ ram và bạn sẽ không nhận được một bộ đệm. đánh.

Trong trường hợp của bạn, bạn đang lặp qua các mảng 2048 chỉ mục 2048 lần, nhưng bạn chỉ có không gian cho 2047 (có thể do một số chi phí từ cấu trúc mảng), vì vậy mỗi lần bạn tích lũy một mảng pos, nó cần có được mảng pos từ ram. Sau đó, nó được lưu trữ trong bộ đệm, nhưng ngay trước khi nó được sử dụng lại, nó sẽ bị hủy. Vì vậy, bộ nhớ cache về cơ bản là vô dụng, dẫn đến thời gian thực hiện lâu hơn nhiều.


1
Sai. 2049 nhanh hơn 2048, từ chối yêu cầu của bạn.
Macke

@Macke: Điều đó hoàn toàn có thể. Nhưng có một khả năng nhỏ là chính sách bộ đệm được sử dụng trong bộ xử lý của anh ta vẫn có thể thực hiện việc này. Nó không có khả năng lắm, nhưng nó không phải là không thể tưởng tượng được.
Automatico
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.