[英]How to optimize this product of three matrices in C++ for x86?
我有一個密鑰算法,其中大部分運行時用於計算密集矩陣產品:
A*A'*Y, where: A is an m-by-n matrix,
A' is its conjugate transpose,
Y is an m-by-k matrix
Typical characteristics:
- k is much smaller than both m or n (k is typically < 10)
- m in the range [500, 2000]
- n in the range [100, 1000]
基於這些維度,根據矩陣鏈乘法問題的教訓,很明顯,在操作數意義上將計算結構化為A*(A'*Y)
是最優的。 我當前的實現就是這樣做的,而只是強迫關聯性到表達式的性能提升是顯而易見的。
我的應用程序是用C ++編寫的x86_64平台。 我正在使用Eigen線性代數庫, 英特爾的數學核心庫作為后端。 Eigen能夠使用IMKL的BLAS接口來執行乘法,並且從我的Sandy Bridge機器上移動到Eigen的原生SSE2實現到Intel優化的基於AVX的實現的提升也很重要。
然而,表達式A * (A.adjoint() * Y)
(以Eigen的說法編寫)被分解為兩個通用矩陣 - 矩陣乘積(調用xGEMM
BLAS例程), xGEMM
創建了臨時矩陣。 我想知道,通過一次專門的實現來一次評估整個表達式,我可以得到一個比我現在的通用更快的實現。 一些讓我相信這一點的觀察結果是:
使用上述典型尺寸,輸入矩陣A
通常不適合緩存。 因此,用於計算三矩陣乘積的特定存儲器訪問模式將是關鍵。 顯然,避免為部分產品創建臨時矩陣也是有利的。
A
及其共軛轉置顯然具有非常相關的結構,可以利用該結構來改善整體表達的存儲器訪問模式。
是否有任何標准技術以緩存友好的方式實現這種表達式? 我發現的矩陣乘法的大多數優化技術都是針對標准的A*B
情況,而不是更大的表達式。 我對這個問題的微優化方面很滿意,比如轉換成適當的SIMD指令集,但是我正在尋找任何可以用最友好的方式打破這個結構的引用。
編輯:根據到目前為止的反應,我想我上面有點不清楚。 我使用C ++ / Eigen的事實實際上只是我對這個問題的看法的一個實現細節。 Eigen在實現表達式模板方面做得很好,但是不支持將此類問題作為簡單表達式進行評估(僅支持2個通用密集矩陣的產品)。
在比編譯器如何評估表達式更高的層次上,我正在尋找復合乘法運算的更有效的數學分解,並且由於A
及其共軛的共同結構而傾向於避免不必要的冗余存儲器訪問。轉。 結果可能很難在純Eigen中有效地實現,所以我可能只是在具有SIMD內在函數的專門例程中實現它。
這不是一個完整的答案(但是 - 我不確定它會變成一個)。
讓我們首先想一下數學。 由於矩陣乘法是關聯的,我們可以做(A * A') Y或A (A'* Y)。
(A * A')* Y的浮點運算
2*m*n*m + 2*m*m*k //the twos come from addition and multiplication
A *(A'* Y)的浮點運算
2*m*n*k + 2*m*n*k = 4*m*n*k
由於k遠小於m和n,因此很明顯為什么第二種情況要快得多。
但是通過對稱性我們原則上可以將A * A'的計算次數減少兩次(雖然這可能不容易用SIMD),因此我們可以減少(A * A')* Y的浮點運算次數至
m*n*m + 2*m*m*k.
我們知道m和n都大於k。 讓我們為m和n選擇一個名為z
的新變量,並找出第一和第二種情況相等的情況:
z*z*z + 2*z*z*k = 4*z*z*k //now simplify
z = 2*k.
因此,只要m和n都超過兩倍k,第二種情況就會有更少的浮點運算。 在您的情況下,m和n都大於100且k小於10,因此情況2使用的浮點運算少得多。
在高效的代碼方面。 如果代碼針對高效使用緩存進行了優化(如MKL和Eigen),那么大密集矩陣乘法是計算綁定而不是內存綁定,因此您不必擔心緩存。 MKL比Eigen更快,因為MKL使用AVX(現在可能是fma3?)。
我認為你不能比你使用第二種情況和MKL(通過Eigen)更有效地做到這一點。 啟用OpenMP以獲得最大FLOPS。
您應該通過將FLOPS與處理器的峰值FLOPS進行比較來計算效率。 假設你有一個Sandy Bridge / Ivy Bridge處理器。 峰值SP FLOPS是
frequency * number of physical cores * 8 (8-wide AVX SP) * 2 (addition + multiplication)
雙進動除以2。 如果你有Haswell並且MKL使用FMA,那么加倍峰值FLOPS。 要獲得正確的頻率,您必須對所有核心使用turbo boost值(它低於單核心)。 如果您沒有超頻系統或在Windows上使用CPU-Z或在Linux上使用Powertop,如果你有一個超頻系統,你可以查看這個。
使用臨時矩陣計算A'* Y,但要確保告訴eigen沒有混疊: temp.noalias() = A.adjoint()*Y
然后計算你的結果,再次告訴eigen對象沒有別名: result.noalias() = A*temp
。
只有當你執行(A*A')*Y
時才會有冗余計算,因為在這種情況下(A*A')
是對稱的,只需要計算的一半。 但是,正如您所注意到的那樣,執行A*(A'*Y)
仍然要快得多,在這種情況下,沒有冗余計算。 我確認臨時創建的成本在這里完全可以忽略不計。
我想這會執行以下操作
result = A * (A.adjoint() * Y)
會像那樣做
temp = A.adjoint() * Y
result = A * temp;
如果您的矩陣Y
適合緩存,您可以利用這樣做
result = A * (Y.adjoint() * A).adjoint()
或者,如果不允許以前的表示法,那就是這樣
temp = Y.adjoint() * A
result = A * temp.adjoint();
然后你不需要做矩陣A的伴隨,並存儲A的臨時伴隨矩陣,這將比Y的那個貴得多。
如果你的矩陣Y適合緩存,那么第一次乘法在A的列上運行循環應該快得多,然后在第二次多重復制的A行上運行(在緩存中有Y.adjoint())第一個乘法和temp.adjoint()為第二個),但我想內部特征已經在處理這些事情。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.