素数生成器

时间:2016-03-14 07:12:21

标签: c# performance f# primes

我注意到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时,我获得至少7倍的C#优势如下:

  • 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中的奇数和非素数来改进,使其成为非递归的,但这与所提出的问题是一个垂直的事实:)

1 个答案:

答案 0 :(得分:18)

此:

  

这两个程序是否相同?

是一个哲学问题。

在我看来,isprime的C#和F#实现的输出将始终同意任何给定的x,因此从这个意义上说它们是等效的。但是,在实施方式方面存在许多差异(例如Seq.unfold会创建中间IEnumerable<_>值,然后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 |> Seq.item 5000在我的机器上大约需要0.6秒,而实施时大约需要2.7秒。我认为一般来说,F#seq表达式的代码生成通常比C#迭代器的代码生成稍差,所以如果C#运行得更快,我也不会感到惊讶。 (但是请注意,有些成语在F#中比在C#中更快,所以F#并不总是更慢 - 根据我的经验,这两种语言总体上相当,我发现编写F#代码更加愉快)。

在任何情况下,我都不建议如何使F#编译器的输出更接近C#编译器的细节,而是建议寻找算法改进。例如,只需在Seq.cache原始定义的末尾拨打primes即可使primes |> Seq.item 5000在我的计算机上只需0.062秒,这比原始C#快得多。< / p>