我對用 f# 編程還很陌生,雖然我很喜歡它,但讓我困擾的一件事是,在 f# 中以“正確”的方式撰寫代碼導致它非常慢的頻率。我一直在用 f# 開發神經網路,與其他語言的實作相比,我的代碼非常慢。一種特殊情況是以下線性代數函式:
// Dot Product
// Slow
let rec dotProduct (vector1 : float []) (vector2 : float []) : float =
if vector1 |> Array.length = 0 then
0.0
else
vector1.[0] * vector2.[0] (dotProduct (vector1 |> Array.tail) (vector2 |> Array.tail))
// Fast
let dotProduct (vector1 : float []) (vector2 : float []) =
Array.fold2 (fun state x y -> state x * y) 0.0 vector1 vector2
// Matrix Vector Product
// Slow
let matrixVectorProduct (matrix : float [,]) (vector : float[]) : float [] =
[|
for i = 0 to (matrix |> Array2D.length1) - 1 do
yield dotProduct matrix.[i, 0..] vector
|]
// Fast
let matrixVectorProduct (matrix : float [,]) (vector : float[]) : float [] =
let mutable product = Array.zeroCreate (matrix |> Array2D.length1)
for i = 0 to (matrix |> Array2D.length1) - 1 do
product.[i] <- (dotProduct matrix.[i, 0..] vector)
product
我想知道是否有更多 f# 經驗的人可以解釋為什么每個示例的第二種情況都更快,就計算機如何解釋我的代碼而言。使用像 f# 這樣的高級語言進行編碼的最大痛苦是,與使用低級語言編程相比,很難知道您的代碼是如何優化和運行的。
uj5u.com熱心網友回復:
對于第一個代碼示例:
您的慢dotProduct功能正在做兩件影響 CPU 性能的事情,按影響順序排列:
- 每次遞回呼叫重新分配一個陣列
- 不使用尾遞回
從我的測量來看,第二點并不是什么大問題。
對于第二個樣本:
你的慢版本很慢,因為 F# 陣列運算式不是固定的。它需要分配和使用一個列舉器來生成下一個專案,直到它完成。在你的迭代代碼中,你預先分配了一個固定的陣列,你只是在填充它。這總是明顯更快,當你關心性能時,變異和回圈通常是一個很好的獲勝方式。
不過,還有另一種方法可以加速您的點積代碼:只需執行一個簡單的回圈即可!
let dotProductLoop (vector1 : float []) (vector2 : float []) : float =
let mutable acc = 0.0
for idx = 0 to vector1.Length - 1 do
acc <- acc (vector1.[idx] * vector2.[idx])
acc
您會注意到[fold2][1]或多或少會這樣做,但它會帶來一些邊際開銷。
我將每種方法都放入一個基準測驗中,以查看一些比較結果。正如您所看到的,回圈方法甚至比fold2呼叫還要快,但兩者都比您的初始實作快得多,因此顯然可以勝任。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1288 (20H2/October2020Update)
AMD Ryzen 9 5900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK=6.0.100-rc.2.21505.57
[Host] : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT DEBUG
DefaultJob : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
| 方法 | 意思 | 錯誤 | 標準差 | 比率 | 第 0 代 | 第一代 | 已分配 |
|---|---|---|---|---|---|---|---|
| 點積 | 320,534.9 納秒 | 1,738.55 納秒 | 1,626.24 納秒 | 1.000 | 480.4688 | 18.0664 | 8,040,000 乙 |
| 點積回圈 | 625.4 納秒 | 1.93 納秒 | 1.71 納秒 | 0.002 | —— | —— | —— |
| 點積折疊 | 1,105.1 納秒 | 10.77 納秒 | 10.07 納秒 | 0.003 | —— | —— | —— |
如果您致力于撰寫遞回代碼,您可以做的另一件事是擁有一個對Spanor執行尾遞回的私有幫助函式ReadonlySpan:
let rec private dotProductImpl (vector1 : Span<float>) (vector2 : Span<float>) (acc: float) =
if vector1.Length = 0 then
acc
else
dotProductImpl (vector1.Slice(1)) (vector2.Slice(1)) (acc vector1.[0] * vector2.[0])
呼叫它的函式將與我提出的回圈一樣執行。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/338989.html
