Recursion F递归与迭代速度/开销

Recursion F递归与迭代速度/开销,recursion,functional-programming,f#,Recursion,Functional Programming,F#,我一直在玩Riccardo Terrell的Akka.NET分形演示,试图理解它。很好,顺便说一句 有一件事对我自己来说是一个小挑战,那就是用一种更实用的方式重写其中的一些内容。我已经让它工作了,但是它比原来的慢多了 以下是适用于测试的原始Mandelbrot集合计算: 让mandelbrotSet xp:intyp:intw:inth:intwidth:intheight:int maxr:float-minr:float-maxi:float-mini:float:List= 设可变zx=0

我一直在玩Riccardo Terrell的Akka.NET分形演示,试图理解它。很好,顺便说一句

有一件事对我自己来说是一个小挑战,那就是用一种更实用的方式重写其中的一些内容。我已经让它工作了,但是它比原来的慢多了

以下是适用于测试的原始Mandelbrot集合计算:

让mandelbrotSet xp:intyp:intw:inth:intwidth:intheight:int maxr:float-minr:float-maxi:float-mini:float:List= 设可变zx=0。 设可变zy=0。 设可变cx=0。 设可变cy=0。 设可变xjump=maxr-minr/浮点宽度 让yjump=最大-最小/浮动高度 设可变tempzx=0。 设loopmax=1000 设可变循环为0 让outputList:int list=list.empty 对于x=xp到xp+w-1 do cx List.mapfun c->Rgba32bytec%32*7,bytec%128*2,bytec%16*14[Rgba32.Black] 让rec mbCalczx:float,zy:float,cx:float,cy:float,loopCount:int= 将zx*zx+zy*zy,loopCount与//z的大小的平方匹配 |a,b当a>4.|b=loopMax->loopCount |->mbCalczx*zx-zy*zy+cx,2.*zx*zy+cy,cx,cy,loopCount+1//迭代是z^2+c的下一个值 [|0..w-1 |]//对于每个x。。。 |>Array.map fun x->让cx=xjump*float x+xp-absminr [0..h-1 |]///并且对于每个y。。。 |>Array.map fun y->let cy=yjump*float y+yp-absmini 让mbVal=mbCalc0.,0.,cx,cy,0//递归计算收敛的迭代次数 img。[x,y]忽略 img
首先,两个函数都返回[],因此即使计算正确,也不会返回mandlebrot集。append返回一个列表,它不会改变现有的列表

使用下面的快速BenchmarkDotNet程序,其中每个函数都在其自己的模块中:

open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
open ActorTest

[<MemoryDiagnoser>]
type Bench() =
    let xp = 1500
    let yp = 1500
    let w = 200
    let h = 200
    let width = 4000
    let height = 4000    

    [<Benchmark(Baseline=true)>]
    member _.Mutable() =
        Mutable.mandelbrotSet xp yp w h width height 0.5 -2.5 1.5 -1.5

    [<Benchmark>]
    member _.Recursive() =
        Recursive.mandelbrotSet xp yp w h width height 0.5 -2.5 1.5 -1.5

[<EntryPoint>]
let main argv =
    let summary = BenchmarkRunner.Run<Bench>()
    printfn "%A" summary
    0 // return an integer exit code
我注意到您使用的是Array.map,但在任何地方都没有捕获结果,因此将其更改为Array.iter使您的代码几乎相同:

|    Method |     Mean |     Error |    StdDev | Ratio |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------- |---------:|----------:|----------:|------:|---------:|------:|------:|----------:|
|   Mutable | 1.515 ms | 0.0107 ms | 0.0094 ms |  1.00 | 406.2500 |     - |     - |   1.22 MB |
| Recursive | 1.652 ms | 0.0114 ms | 0.0101 ms |  1.09 | 607.4219 |     - |     - |   1.82 MB |
这种差异可能是通过映射调用完成的额外分配来解释的。分配是昂贵的,特别是当它是较大的阵列时,因此如果可能,最好避免使用它们。不同机器的精确计时会有所不同,但我希望在使用BenchmarkDotNet时,前后比率类似


这可以通过避免列表分配和预分配您填写的列表或数组来进一步优化。迭代调用也是如此。首先,两个函数都返回[],因此即使计算正确,也不会返回mandlebrot集。append返回一个列表,它不会改变现有的列表

使用下面的快速BenchmarkDotNet程序,其中每个函数都在其自己的模块中:

open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
open ActorTest

[<MemoryDiagnoser>]
type Bench() =
    let xp = 1500
    let yp = 1500
    let w = 200
    let h = 200
    let width = 4000
    let height = 4000    

    [<Benchmark(Baseline=true)>]
    member _.Mutable() =
        Mutable.mandelbrotSet xp yp w h width height 0.5 -2.5 1.5 -1.5

    [<Benchmark>]
    member _.Recursive() =
        Recursive.mandelbrotSet xp yp w h width height 0.5 -2.5 1.5 -1.5

[<EntryPoint>]
let main argv =
    let summary = BenchmarkRunner.Run<Bench>()
    printfn "%A" summary
    0 // return an integer exit code
我注意到您使用的是Array.map,但在任何地方都没有捕获结果,因此将其更改为Array.iter使您的代码几乎相同:

|    Method |     Mean |     Error |    StdDev | Ratio |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------- |---------:|----------:|----------:|------:|---------:|------:|------:|----------:|
|   Mutable | 1.515 ms | 0.0107 ms | 0.0094 ms |  1.00 | 406.2500 |     - |     - |   1.22 MB |
| Recursive | 1.652 ms | 0.0114 ms | 0.0101 ms |  1.09 | 607.4219 |     - |     - |   1.82 MB |
这种差异可能是通过映射调用完成的额外分配来解释的。分配是昂贵的,特别是当它是较大的阵列时,因此如果可能,最好避免使用它们。不同机器的精确计时会有所不同,但我希望在使用BenchmarkDotNet时,前后比率类似


这可以通过避免列表分配和预分配您填写的列表或数组来进一步优化。迭代调用也是如此。也在我的老朋友斯潘·曼德布罗特的房间里转来转去。无论如何,根据我的经验,在F中编写良好的尾部递归函数是我首选的等待迭代。如果您使用像dnSpy这样的工具,您可以分析生成的代码,如果您正确地进行了分析,您将看到生成的代码确实是一个循环。它通常包含一些奇怪的东西,但总的来说,抖动似乎能够消除不必要的变量。F中尾部递归的一个好处是,这是正确地迭代到0的唯一方法,消除了对额外寄存器的需要,有时可以提高一点速度。此外,可变循环本身效率低下。有许多优化可以应用于它。通常,JIT会根据您的评论优化递归,从而消除任何额外的开销。在某些情况下,.tail指令的过度发出可能会影响性能,但对于.NET5来说,这也基本上得到了解决。换句话说,免费表演就要来了!曼德布罗特,我的老朋友。无论如何,根据我的经验,在F中编写良好的尾部递归函数是我首选的等待迭代。如果使用dnSpy之类的工具,您可以分析生成的代码
如果你做得正确,你会发现生成的代码确实是一个循环。它通常包含一些奇怪的东西,但总的来说,抖动似乎能够消除不必要的变量。F中尾部递归的一个好处是,这是正确地迭代到0的唯一方法,消除了对额外寄存器的需要,有时可以提高一点速度。此外,可变循环本身效率低下。有许多优化可以应用于它。通常,JIT会根据您的评论优化递归,从而消除任何额外的开销。在某些情况下,.tail指令的过度发出可能会影响性能,但对于.NET5来说,这也基本上得到了解决。换句话说,免费表演就要来了!