为什么F#比C#慢得多? (素数基准)

时间:2018-07-23 15:22:57

标签: c# f# benchmarking primes

我认为F#应该比C#快,我制作了一个可能很差的基准测试工具,C#获得了16239毫秒,而F#却恶化了49583毫秒。有人可以解释为什么会这样吗?我正在考虑离开F#并回到C#。可以用更快的代码在F#中获得相同的结果吗?

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

F#(49583ms)

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#(16239ms)

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();
        }
    }
}

4 个答案:

答案 0 :(得分:17)

F#程序较慢,因为您的程序不相同。您的C#代码在内部break循环中有一个for语句,但是您的F#程序则没有。因此,对于每个偶数,C#代码将在除以2之后检查除数,而F#程序将检查从2到i的每个数字。由于工作差异如此之大,实际上令人惊讶的是,F#代码的运行速度仅慢了三倍!

现在,F#故意没有break语句,因此您不能完全将C#代码直接转换为F#。但是您可以使用包含短路逻辑的功能。例如,在评论中,亚伦·埃希巴赫(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减1(含)的惰性序列。 (或者,您可以将其视为从2开始一直上升到i,但不包括i)。然后,它检查该序列的每个项目是否满足特定条件(即Seq.forall函数)。并且,作为Seq.forall的实现细节,因为它很懒,并且没有超出绝对的工作量,所以一旦发现false的结果,它将停止并且不再经历输入序列。 (因为一旦找到一个反例,forall函数就不再可能返回true,因此一旦知道其结果,它就会停止。)而{{ 1}}?是Seq.forall。因此,fun j -> i % j <> 0是内部循环变量,j是外部变量(在i部分分配的变量),其逻辑与您的C#循环相同。

现在,请记住,我们在Seq.filter内。因此,如果Seq.filter返回true,则Seq.forall将保留Seq.filter的值。但是,如果i返回false,那么Seq.forall会丢弃Seq.filter的值,直到进行下一步。

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

i

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

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

如果您不熟悉F#,那么for number in inputSoFar do printfn "%i" number 调用对您来说可能看起来很不寻常。这是currying,这是一个非常有用的概念,并且很重要。因此,花点时间思考一下:在F#中,以下两行代码是完全等效的

(printfn "%i")

因此(fun y -> someFunctionCall x y) (someFunctionCall x) 总是可以用fun item -> printfn "%i" item代替。 printfn "%i等效于Seq.iter循环:

for

完全等同于:

inputSoFar |> Seq.iter (someFunctionCall x)

所以您知道了:为什么您的F#程序变慢,以及如何编写遵循与C#相同的逻辑,但其中包含一个for item in inputSoFar do someFunctionCall x item 语句的F#程序

答案 1 :(得分:9)

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

这些年来,做了很多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也出乎意料地相似。

答案 2 :(得分:7)

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

此外,您正在将值打印为标准输出。在进行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#代码更改为不使用stdout
  2. Original(C#)-OP将原始C#代码更改为不使用stdout
  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#中的
  4. Seq增加了可观的开销。即使开销无关紧要,我也很难使自己使用它。
  5. 现代CPU指令具有不同的吞吐量和延迟编号。这意味着有时为了提高性能,需要在内部循环中处理多个独立样本,以允许乱序执行单元对程序重新排序以隐藏延迟。如果您的CPU具有超线程,并且您在多个线程上运行该算法,则超线程可以“自动”减轻延迟。
  6. 向量mod的缺乏阻止了使用SIMD获得非SIMD解决方案性能的尝试。
  7. 如果我修改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

答案 3 :(得分: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