F#: 部分应用和预计算

5

今天我在看我的代码中的一个函数,我想知道是否可能将部分组合和优化结合起来:

let foo (X:float) y1 y2 dx = 
    y1 + (y2 - y1) * dx / X

基本上,只需应用比率 - 因此,在给定的循环内,前三个参数通常是相同的。

我想也许只需要这样做:

let foo2 (X:float) y1 y2 dx = 
    let dy = (y2 - y1) / X
    y1 + dy * dx

当我部分应用前三个参数时,F#会变得聪明并对我进行优化,但在调试模式下似乎并非如此(尽管我不确定我是否以正确的方式进行了测试)。

问题是,这样做是否可行?如果不行,有没有更好的方法(除了编写另一个只有两个参数的函数)?

3个回答

4
我认为大多数这样的“神奇优化”需要进行“影响分析”,这只有神秘的“足够聪明的编译器”才能完成。
请考虑以下内容:
let Expensive x y = 
    printfn "I am a side-effect of Expensive"
    x + y  // imagine something expensive

let F x y z =
    let tmp = Expensive x y
    z + tmp

printfn "Least chance of magic compiler optimization"
for i in 1..3 do    
    F 3 4 i

printfn "Slightly better chance"
let Part = F 3 4
for i in 1..3 do    
    Part i

printfn "Now for real"
let F2 x y =
    let tmp = Expensive x y
    (fun z -> z + tmp)

printfn "Of course this still re-does it"
for i in 1..3 do    
    F2 3 4 i

printfn "Of course this finally saves re-do-ing Expensive"
let Opt = F2 3 4
for i in 1..3 do    
    Opt i

(* output

Least chance of magic compiler optimization
I am a side-effect of Expensive
I am a side-effect of Expensive
I am a side-effect of Expensive
Slightly better chance
I am a side-effect of Expensive
I am a side-effect of Expensive
I am a side-effect of Expensive
Now for real
Of course this still re-does it
I am a side-effect of Expensive
I am a side-effect of Expensive
I am a side-effect of Expensive
Of course this finally saves re-do-ing Expensive
I am a side-effect of Expensive

*)    

重点是,关于效果的语义要求编译器像这样精确地行事,除非“Expensive”没有效果,编译器真的很聪明,并且能自行发现这一点。


此外,这就是为什么人们希望在.NET中有一个“PureAttribute”,你可以放在‘Expensive’上(假设它不像我举例时会打印出来),以便引导编译器进行此优化。或者,这就是为什么人们喜欢Haskell,其中一切都是纯的,编译器/运行时可以缓存任何函数调用。归根结底,我个人认为寄希望于系统会“神奇地优化”代码是一厢情愿。如果您希望您的代码运行快速,请逐步向计算机先生说明。人类必须始终做出艰苦的努力。 - Brian
你的波特率和我的大脑完全一样 :) - Benjol

3

(这不是为了炫耀声誉,我问问题时真的没有想到这个)

这是我想出来的一个解决方案,不确定是否最好:

let foo3 (X:float) y1 y2 =
    let dy = (y2 - y1) / X
    (fun dx -> y1 + dy * dx)

速度更快。


这应该和我的答案一样。这就是柯里化函数的部分应用(F#默认)的工作方式。 - Jonathan Graehl
好的...非优化模式下速度更快。奇怪。这应该被修复。 - Jonathan Graehl
@wrang-wrang,这与柯里化函数的简单部分应用不同,因为效果已被重新排序。一个简单的 eta 转换将保留函数顶部的 "fun dx ->",但将 "fun dx ->" 移动到其他代码之后意味着在应用前两个参数后,其他代码仅运行一次。(在没有效果的情况下,这是无法区分的,但在存在效果时,差异是明显的。) - Brian

1

我并不惊讶在调试模式下似乎没有任何不同。为什么不实际计时N次重复操作(在F#交互提示符中键入#time;;)。

至于你希望共享所有但dx固定值的常见计算,可以尝试这个:

let fdx = foo2 X y1 y2
for dx in dxes do
    fdx dx

也就是说,fdx是部分应用程序。在循环外明确存储它,让我对优化器有更高的期望。

至少在交互式提示符中(我不认为那里进行了完全优化,是吗?),看起来我的建议只能提高15%的速度(很奇怪会有任何加速,因为它肯定会重复foo2的全部内容)。这样做要快得多:

let fdx = foo3 X y1 y2
for dx in dxes do
    fdx dx

其中 foo3 是(来自 Benjlol):

let foo3 (X:float) y1 y2 =
    let dy = (y2 - y1) / X
    (fun dx -> y1 + dy * dx)

请注意,在循环中仅将foo3用作4个参数函数的速度是foo2的两倍,但在循环外存储部分应用程序,则速度快3倍。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接