【问题标题】:Why is F# so much slower than C#? (prime number benchmark)为什么 F# 比 C# 慢这么多? (质数基准)
【发布时间】:2018-12-31 02:42:28
【问题描述】:

我认为 F# 的本意是比 C# 更快,我做了一个可能很糟糕的基准测试工具,C# 得到了 16239 毫秒,而 F# 做得更糟,只有 49583 毫秒。有人可以解释这是为什么吗?我正在考虑离开 F# 并回到 C#。是否有可能在 F# 中使用更快的代码获得相同的结果?

这是我使用的代码,我尽可能做到了。

F# (49583 毫秒)

open System
open System.Diagnostics

let stopwatch = new Stopwatch()
stopwatch.Start()

let mutable isPrime = true

for i in 2 .. 100000 do
    for j in 2 .. i do
        if i <> j && i % j = 0 then
            isPrime <- false
    if isPrime then
        printfn "%i" i
    isPrime <- true

stopwatch.Stop()
printfn "Elapsed time: %ims" stopwatch.ElapsedMilliseconds

Console.ReadKey() |> ignore

C# (16239 毫秒)

using System;
using System.Diagnostics;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            bool isPrime = true;

            for (int i = 2; i <= 100000; i++)
            {
                for (int j = 2; j <= i; j++)
                {
                    if (i != j && i % j == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime)
                {
                    Console.WriteLine(i);
                }
                isPrime = true;
            }
            stopwatch.Stop();
            Console.WriteLine("Elapsed time: " + stopwatch.ElapsedMilliseconds + "ms");
            Console.ReadKey();
        }
    }
}

【问题讨论】:

  • 您的 C# 代码会跳出内部循环,而 F# 不会。
  • 这不是链接问题的副本。链接问题中的问题是枚举数,而这里的问题是 F# 代码中没有 break 等效项。当问题被错误地关闭为骗子时,我正在写答案。
  • 当我们等待它重新打开时,您可以玩这个:{2 .. 100000} |&gt; Seq.filter (fun i -&gt; {2 .. i-1} |&gt; Seq.forall (fun j -&gt; i % j &lt;&gt; 0)) |&gt; Seq.iter (printfn "%i")。对于相同的逻辑,这是更惯用的 F#,它在 F# Interactive 中对我来说运行大约 3 秒。
  • @Alanay - 您可以对此代码进行的一项改进(我没有在答案中提到,因为它与问题无关)将只检查除数(包括)正方形数的根。因此,如果您要检查 42 是否为素数,则最多需要检查 6,但不必检查 7,因为 6 Seq.filter 调用就在Seq.foreach 之前,比如{2 .. i-1} |&gt; Seq.filter (fun j -&gt; j * j &lt;= i) |&gt; Seq.forall (fun j -&gt; i % j &lt;&gt; 0)
  • 我认为没有人声称 F# 更快。也许更容易编写并行代码,因此更容易利用多核。此外,Seq 和性能很少能很好地融合在一起。

标签: c# f# benchmarking primes


【解决方案1】:

F# 程序较慢,因为您的程序不等效。您的 C# 代码在内部 for 循环中有一个 break 语句,但您的 F# 程序没有。因此,对于每个偶数,C# 代码将在检查可被 2 整除后停止,而 F# 程序将检查从 2 到 i 的每个数字。由于完成的工作差异如此之大,F# 代码慢了三倍,这实际上令人惊讶!

现在,F# 故意没有break 语句,因此您不能将 C# 代码直接翻译成 F#。但是您可以使用包含短路逻辑的函数。例如,在 cmets 中,Aaron M. Eshbach 提出了以下建议:

{2 .. 100000}
|> Seq.filter (fun i -> {2 .. i-1} |> Seq.forall (fun j -> i % j <> 0))
|> Seq.iter (printfn "%i")

这使用了Seq.forall,它确实进行了短路:它将根据条件检查序列中的每个输入,如果条件返回false,它将停止并且不再进行检查。 (因为Seq 模块中的函数是lazy 并且不会做更多的工作而不是绝对需要获得它们的输出)。这就像在你的 C# 代码中有一个break

我将逐步完成此操作,以便您了解它是如何工作的:

{2 .. 100000}

这会创建一个惰性整数序列,从 2 开始直到(包括)100000。

|> Seq.filter (fun i -> (some expression involving i))

