Phép nhân ma trận: Sự khác biệt nhỏ về kích thước ma trận, sự khác biệt lớn về thời gian


77

Tôi có một mã nhân ma trận trông giống như sau:

Ở đây, kích thước của ma trận được biểu diễn bằng dimension. Bây giờ, nếu kích thước của ma trận là 2000, thì mất 147 giây để chạy đoạn mã này, trong khi nếu kích thước của ma trận là 2048 thì mất 447 giây. Vì vậy, trong khi sự khác biệt trong không. của phép nhân là (2048 * 2048 * 2048) / (2000 * 2000 * 2000) = 1,073, sự khác biệt trong thời gian là 447/147 = 3. Ai đó có thể giải thích tại sao điều này xảy ra? Tôi mong đợi nó sẽ mở rộng tuyến tính, điều này không xảy ra. Tôi không cố tạo mã nhân ma trận nhanh nhất, chỉ đơn giản là cố gắng hiểu tại sao nó xảy ra.

Thông số kỹ thuật: Nút lõi kép AMD Opteron (2.2GHz), RAM 2G, gcc v 4.5.0

Chương trình được biên dịch thành gcc -O3 simple.c

Tôi cũng đã chạy điều này trên trình biên dịch icc của Intel và thấy kết quả tương tự.

BIÊN TẬP:

Như đã đề xuất trong phần nhận xét / câu trả lời, tôi đã chạy mã với thứ nguyên = 2060 và mất 145 giây.

Đây là chương trình hoàn chỉnh:


9
Có lẽ chìa khóa để bạn hiểu là phép nhân ma trận không chia tỷ lệ tuyến tính, mã của bạn theo thứ tự O(n^3).
brc

6
Có thể liên quan đến bộ nhớ đệm, xem xét sức mạnh của hai-ness của năm 2048?
Christian Rau

12
@brc Tôi không biết điều này có liên quan như thế nào đến vấn đề của anh ấy. Anh ấy hoàn toàn nhận thức được độ phức tạp của thuật toán của mình. Bạn thậm chí đã đọc câu hỏi?
Christian Rau

3
Hãy thử kiểm tra với ví dụ: kích thước = 2060 - điều này sẽ cho bạn biết liệu sự cố có liên quan đến kích thước bộ nhớ cache, ví dụ: hoặc liệu đó có phải là sự cố siêu căn chỉnh chẳng hạn như chèn bộ nhớ cache hoặc chèn TLB hay không.
Paul R

2
Lưu ý rằng việc hoán vị một trong các ma trận (có thể được thực hiện tại chỗ) sẽ dẫn đến kết quả tốt hơn cho các kích thước điển hình này (điểm hòa vốn có thể thay đổi). Thật vậy, chuyển vị là phép nhân O (n ^ 2) (so với O (n ^ 3)) và bộ nhớ được truy cập tuần tự cho cả hai ma trận, dẫn đến việc sử dụng bộ đệm tốt hơn.
Alexandre C.

Câu trả lời:


84

Đây là dự đoán hoang dã của tôi: bộ nhớ cache

Có thể là bạn có thể lắp 2 hàng 2000 doublegiây vào bộ nhớ đệm. Ít hơn rất nhiều so với bộ nhớ cache 32kb L1. (trong khi rời khỏi phòng những thứ cần thiết khác)

Nhưng khi bạn tăng nó lên 2048, nó sử dụng toàn bộ bộ nhớ cache (và bạn tràn một số vì bạn cần chỗ cho những thứ khác)

Giả sử chính sách bộ nhớ cache là LRU, việc tràn bộ nhớ cache chỉ một chút nhỏ thôi cũng sẽ khiến toàn bộ hàng bị xóa liên tục và được tải lại vào bộ nhớ đệm L1.

Khả năng khác là liên kết bộ nhớ cache do sức mạnh của hai. Mặc dù tôi nghĩ rằng bộ xử lý đó là kết hợp L1 2 chiều nên tôi không nghĩ nó quan trọng trong trường hợp này. (nhưng dù sao tôi cũng sẽ ném ý tưởng ra khỏi đó)

Giải thích có thể có 2: Bộ đệm xung đột bị bỏ lỡ do căn chỉnh siêu trên bộ đệm L2.

