當我比較IL
F#爲seq{}
表達式生成的代碼與用戶定義的計算工作流程相比時,很明顯seq{}
的實現方式非常不同:它生成一個狀態機器類似於C#用於其迭代器方法的機器。另一方面,用戶定義的工作流將按照您的期望使用相應的構建器對象。F#:爲seq {}生成的IL代碼vs其他計算工作流程
所以我想知道 - 爲什麼區別?
這是由於歷史原因,例如, 「工作流程之前有序嗎?」?
或者,是否有顯着的表現可以獲得?
其他一些原因?
當我比較IL
F#爲seq{}
表達式生成的代碼與用戶定義的計算工作流程相比時,很明顯seq{}
的實現方式非常不同:它生成一個狀態機器類似於C#用於其迭代器方法的機器。另一方面,用戶定義的工作流將按照您的期望使用相應的構建器對象。F#:爲seq {}生成的IL代碼vs其他計算工作流程
所以我想知道 - 爲什麼區別?
這是由於歷史原因,例如, 「工作流程之前有序嗎?」?
或者,是否有顯着的表現可以獲得?
其他一些原因?
這是由F#編譯器執行的優化。據我所知,後來實際上已經實現了它--F#編譯器首先有列表解析,然後是通用版本的計算表達式(也用於seq { ... }
),但效率較低,所以優化在某些更新的版本中添加。
主要原因是這消除了許多分配和間接。比方說,你有這樣的:
seq { for i in input do
yield i
yield i * 10 }
當使用計算表達式,這被轉換爲類似:
seq.Delay(fun() -> seq.For(input, fun i ->
seq.Combine(seq.Yield(i), seq.Delay(fun() -> seq.Yield(i * 10)))))
有一對夫婦的功能分配和For
循環總是需要調用拉姆達功能。優化將其轉換爲狀態機(類似於C#狀態機),因此對生成的枚舉器的MoveNext()
操作只是改變了某個類的狀態,然後返回...
您可以通過定義對序列的自定義計算建設者:
type MSeqBuilder() =
member x.For(en, f) = Seq.collect f en
member x.Yield(v) = Seq.singleton v
member x.Delay(f) = Seq.delay f
member x.Combine(a, b) = Seq.concat [a; b]
let mseq = MSeqBuilder()
let input = [| 1 .. 100 |]
現在我們就可以進行測試(使用#time
在F#互動):
for i in 0 .. 10000 do
mseq { for x in input do
yield x
yield x * 10 }
|> Seq.length |> ignore
在我的電腦,使用自定義012時,這需要2.644sec構建器,但使用內置優化seq
表達式時僅0.065秒。所以優化使得序列表達式更加高效。
歷史上,計算表達式(「工作流程」)是序列表達式的一般化:http://blogs.msdn.com/b/dsyme/archive/2007/09/22/some-details-on-f-computation-expressions-aka-monadic-or-workflow-syntax.aspx。
但是,答案肯定是有顯着的表現。我無法找到任何固定鏈接(雖然在http://blogs.msdn.com/b/dsyme/archive/2007/11/30/full-release-notes-for-f-1-9-3-7.aspx中提到了「在'順序表達式中過濾'時與'相關的優化'),但我確實記得這是一種優化,它在某個點時間。我想說,好處是不言而喻的:序列表達式是一種「核心」語言特性,並且可以進行任何優化。
同樣,你會看到某些尾遞歸函數將優化到循環,而不是尾調用。
值得注意的是,你可以'內聯'你的自定義構建器方法來獲得一些優化。 – t0yv0
@TomasPetricek:有什麼方法可以重寫MSeqBuilder,使其生成更接近優化狀態機版本的代碼? – user1411900
@toyvo這是一個很好的觀點。這應該會讓它變得更快。 –