我将下一行分成两部分:外部Seq.filter 部分和涉及i 的内部表达式。 Seq.filter 部分接受序列并对其进行过滤:对于序列中的每个项目,将其称为 i 并评估表达式。如果该表达式的计算结果为true,则保留该项目并将其传递到链中的下一步。如果表达式是false,则丢弃该项目。

现在,涉及i 的表达式为:

{2 .. i-1} |> Seq.forall (fun j -> i % j <> 0)

这首先构造了一个惰性序列,从 2 开始,一直到 i 减一(含)。 (或者你可以认为它从 2 开始,一直到 i,但不包括 i)。然后它检查该序列的每个 项是否满足特定条件(即Seq.forall 函数)。并且,作为Seq.forall 的一个实现细节,因为它很懒惰并且不会比它绝对必须做的更多的工作,一旦它找到false 结果,它就会停止并且不再通过输入序列。 (因为一旦你找到了一个反例,forall 函数就不再可能返回 true,因此一旦知道结果就会停止。)Seq.forall 中检查的表达式是什么?这是fun j -&gt; i % j &lt;&gt; 0。所以j是内部循环变量,i是外部变量(在Seq.filter部分分配的那个),逻辑和你的C#循环一样。

现在,请记住我们在 Seq.filter 中。所以如果Seq.forall 返回真,那么Seq.filter 将保持i 的值。但如果Seq.forall 返回false,那么Seq.filter 将丢弃i 的这个值,不再传递到下一步。

最后,我们将这一行作为下一步(也是最后一步):

|> Seq.iter (printfn "%i")

这与以下内容几乎完全相同:

for number in inputSoFar do
    printfn "%i" number

如果您是 F# 的新手,(printfn "%i") 调用可能看起来不寻常。这是currying,这是一个非常有用的概念,而且习惯这一点很重要。所以花点时间思考一下:在 F# 中,以下两行代码完全等价

(fun y -> someFunctionCall x y)
(someFunctionCall x)

所以fun item -&gt; printfn "%i" item 总是可以替换为printfn "%i。而Seq.iter 相当于for 循环:

inputSoFar |> Seq.iter (someFunctionCall x)

完全等同于:

for item in inputSoFar do
    someFunctionCall x item

那么你就知道了:为什么你的 F# 程序更慢,以及如何编写一个 F# 程序,它遵循与 C# 相同的逻辑,但其中有一个 break 语句。

【讨论】:

  • 顺便说一句,如果你把Seq.iter最后换成Seq.map,你会发现代码不打印任何东西。那是因为Seq.iter 是一个 non-lazy 函数(我更喜欢“一个 eager 函数”这个词,但你之前可能遇到过也可能没有遇到过这个词):它是保证通过所有输入并对每个输入执行操作。而Seq.maplazy:它设置了一个映射来执行它收到的每个项目,但它实际上不会做任何工作,直到它必须做。 (通常是因为有人调用了 eager 函数,例如 Seq.iterSeq.toList,在整个链条中)。
  • 非常感谢,这对我很有帮助,我想了解更多关于序列和列表函数的信息,但没有太多示例。
  • @Alanay - 了解 F# 的列表函数(以及序列函数,它们具有基本相同的 API)的最佳方式是 Scott Wlaschin 的 优秀 站点:fsharpforfunandprofit.com/posts/list-module-functions跨度>
【解决方案2】:

我知道有一个已经被接受的答案,但只是想添加这个。

这些年来做了很多 C#,但没有太多 F#。以下将更等效于 C# 代码。

open System
open System.Diagnostics

let stopwatch = new Stopwatch()
stopwatch.Start()

let mutable loop = true

for i in 2 .. 100000 do
    let mutable j = 2
    while loop do
        if i <> j && i % j = 0 then
            loop <- false
        else
            j <- j + 1
            if j >= i then
                printfn "%i" i
                loop <- false
    loop <- true

stopwatch.Stop()
printfn "Elapsed time: %ims" stopwatch.ElapsedMilliseconds

在我对 LinqPad 的测试中,上述方法比 Aaron M. Eshbach 建议的解决方案更快。

它的 IL 也非常相似。