BMảng của bạn đang được lặp lại trên cột. Vì vậy, truy cập được xếp hạng. Tổng kích thước dữ liệu của bạn 2k x 2klà khoảng 32 MB trên mỗi ma trận. Nó lớn hơn nhiều so với bộ nhớ cache L2 của bạn.

Khi dữ liệu không được căn chỉnh hoàn hảo, bạn sẽ có vị trí không gian phù hợp trên B. Mặc dù bạn đang nhảy các hàng và chỉ sử dụng một phần tử trên mỗi dòng bộ nhớ cache, dòng bộ đệm vẫn nằm trong bộ đệm L2 sẽ được sử dụng lại trong lần lặp tiếp theo của vòng lặp giữa.

Tuy nhiên, khi dữ liệu được căn chỉnh hoàn hảo (2048), tất cả các bước nhảy này sẽ hạ cánh trên cùng một "cách bộ nhớ cache" và sẽ vượt xa khả năng kết hợp bộ nhớ cache L2 của bạn. Do đó, các dòng bộ nhớ cache được truy cập của Bsẽ không ở trong bộ nhớ cache cho lần lặp tiếp theo. Thay vào đó, chúng sẽ cần được kéo theo mọi cách từ ram.


3
Tôi đồng ý trong việc nghi ngờ bộ nhớ cache. Bạn có thể thực hiện một tập hợp các thử nghiệm và vẽ biểu đồ thời gian chạy so với thứ nguyên. Nếu đó là bộ nhớ cache, bạn sẽ thấy độ tuyến tính trong vùng lân cận có kích thước tương tự, với một số điểm đứt gãy rõ nét nơi bạn đạt được một bước lớn và thay đổi về độ dốc tuyến tính.
TJD

2
Không chỉ kích thước bộ nhớ cache - khi các ma trận được căn chỉnh siêu thẳng hàng như trong trường hợp năm 2048 thì bạn có thể bắt đầu thấy các vấn đề với việc xóa bộ nhớ cache, xóa TLB, v.v. Hãy thử nó với ví dụ 2060 và xem điều gì sẽ xảy ra ...
Paul R

Tôi chạy nó với thứ nguyên = 2060 và mất 145 giây. Nhìn vào giải thích 2, điều này cũng nên tính cục bộ không gian kém. Đối với kích thước> = 2048, các dòng bộ nhớ cache của B sẽ cần được tìm nạp từ RAM, phải không?
jitihsk

2
@AhmedMasud Và tôi cũng không nghĩ việc sử dụng sẽ timesgiải thích được vấn đề của anh ấy.
Christian Rau

4
Do cách thức hoạt động của bộ nhớ đệm, bộ đệm N-way chỉ có thể chứa tối đa N dòng bộ đệm với cùng một mô-đun địa chỉ với hàm lượng lớn là hai. (Tôi không biết con số chính xác trừ khi bạn cho tôi biết bạn có kiểu bộ xử lý nào.) Khi N = 2048, các đường bộ nhớ đệm được truy cập bởi btất cả đều có địa chỉ với cùng một mô-đun trên lũy thừa của hai. Vì vậy, họ sẽ xung đột. (Google: "Conflict Cache Miss")
Mysticial

34

Bạn chắc chắn đang nhận được những gì tôi gọi là cộng hưởng bộ nhớ cache . Điều này tương tự với răng cưa , nhưng không hoàn toàn giống nhau. Hãy để tôi giải thích.

Bộ nhớ đệm là cấu trúc dữ liệu phần cứng trích xuất một phần của địa chỉ và sử dụng nó làm chỉ mục trong bảng, không giống như một mảng trong phần mềm. (Trên thực tế, chúng tôi gọi chúng là mảng trong phần cứng.) Mảng bộ nhớ cache chứa các dòng dữ liệu và thẻ trong bộ nhớ cache - đôi khi một mục nhập như vậy cho mỗi chỉ mục trong mảng (được ánh xạ trực tiếp), đôi khi là một số như vậy (liên kết tập N-way). Phần thứ hai của địa chỉ được trích xuất và so sánh với thẻ được lưu trữ trong mảng. Cùng với nhau, chỉ mục và thẻ xác định duy nhất một địa chỉ bộ nhớ dòng bộ nhớ cache. Cuối cùng, phần còn lại của các bit địa chỉ xác định các byte nào trong dòng bộ đệm được địa chỉ hóa, cùng với kích thước của quyền truy cập.

