【问题标题】:F# vs C# performance for prime number generator素数生成器的 F# vs C# 性能
【发布时间】:2016-06-29 03:13:47
【问题描述】:

我注意到 F# 和 C# 中看似相同的代码执行起来并不相同。 F# 慢了一个数量级。作为一个例子,我提供了在 F# 和 C# 中生成素数/给出第 n 个素数的代码。我的 F# 代码是:

let rec isprime x =
primes
|> Seq.takeWhile (fun i -> i*i <= x)
|> Seq.forall (fun i -> x%i <> 0)

and primes = 
    seq {
        yield 2
        yield! (Seq.unfold (fun i -> Some(i, i+2)) 3)
                |> Seq.filter isprime
    }


let n = 1000
let start = System.DateTime.Now
printfn "%d" (primes |> Seq.nth n)
let duration = System.DateTime.Now - start
printfn "Elapsed Time: "
System.Console.WriteLine duration

而 C# 看起来像这样:

class Program
{
    static bool isprime(int n)
    {
        foreach (int p in primes())
        {
            if (p * p > n)
                return true;
            if (n % p == 0)
                return false;
        }
        return true;
    }

    static IEnumerable<int> primes()
    {
        yield return 2;
        for (int i=3; ; i+=2)
        {
            if (isprime(i))
                yield return i;
        }
    }

    static void Main(string[] args)
    {
        int n = 1000;
        var pr = primes().GetEnumerator();
        DateTime start = DateTime.Now;
        for (int count=0; count<n; count++)
        {
            pr.MoveNext();
        }
        Console.WriteLine(pr.Current);
        DateTime end = DateTime.Now;
        Console.WriteLine("Duration " + (end - start));
    }
}

当我测量不同的n 时,C# 的优势至少为 7 倍,如下所示:

  • n= 100:C#=5milsec F#=64milsec
  • n= 1000: C#=22milsec F#=180milsec
  • n= 5000: C#=280milsec F#=2.05sec
  • n=10000: C#=960milsec F#=6.95sec

我的问题:

  • 这两个程序是否等效?
  • 如果是,为什么不将它们编译到相同/等效的 CLI 中?
  • 如果没有,为什么不呢?
  • 我如何/我可以改进我的 F# 素数生成器,使其性能与 C# 更相似?
  • 一般来说,我能否(或者为什么我不能)总是在 F# 中模仿 C# 代码,这样我的 F# 代码的执行速度也同样快?

Edit1:我已经意识到算法本身可以通过仅遍历 isprime 中的奇数而不是质数来改进,使其成为非递归的,但这与所提出的问题有点垂直:)

【问题讨论】:

  • 一个明显的区别是 takewhile 和 forall 可能会涉及两次数据传递,这可能是灾难性的,特别是如果序列没有被缓存。
  • 这是一个非常好的收获!我认为编译器会优化它,但第二个想法我发现它不会。那么结合 Seq.takeWhile 和 Seq.forall 有什么好的方法呢?
  • 如果你想发疯,试试stackoverflow.com/questions/12014224/…
  • 另外,不要使用日期时间,使用秒表
  • @JohnPalmer - 我很好奇你所说的“两次通过”是什么意思 - primes 每次调用 isprime 只会被迭代一次(可以通过添加一些 printfs 来看出) )。

标签: c# performance f# primes


【解决方案1】:

这个:

这两个程序是等价的吗?

有点哲学问题。

在我看来,isprime 的 C# 和 F# 实现的输出对于任何给定的x 总是一致的,所以从这个意义上说,它们是等价的。但是,在您实现它们的方式方面存在许多差异(例如,Seq.unfold 将创建一个中间 IEnumerable&lt;_&gt; 值,然后 Seq.filter 将创建另一个值,因此您将生成更多的短期对象并在 F# 代码中使用更多的函数调用),因此它们在相应编译器生成的低级指令方面并不等效也就不足为奇了。

如果您愿意,您可以创建与 C# 代码更相似的 F# 代码,但代价是命令性更强、惯用语更少:

let rec primes = 
    seq {
        yield 2
        let mutable x = 3
        while true do
            if isprime x then 
                yield x
            x <- x + 2
    }
and isprime x =
    use e = primes.GetEnumerator()
    let rec loop() =
        if e.MoveNext() then
            let p = e.Current
            if p * p > x then true
            elif x % p = 0 then false
            else loop()
        else true            
    loop()

primes |&gt; Seq.item 5000 在我的机器上使用这个实现大约需要 0.6 秒,而你的实现大约需要 2.7 秒。我认为一般来说,F#seq 表达式的代码生成通常比 C# 迭代器的代码生成稍差,所以如果 C# 的运行速度仍然更快,我不会感到惊讶。 (但也请注意,有些习语在 F# 中最终比在 C# 中更快,因此 F# 并不总是较慢 - 根据我的经验,这两种语言总体上相当,而且我发现编写 F# 代码更有趣)。

在任何情况下,我都建议您寻找算法改进,而不是费尽心思研究如何使 F# 编译器的输出更接近 C# 编译器的输出。例如,只需在 primes 的原始定义末尾调用 Seq.cache 即可使 primes |&gt; Seq.item 5000 在我的机器上只需 0.062 秒,这比原始 C# 快得多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-11
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    相关资源
    最近更新 更多