功能化数字代码

pro*_*rth 3 performance f# mathnet-numerics math.net

玩F#,我试图以更实用的方式思考代码.我工作的很大一部分恰好是数字化的,所以我在想这种再教育是否有意义.是否正在以一种功能性的方式编写数字代码,例如试图在圆孔中安装方形钉,或者它只是一个陡峭的学习曲线而不管应用程序如何?

例如,让我们拿一个片段来演示大数的弱定律:

open System
open System.IO
open System.Windows.Forms
open System.Windows.Forms.DataVisualization
open FSharp.Data
open FSharp.Charting
open FSharp.Core.Operators
open MathNet.Numerics
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.Random
open MathNet.Numerics.Distributions
open MathNet.Numerics.Statistics


let T = 1000

let arr1 = Array.init T (fun i -> float i*0.)
for i in 0 .. T-1 do
    arr1.[i] <- [|for j in 1..i do yield Exponential.Sample(0.1)|] |> Statistics.Mean

let arr2 = Array.init T (fun i -> float i*0.)
for i in 0 .. T-1 do
    arr2.[i] <- arr1.[1 .. i] |> Statistics.Mean

arr2 |> Chart.Line |> Chart.Show
Run Code Online (Sandbox Code Playgroud)

是否有一种简洁的功能性表达方式?有多少功能范例可以融入到这样的工作中?

不确定问题是否属于主题.谢谢.

Van*_*oiy 6

这实际上是两个问题:一个是关于改进给定的代码,另一个是关于F#中的函数数字代码.由于其他答案已经集中在特定代码上,我将重点关注更一般的问题.

是关于表现吗?

根据我的经验,函数式编程在数值方面的适用性取决于性能要求.执行时间越重要,您就越想在功能风格上妥协.

如果性能不是问题,功能代码往往可以很好地工作.它简洁而安全,与命令式编程相比更接近数学写作.当然,一些问题很好地映射到命令式程序,但总体而言,功能风格是一个很好的默认选择.

如果性能有点问题,您可能希望在不变性方面做出妥协.F#中功能代码的主要成本来自垃圾收集器,尤其是来自具有中间生命周期的对象.使昂贵的对象变得可变并重新使用它们可以在执行速度上产生巨大的差异.如果你想以简洁和安全的方式写出流体动力学,n体仿真或游戏等东西,但不是针对踏板到金属的执行速度,多范式F#风格可能是一个很好的方法走.

如果性能就是一切,那么无论如何你都需要GPU执行.或者可以充分利用CPU矢量单元,多线程等.虽然有人试图在GPU上使用F#,但这种语言并不是为了不惜一切代价而设计的.在这种情况下使用更接近硬件的语言可能更好.

当问题是这些问题的混合时,通常可以混合解决方案.例如,昨天我需要对一组图像进行逐像素计算,执行时​​间很重要.所以我使用.NET库读取F#中的图像,然后将它们与转换像素的GLSL计算着色器一起上传到GPU,然后将它们下载回"F#land".这里的要点是管理运作效率不高; 代码仍在抄袭周围没有任何实际原因.但这只是一次操作会吃掉所有的性能,因此在一次操作中使用高性能工具是合理的,而所有其他操作在F#中安排得非常安全.

  • 很高兴有人选择详细讨论这部分问题.我还要补充一点,你可以沿着不同的路线走,以提高性能.当涉及到小的常数因子加速时,命令式代码总是会获胜,但是如何使用更抽象的功能代码设计具有更好算法复杂度的算法通常更容易也更明显.出于这个原因,我认为对于已经解决的问题,我们通常会更好地使用经过验证的必要解决方案,但对于新颖开发的更多功能代码可能会更好. (2认同)

Ant*_*fer 5

我首先不要将调用Array.init与设置初始值分开.您可以使用@ s952163在其答案中使用的表单,或者根据您的代码:

let arr1 = Array.init T (fun i ->
    [|for j in 1..i do yield Exponential.Sample 0.1 |] |> Statistics.Mean
)
Run Code Online (Sandbox Code Playgroud)

问题是你正在分配中间数组,这是昂贵的 - 而且你无论如何都要在计算平均值之后丢弃它们.替代方案:

let arr1 = Array.init T (fun i ->
    Exponential.Samples 0.1 |> Seq.take (i+1) |> Seq.average
)
Run Code Online (Sandbox Code Playgroud)

现在对于第二部分:您重复计算元素1..i的平均值,这将成为O(n ^ 2)运算.您可以通过使用元素1..i的总和是元素1 .. {i-1}加上第i个元素的总和来在O(n)中求解它.

let sums, _ =
    arr1
    |> Array.mapFold (fun sumSoFar xi ->
        let s = sumSoFar + xi
        s, s
    ) 0.0
let arr2 = 
    sums
    |> Array.mapi (fun i sumi -> sumi / (float (i + 1)))
Run Code Online (Sandbox Code Playgroud)

当然,你们都可以在一个管道中写出来.

或者,使用库函数Array.scan计算累积总和,在这种情况下会给出长度的结果,T+1然后从中删除第一个元素:

let arr2 =
    Array.sub (Array.scan (+) 0.0 arr1) 1 T
    |> Array.mapi (fun i sumi -> sumi / (float (i + 1)))
Run Code Online (Sandbox Code Playgroud)

或避免中间数组:

Seq.scan (+) 0.0 arr1
|> Seq.skip 1
|> Seq.mapi (fun i sumi -> sumi / (float (i + 1)))
|> Seq.toArray
Run Code Online (Sandbox Code Playgroud)