Thông thường chỉ mục và thẻ là các trường bit đơn giản. Vì vậy, một địa chỉ bộ nhớ trông giống như

(Đôi khi chỉ mục và thẻ là các hàm băm, ví dụ như một vài XOR của các bit khác vào các bit tầm trung là chỉ mục. Hiếm khi hơn, đôi khi là chỉ mục và hiếm hơn là thẻ, là những thứ như lấy mô-đun địa chỉ dòng bộ nhớ cache a số nguyên tố. Các phép tính chỉ số phức tạp hơn này là nỗ lực để chống lại vấn đề cộng hưởng, mà tôi giải thích ở đây. Tất cả đều phải chịu một số dạng cộng hưởng, nhưng các sơ đồ trích xuất trường bit đơn giản nhất bị cộng hưởng trên các mẫu truy cập phổ biến, như bạn đã tìm thấy.)

Vì vậy, các giá trị điển hình ... có nhiều mô hình khác nhau của "Opteron Dual Core", và tôi không thấy bất kỳ điều gì ở đây chỉ định bạn có cái nào. Chọn một cách ngẫu nhiên, hướng dẫn sử dụng gần đây nhất mà tôi thấy trên trang web của AMD, Hướng dẫn của nhà phát triển Bios và Kernel (BKDG) cho các kiểu máy AMD Family 15h 00h-0Fh , ngày 12 tháng 3 năm 2012.

(Family 15h = Bulldozer family, bộ vi xử lý cao cấp gần đây nhất - BKDG đề cập đến lõi kép, mặc dù tôi không biết số sản phẩm chính xác như những gì bạn mô tả. Nhưng dù sao, ý tưởng cộng hưởng giống nhau áp dụng cho tất cả các bộ xử lý, chỉ là các tham số như kích thước bộ nhớ cache và tính liên kết có thể thay đổi một chút.)

Từ tr.33:

Bộ xử lý AMD Family 15h chứa bộ nhớ đệm dữ liệu L1 dự đoán 16-Kbyte, 4 chiều với hai cổng 128 bit. Đây là bộ nhớ đệm ghi qua hỗ trợ lên đến hai lần tải 128 Byte mỗi chu kỳ. Nó được chia thành 16 ngân hàng, mỗi ngân hàng rộng 16 byte. [...] Chỉ có thể thực hiện một lần tải từ một ngân hàng nhất định của bộ đệm L1 trong một chu kỳ duy nhất.

Tóm lại:

  • Dòng bộ đệm 64 byte => 6 bit bù trong dòng bộ đệm

  • 16KB / 4 chiều => cộng hưởng là 4KB.

    Tức là các bit địa chỉ 0-5 là độ lệch dòng bộ nhớ cache.

  • 16KB / 64B dòng bộ đệm => 2 ^ 14/2 ^ 6 = 2 ^ 8 = 256 dòng bộ đệm trong bộ đệm.
    (Sửa lỗi: Ban đầu tôi đã tính sai giá trị này là 128. rằng tôi đã sửa tất cả các phần phụ thuộc.)

  • 4 cách kết hợp => 256/4 = 64 chỉ mục trong mảng bộ nhớ cache. Tôi (Intel) gọi đây là "bộ".

    tức là bạn có thể coi bộ đệm là một mảng gồm 32 mục nhập hoặc tập hợp, mỗi mục nhập chứa 4 dòng bộ nhớ cache quảng cáo thẻ của chúng. (Nó phức tạp hơn thế này, nhưng không sao).

(Nhân tiện, các thuật ngữ "set" và "way" có các định nghĩa khác nhau .)

  • có 6 bit chỉ số, bit 6-11 trong sơ đồ đơn giản nhất.

    Điều này có nghĩa là bất kỳ dòng bộ đệm nào có cùng giá trị trong các bit chỉ mục, bit 6-11, sẽ ánh xạ đến cùng một bộ bộ đệm.

Bây giờ hãy nhìn vào chương trình của bạn.

Vòng lặp k là vòng lặp trong cùng. Loại cơ sở là gấp đôi, 8 byte. Nếu thứ nguyên = 2048, tức là 2K, thì các phần tử liên tiếp của B[dimension*k+j]vòng lặp được truy cập sẽ cách nhau 2048 * 8 = 16K byte. Tất cả chúng sẽ ánh xạ đến cùng một tập hợp của bộ đệm L1 - tất cả chúng sẽ có cùng một chỉ mục trong bộ đệm. Điều đó có nghĩa là, thay vì có 256 dòng bộ nhớ cache trong bộ nhớ cache có sẵn để sử dụng thì sẽ chỉ có 4 - "tính liên kết 4 chiều" của bộ nhớ cache.