【讨论】:

  • 不知道它是如何更快但很棒的工作,感谢您发布这个!我很快就会更彻底地阅读这些内容。
  • 这会稍微快一些,因为它消除了调用Seq.filterSeq.forallSeq.iter 函数的开销。通过数十万或数百万次迭代,即使是几个函数调用的微小开销也会加起来。我还是会用序列处理函数来做,因为意思更清楚,代码更惯用。
  • @AaronM.Eshbach 在我的 LinqPad 测试中要快得多。我同意我的代码不符合 F# 的精神。只是出于说明目的发布。
【解决方案3】:

正如其他人提到的,代码没有做同样的事情,您需要采用技术来确保在找到素数后停止内部循环。

此外,您正在将值打印到标准输出。当您进行 CPU 性能测试时,这通常是不希望的,因为大量时间可能会导致 I/O 扭曲测试结果。

无论如何,即使有一个公认的答案,我还是决定对此进行一些修改,以将不同的建议解决方案与我自己的一些解决方案进行比较。

性能运行在 .NET 4.7.1 上的 x64 模式下。

我比较了不同的 F# 解决方案以及我自己的一些变体:

Running 'Original(F#)' with 100000 (10512)...
  ... it took 14533 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Original(C#)' with 100000 (10512)...
  ... it took 1343 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Aaron' with 100000 (10512)...
  ... it took 5027 ms with (3, 1, 0) cc and produces 9592 GOOD primes
Running 'SteveJ' with 100000 (10512)...
  ... it took 1640 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Dumetrulo1' with 100000 (10512)...
  ... it took 1908 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Dumetrulo2' with 100000 (10512)...
  ... it took 970 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Simple' with 100000 (10512)...
  ... it took 621 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'PushStream' with 100000 (10512)...
  ... it took 1627 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Unstalling' with 100000 (10512)...
  ... it took 551 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Vectors' with 100000 (10512)...
  ... it took 1076 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'VectorsUnstalling' with 100000 (10512)...
  ... it took 1072 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'BestAttempt' with 100000 (10512)...
  ... it took 4 ms with (0, 0, 0) cc and produces 9592 GOOD primes  
  1. Original(F#) - OP 的原始 F# 代码更改为不使用标准输出
  2. Original(C#) - OP 的原始 C# 代码更改为不使用标准输出
  3. Aaron - 使用 Seq 的惯用方法。正如所料Seq 和性能通常不能很好地结合在一起。
  4. SteveJ - @SteveJ 试图模仿 F# 中的 C# 代码
  5. Dumetrulo1 - @dumetrulo 在尾递归中实现了算法
  6. Dumetrulo2 - @dumetrulo 通过步进 +2 而不是 +1 改进了算法(不需要检查偶数)。
  7. Simple - 我尝试使用类似于 Dumetrulo2 的尾递归方法。
  8. PushStream - 我尝试使用简单的推流(Seq 是拉流)
  9. Unstalling - 如果使用的指令有延迟,我会尝试解除 CPU 的失速
  10. Vectors - 我尝试使用 System.Numerics.Vectors 对每个操作进行多个除法(又名 SIMD)。不幸的是,向量库不支持mod,所以我不得不模仿它。
  11. VectorsUnstalling - 我尝试通过卸载 CPU 来改进 Vectors
  12. BestAttempt - 与 Simple 类似,但在确定是否为素数时仅检查直到 sqrt n 的数字。

总结

  1. F# 循环没有continue 也没有break。 F# 中的尾递归是 IMO 实现需要 break 的循环的更好方法。
  2. 在比较语言的性能时,应该比较可能的最佳性能还是比较惯用解决方案的性能?我个人认为最好的性能是正确的方法,但我知道人们不同意我的观点(我为 F# 写了一个mandelbrot version for benchmark the game,其性能与 C 相当,但它没有被接受,因为这种风格被认为是非惯用的F#)。
  3. 不幸的是,F# 中的Seq 增加了显着的开销。即使开销不相关,我也很难让自己使用它。
  4. 现代 CPU 指令具有不同的吞吐量和延迟数。这意味着有时为了提高性能,需要在内循环中处理多个独立样本,以允许乱序执行单元重新排序程序以隐藏延迟。如果您的 CPU 具有超线程并且您在多个线程上运行算法,则超线程可以“自动”减轻延迟。
  5. 缺少mod 向量阻止了尝试使用 SIMD 来获得优于非 SIMD 解决方案的任何性能。
  6. 如果我修改 Unstalling 尝试循环与 C# 代码相同的次数,最终结果是 F# 中的 1100 ms 与 C# 中的 1343 ms 相比。因此,可以使 F# 的运行与 C# 非常相似。如果再应用一些技巧,它只需要4 ms,但对于 C# 也是一样的。无论如何,从 15 sec4 ms 几乎是不错的选择。

希望有人感兴趣

完整源代码:

module Common = 
  open System
  open System.Diagnostics

  let now =
    let sw = Stopwatch ()
    sw.Start ()
    fun () -> sw.ElapsedMilliseconds

  let time i a =
    let inline cc i       = GC.CollectionCount i

    let ii = i ()

    GC.Collect (2, GCCollectionMode.Forced, true)

    let bcc0, bcc1, bcc2  = cc 0, cc 1, cc 2
    let b                 = now ()

    let v = a ii

    let e = now ()
    let ecc0, ecc1, ecc2  = cc 0, cc 1, cc 2

    v, (e - b), ecc0 - bcc0, ecc1 - bcc1, ecc2 - bcc2

  let limit    = 100000
  // pi(x) ~= limit/(ln limit - 1)
  // Using pi(x) ~= limit/(ln limit - 2) to over-estimate
  let estimate = float limit / (log (float limit) - 1.0 - 1.0) |> round |> int

module Original =
  let primes limit =
    let ra = ResizeArray Common.estimate

    let mutable isPrime = true

    for i in 2 .. limit do
      for j in 2 .. i do
        if i <> j && i % j = 0 then
          isPrime <- false
      if isPrime then
          ra.Add i
      isPrime <- true

    ra.ToArray ()

module SolutionAaron =
  let primes limit =
    {2 .. limit}
    |> Seq.filter (fun i -> {2 .. i-1} |> Seq.forall (fun j -> i % j <> 0))
    |> Seq.toArray

module SolutionSteveJ =
  let primes limit =
    let ra = ResizeArray Common.estimate
    let mutable loop = true

    for i in 2 .. limit do
        let mutable j = 2
        while loop do
            if i <> j && i % j = 0 then
                loop <- false
            else
                j <- j + 1
                if j >= i then
                    ra.Add i
                    loop <- false
        loop <- true

    ra.ToArray ()

module SolutionDumetrulo1 =
  let rec isPrimeLoop (ra : ResizeArray<_>) i j limit =
    if i > limit then ra.ToArray ()
    elif j > i then
      ra.Add i
      isPrimeLoop ra (i + 1) 2 limit
    elif i <> j && i % j = 0 then
      isPrimeLoop ra (i + 1) 2 limit
    else
      isPrimeLoop ra i (j + 1) limit

  let primes limit =
    isPrimeLoop (ResizeArray Common.estimate) 2 2 limit

module SolutionDumetrulo2 =
  let rec isPrimeLoop (ra : ResizeArray<_>) i j limit =
    let incr x = if x = 2 then 3 else x + 2
    if i > limit then ra.ToArray ()
    elif j > i then
      ra.Add i
      isPrimeLoop ra (incr i) 2 limit
    elif i <> j && i % j = 0 then
      isPrimeLoop ra (incr i) 2 limit
    else
      isPrimeLoop ra i (incr j) limit

  let primes limit =
    isPrimeLoop (ResizeArray Common.estimate) 2 2 limit

module SolutionSimple =
  let rec isPrime i j k =
    if i < k then
      (j % i) <> 0 && isPrime (i + 2) j k
    else
      true

  let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
    if i < limit then 
      if isPrime 3 i i then
        ra.Add i
      isPrimeLoop ra (i + 2) limit
    else
      ra.ToArray ()

  let primes limit =
    let ra = ResizeArray Common.estimate
    ra.Add 2
    isPrimeLoop ra 3 limit

module SolutionPushStream =
  type Receiver<'T> = 'T -> bool
  type PushStream<'T> = Receiver<'T> -> bool

  module Details =
    module Loops =
      let rec range e r i =
        if i <= e then
          if r i then
            range e r (i + 1)
          else
            false
        else
          true

  open Details

  let range s e : PushStream<int> =
    fun r -> Loops.range e r s

  let filter p (t : PushStream<'T>) : PushStream<'T> =
    fun r -> t (fun v -> if p v then r v else true)

  let forall p (t : PushStream<'T>) : bool =
    t p

  let toArray (t : PushStream<'T>) : _ [] =
    let ra = ResizeArray 16

    t (fun v -> ra.Add v; true) |> ignore

    ra.ToArray ()

  let primes limit =
    range 2 limit
    |> filter (fun i -> range 2 (i - 1) |> forall (fun j -> i % j <> 0))
    |> toArray

module SolutionUnstalling =
  let rec isPrime i j k =
    if i + 6 < k then
      (j % i) <> 0 && (j % (i + 2)) <> 0 && (j % (i + 4)) <> 0 && (j % (i + 6)) <> 0  && isPrime (i + 8) j k
    else
      true

  let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
    if i < limit then 
      if isPrime 3 i i then
        ra.Add i
      isPrimeLoop ra (i + 2) limit
    else
      ra.ToArray ()

  let primes limit =
    let ra = ResizeArray Common.estimate
    ra.Add 2
    ra.Add 3
    ra.Add 5
    ra.Add 7
    ra.Add 11
    ra.Add 13
    ra.Add 17
    ra.Add 19
    ra.Add 23
    isPrimeLoop ra 29 limit

module SolutionVectors =
  open System.Numerics

  assert (Vector<int>.Count = 4)

  type I4 = Vector<int>

  let inline (%%) (i : I4) (j : I4) : I4 =
    i - (j * (i / j))

  let init : int [] = Array.zeroCreate 4

  let i4 v0 v1 v2 v3 =
    init.[0] <- v0
    init.[1] <- v1
    init.[2] <- v2
    init.[3] <- v3
    I4 init

  let i4_ (v0 : int) =
    I4 v0

  let zero    = I4.Zero
  let one     = I4.One 
  let two     = one + one
  let eight   = two*two*two

  let step = i4 3 5 7 9

  let rec isPrime (i : I4) (j : I4) k l =
    if l + 6 < k then
      Vector.EqualsAny (j %% i, zero) |> not && isPrime (i + eight) j k (l + 8)
    else
      true

  let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
    if i < limit then 
      if isPrime step (i4_ i) i 3 then
        ra.Add i
      isPrimeLoop ra (i + 2) limit
    else
      ra.ToArray ()

  let primes limit =
    let ra = ResizeArray Common.estimate
    ra.Add 2
    ra.Add 3
    ra.Add 5
    ra.Add 7
    ra.Add 11
    ra.Add 13
    ra.Add 17
    ra.Add 19
    ra.Add 23
    isPrimeLoop ra 29 limit

module SolutionVectorsUnstalling =
  open System.Numerics

  assert (Vector<int>.Count = 4)

  type I4 = Vector<int>

  let init : int [] = Array.zeroCreate 4

  let i4 v0 v1 v2 v3 =
    init.[0] <- v0
    init.[1] <- v1
    init.[2] <- v2
    init.[3] <- v3
    I4 init

  let i4_ (v0 : int) =
    I4 v0

  let zero    = I4.Zero
  let one     = I4.One 
  let two     = one + one
  let eight   = two*two*two
  let sixteen = two*eight

  let step = i4 3 5 7 9

  let rec isPrime (i : I4) (j : I4) k l =
    if l + 14 < k then
      // i - (j * (i / j))      
      let i0 = i
      let i8 = i + eight
      let d0 = j / i0
      let d8 = j / i8
      let n0 = i0 * d0
      let n8 = i8 * d8
      let r0 = j - n0
      let r8 = j - n8
      Vector.EqualsAny (r0, zero) |> not && Vector.EqualsAny (r8, zero) |> not && isPrime (i + sixteen) j k (l + 16)
    else
      true

  let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
    if i < limit then 
      if isPrime step (i4_ i) i 3 then
        ra.Add i
      isPrimeLoop ra (i + 2) limit
    else
      ra.ToArray ()

  let primes limit =
    let ra = ResizeArray Common.estimate
    ra.Add 2
    ra.Add 3
    ra.Add 5
    ra.Add 7
    ra.Add 11
    ra.Add 13
    ra.Add 17
    ra.Add 19
    ra.Add 23
    isPrimeLoop ra 29 limit

module SolutionBestAttempt =
  let rec isPrime i j k =
    if i < k then
      (j % i) <> 0 && isPrime (i + 2) j k
    else
      true

  let inline isqrt i = (i |> float |> sqrt) + 1. |> int

  let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
    if i < limit then 
      if isPrime 3 i (isqrt i) then
        ra.Add i
      isPrimeLoop ra (i + 2) limit
    else
      ra.ToArray ()

  let primes limit =
    let ra = ResizeArray Common.estimate
    ra.Add 2
    isPrimeLoop ra 3 limit

[<EntryPoint>]
let main argv =

  let testCases =
    [|
      "Original"    , Original.primes
      "Aaron"       , SolutionAaron.primes
      "SteveJ"      , SolutionSteveJ.primes
      "Dumetrulo1"  , SolutionDumetrulo1.primes
      "Dumetrulo2"  , SolutionDumetrulo2.primes
      "Simple"            , SolutionSimple.primes
      "PushStream"        , SolutionPushStream.primes
      "Unstalling"        , SolutionUnstalling.primes
      "Vectors"           , SolutionVectors.primes
      "VectorsUnstalling" , SolutionVectors.primes
      "BestAttempt"       , SolutionBestAttempt.primes
    |]

  do
    // Warm-up
    printfn "Warm up"
    for _, a in testCases do
      for i = 0 to 100 do
        a 100 |> ignore

  do
    let init ()   = Common.limit

    let expected  = SolutionSimple.primes Common.limit

    for testCase, a in testCases do
      printfn "Running '%s' with %d (%d)..." testCase Common.limit Common.estimate
      let actual, time, cc0, cc1, cc2 = Common.time init a
      let result = if expected = actual then "GOOD" else "BAD"
      printfn "  ... it took %d ms with (%d, %d, %d) cc and produces %d %s primes" time cc0 cc1 cc2 actual.Length result 

  0

【讨论】:

  • 您在哪个 CPU 上进行了测试? Skylake 在每个时钟上比 Atom 完成的工作更多,在某些情况下,您可以在一个 CPU 上让代码 A 更快,而在另一个 CPU 上让代码 B 更快。
  • “卸载”是否只是展开多个累加器?顺便说一句,x86 SSE2 / AVX2 / AVX512 没有整数除法,只有 FP 除法。因此,您正在使用的向量整数除法本身就是模拟的,可能通过将 divpd 解压缩到 double,除非除数是编译时常量。 (例如,请参阅 SSE integer division? 了解使用转换 float 和 C 内在函数的 int16 向量。)编译时间常数除数可以使用乘法逆技巧。
  • 原始 F# 代码花费了 14533 毫秒,原始 C# 代码花费了 1343 毫秒,当打印被删除时。鉴于完成的工作量存在差异,这更符合我的预期。我在回答中提到,我很惊讶 F# 代码快了三倍,您的基准测试给了我解释。 C# 代码因打印到标准输出而出现瓶颈,当消除该瓶颈时,breaking C# 代码的运行速度比非breaking F# 代码快约 11 倍。这更符合我的预期。很好的答案!
【解决方案4】:

如果您想要一个完全等效于 C# 中 for 循环的迭代 F# 函数,您可以使用以下尾递归函数:

let rec isPrimeLoop i j limit =
    if i > limit then ()
    elif j > i then
        stdout.WriteLine (string i)
        isPrimeLoop (i + 1) 2 limit
    elif i <> j && i % j = 0 then
        isPrimeLoop (i + 1) 2 limit
    else
        isPrimeLoop i (j + 1) limit

如您所见,由于它调用自身的方式,不再需要 isPrime 标志。代替嵌套的 for 循环,调用如下:

let sw = System.Diagnostics.Stopwatch.StartNew ()
isPrimeLoop 2 2 100000
sw.Stop ()
printfn "Elapsed time: %ims" sw.ElapsedMilliseconds

PS:你可以通过只检查 2 之后的奇数来显着缩短时间:

let rec isPrimeLoop i j limit =
    let incr x = if x = 2 then 3 else x + 2
    if i > limit then ()
    elif j > i then
        stdout.WriteLine (string i)
        isPrimeLoop (incr i) 2 limit
    elif i <> j && i % j = 0 then
        isPrimeLoop (incr i) 2 limit
    else
        isPrimeLoop i (incr j) limit

【讨论】:

    猜你喜欢
    • 2015-06-09
    • 2013-02-01
    • 2013-06-05
    • 2019-12-05
    • 2019-06-26
    • 1970-01-01
    • 1970-01-01
    • 2011-08-31
    • 1970-01-01
    相关资源
    最近更新 更多