Tức là bạn có thể sẽ bị lỗi bộ nhớ cache cứ sau 4 lần lặp lại xung quanh vòng lặp này. Không tốt.

(Thực ra, mọi thứ phức tạp hơn một chút. Nhưng trên đây là cách hiểu sơ bộ. Địa chỉ của các mục nhập của B được đề cập ở trên là địa chỉ ảo. Vì vậy, có thể có các địa chỉ vật lý hơi khác nhau. Hơn nữa, Bulldozer có cách dự đoán bộ nhớ cache, có thể sử dụng các bit địa chỉ ảo để không phải đợi bản dịch địa chỉ ảo sang địa chỉ vật lý. Tuy nhiên, trong mọi trường hợp: mã của bạn có "cộng hưởng" là 16K. Bộ đệm dữ liệu L1 có cộng hưởng là 16K. Không tốt .)]

Nếu bạn chỉ thay đổi thứ nguyên một chút, ví dụ thành 2048 + 1, thì địa chỉ của mảng B sẽ được trải rộng trên tất cả các bộ của bộ đệm. Và bạn sẽ nhận được ít bộ nhớ cache hơn đáng kể.

Đó là một cách tối ưu hóa khá phổ biến để đệm các mảng của bạn, ví dụ: thay đổi 2048 thành 2049, để tránh hiện tượng cộng hưởng này. Nhưng "chặn bộ nhớ cache là một cách tối ưu hóa thậm chí còn quan trọng hơn. Http://suif.stanford.edu/papers/lam-asplos91.pdf


Ngoài sự cộng hưởng của dòng bộ nhớ cache, có những thứ khác đang diễn ra ở đây. Ví dụ, bộ đệm L1 có 16 ngân hàng, mỗi dải rộng 16 byte. Với thứ nguyên = 2048, các lần truy cập B liên tiếp trong vòng lặp bên trong sẽ luôn đi đến cùng một ngân hàng. Vì vậy, chúng không thể đi song song - và nếu quyền truy cập A xảy ra đến cùng một ngân hàng, bạn sẽ thua.

Tôi không nghĩ, nhìn vào nó, điều này lớn như sự cộng hưởng từ bộ nhớ cache.

Và, vâng, có thể, có thể có răng cưa. Ví dụ: STLF (Bộ đệm lưu trữ để tải chuyển tiếp) có thể chỉ được so sánh bằng cách sử dụng một trường bit nhỏ và nhận được kết quả phù hợp sai.

(Thực ra, nếu bạn nghĩ về nó, cộng hưởng trong bộ nhớ cache giống như răng cưa, liên quan đến việc sử dụng các trường bit. Cộng hưởng là do nhiều dòng bộ nhớ cache ánh xạ cùng một tập hợp, không được lan truyền trước. Hiện tượng cộng hưởng là do đối sánh dựa trên địa chỉ không đầy đủ chút ít.)


Nhìn chung, đề xuất của tôi để điều chỉnh:

  1. Thử chặn bộ nhớ cache mà không cần phân tích thêm. Tôi nói điều này vì chặn bộ nhớ cache rất dễ dàng và rất có thể đây là tất cả những gì bạn cần làm.

  2. Sau đó, sử dụng VTune hoặc OProf. Hoặc Cachegrind. Hoặc là ...

  3. Tốt hơn, hãy sử dụng một quy trình thư viện được điều chỉnh tốt để nhân ma trận.


2
Câu trả lời rất thú vị (+1) nhưng định dạng và chỉnh sửa khủng khiếp :) Tôi đã cố gắng hết sức để cải thiện nó một chút.
UncleZeiv

Đẹp. lỗi đánh máy nhỏ: 256 dòng bộ nhớ cache thay vì 128.
Taye

Cảm ơn bạn đã nắm bắt được điều đó: 2 ^ 8 = 256. Tôi sẽ cố gắng sửa, nhưng tôi cá là tôi không nắm bắt được tất cả các phụ thuộc. Quay lại khi tôi làm việc tại Intel, tôi đã viết một chút "Bảng tính văn bản miễn phí", cho phép các công thức được đặt trong văn bản: nhập một số mới và cách khắc phục được phổ biến. (Tôi đã viết rằng trong undergrad; có lẽ tôi có thể hồi sinh.)
Krazy Glew

17

Có một số giải thích khả thi. Một giải thích có thể xảy ra là những gì Mysticial gợi ý: cạn kiệt tài nguyên hạn chế (bộ nhớ cache hoặc TLB). Một khả năng có thể xảy ra khác là lỗi răng cưa giả, có thể xảy ra khi các lần truy cập bộ nhớ liên tiếp được phân tách bằng bội số của một số hai (thường là 4KB).

Bạn có thể bắt đầu thu hẹp những gì đang làm bằng cách vẽ biểu đồ thời gian / thứ nguyên ^ 3 cho một phạm vi giá trị. Nếu bạn đã làm hỏng bộ nhớ cache hoặc phạm vi tiếp cận TLB cạn kiệt, bạn sẽ thấy một phần phẳng hơn hoặc ít hơn, tiếp theo là sự gia tăng mạnh trong khoảng thời gian từ năm 2000 đến năm 2048, tiếp theo là một phần phẳng khác. Nếu bạn đang nhìn thấy các gian hàng liên quan đến răng cưa, bạn sẽ thấy một biểu đồ phẳng ít nhiều với mức tăng đột biến hẹp đi lên ở mức 2048.

Tất nhiên, điều này có sức mạnh chẩn đoán, nhưng nó không phải là kết luận. Nếu bạn muốn biết rõ ràng nguồn gốc của sự chậm lại là gì, bạn sẽ muốn tìm hiểu về các bộ đếm hiệu suất , có thể trả lời dứt khoát loại câu hỏi này.


+1, Tôi thậm chí chưa bao giờ nghe nói về các gian hàng khai báo sai trong bối cảnh này. Nhưng nghĩ từ khía cạnh thiết kế phần cứng, nó có lý.
Mysticial

10

Tôi biết điều này đã quá cũ, nhưng tôi sẽ ăn một chút. Đó là (như người ta đã nói) một vấn đề về bộ nhớ cache, nguyên nhân gây ra sự chậm lại ở xung quanh quyền hạn của hai. Nhưng có một vấn đề khác với điều này: nó quá chậm. Nếu bạn nhìn vào vòng lặp tính toán của mình.

Vòng lặp trong cùng thay đổi k bằng 1 mỗi lần lặp, có nghĩa là bạn chỉ truy cập 1 nhân đôi từ phần tử cuối cùng mà bạn đã sử dụng của A nhưng toàn bộ 'thứ nguyên' sẽ tăng gấp đôi so với phần tử cuối cùng của B. Điều này không tận dụng được lợi thế của bộ nhớ đệm của các phần tử của B.

Nếu bạn thay đổi điều này thành:

Bạn nhận được kết quả chính xác như nhau (lỗi kết hợp cộng kép modulo), nhưng nó thân thiện với bộ nhớ cache hơn ( cục bộ ). Tôi đã thử nó và nó mang lại những cải tiến đáng kể. Điều này có thể được tóm tắt là

Không nhân ma trận theo định nghĩa, mà thay vào đó, theo hàng


Ví dụ về tăng tốc (Tôi đã thay đổi mã của bạn để lấy thứ nguyên làm đối số)


Phần thưởng (và điều khiến điều này liên quan đến câu hỏi này) là vòng lặp này không bị vấn đề trước đó.

Nếu bạn đã biết tất cả những điều này, thì tôi xin lỗi!


+1 Một thuật toán tốt hơn luôn tạo ra sự khác biệt lớn hơn - bất kể loại bộ nhớ cache nào (hoặc ngay cả khi có một cái), điều này nhanh hơn.
Jerry Jeremiah,

9

Một số câu trả lời đã đề cập đến vấn đề Bộ nhớ đệm L2.

Bạn thực sự có thể xác minh điều này bằng một mô phỏng bộ nhớ cache . Công cụ cachegrind của Valgrind có thể làm được điều đó.

Đặt các thông số dòng lệnh để chúng khớp với các thông số L2 của CPU của bạn.

Kiểm tra nó với các kích thước ma trận khác nhau, có thể bạn sẽ thấy tỷ lệ trượt L2 tăng đột ngột.

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.