2017-02-06 85 views
8

我做了非常簡單的C#和F#測試程序。爲什麼LINQ(c#)與Seq(f#)之間有性能差異

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 

namespace ConsoleApplication2 
{ 
    class Program 
    { 
     static int Remainder(int num) 
     { 
      return num % 2; 
     } 
     static int SumOfremainders(IEnumerable<int> list) 
     { 
      var sum = 0; 
      foreach (var num in list) 
      { 
       sum += Remainder(num); 
      } 
      return sum; 
     } 

     static void Main(string[] args) 
     { 
      Stopwatch sw = new Stopwatch(); 
      var nums = Enumerable.Range(1, 10000000); 
      sw.Start(); 
      var a = SumOfremainders(nums); 
      sw.Stop(); 
      Console.WriteLine("Duration " + (sw.ElapsedMilliseconds)); 
      Console.WriteLine("Sum of remainders: {0}", a); 
     } 
    } 
} 


let remainder x = x % 2 

let sumORemainders n = 
    n 
    |> Seq.map(fun n-> remainder n) 
    |> Seq.sum 

let seqb = Seq.init 10000000(fun n->n) 
let timer =System.Diagnostics.Stopwatch() 
timer.Start() 
let a =(sumORemainders seqb) 
timer.Stop() 
printfn "Elapsed Time: " 
System.Console.WriteLine timer.ElapsedMilliseconds 
printfn "Sum of squares of 1-100: %d" a 


[<EntryPoint>] 
let main argv = 

    0 // return an integer exit code 

C#71毫秒 F#1797毫秒

我從F#第二版本,其工作相似比C#

let remainder x = x % 2 

let sumORemainders (input:seq<int>) = 
    let mutable sum = 0 
    let en = input.GetEnumerator() 
    while (en.MoveNext()) do 
     sum <- sum + remainder en.Current 
    sum 


let seqb = Seq.init 10000000(fun n->n) 
let timer =System.Diagnostics.Stopwatch() 
timer.Start() 
let a =(sumORemainders seqb) 
timer.Stop() 
printfn "Elapsed Time: " 
System.Console.WriteLine timer.ElapsedMilliseconds 
printfn "Sum of squares of 1-100: %d" a 


[<EntryPoint>] 
let main argv = 

    0 // return an integer exit code 

但結果不是未顯著(1650ms)

改變我不明白這兩種語言在速度上的巨大差異。

這兩個程序具有非常相似的IL代碼,都使用IEnumerable,而且F#用函數替換函數調用。

我重寫了基於f#IL代碼的c#代碼。

static int SumOfremainders(IEnumerable<int> list) 
     { 
      var sum = 0; 
      IEnumerator<int> e = list.GetEnumerator(); 
      while (e.MoveNext()) 
      { 
       sum += e.Current % 2; 
      } 
      return sum; 
     } 

兩個程序的IL代碼是相同的,但速度還是有很大差別。 我發現IL差別感謝霧搜索

,慢速代碼

[CompilationMapping(SourceConstructFlags.Module)] 
public static class Program 
{ 
    [Serializable] 
    internal class [email protected] : FSharpFunc<int, int> 
    { 
     internal [email protected]() 
     { 
     } 

     public override int Invoke(int n) 
     { 
      return n; 
     } 
    } 

    [CompilationMapping(SourceConstructFlags.Value)] 
    public static IEnumerable<int> seqb 
    { 
     get 
     { 
      return [email protected]; 
     } 
    } 

快速代碼

[CompilationMapping(SourceConstructFlags.Module)] 
public static class Program 
{ 
    [CompilationMapping(SourceConstructFlags.Value)] 
     public static int[] seqb 
     { 
      get 
      { 
       return [email protected]; 
      } 
     } 
+2

我不同意這個問題是重複的。另一個問題是公開的,而這更具體。但是我認爲OP應該改寫這個問題來包含'Seq/LINQ',因爲這很可能是問題('LINQ'很慢'Seq'慢) – FuleSnabel

+0

這是因爲你的C#代碼和F#實際上是不同的。 –

+0

此:'讓seqb = Seq.init 10000000(樂趣N-> N)'不等於'VAR NUMS = Enumerable.Range(1,10000000);' –

回答

8

的主要原因OP看到的性能差異是因爲Seq.init在F#是緩慢的。其原因是在每個迭代Seq.upto(其中Seq.init使用)分配一個新的Lazy<_>對象。你可以在Seqsource中看到。 如果您的開銷函數較低,如fun n -> n % 2,則新對象的開銷以及對它的評估(互斥鎖定和解鎖)需要大量時間。

OP看到性能差異的第二個原因是F#中的Seq一般是緩慢的。這是由manofstick在此PR

補救隨着PR就地F#Seq會表現非常好,相比所存在的替代品(一些細節here

有了這一切說,我準備了不同的方式有些攀比做用戶發佈的計算(除了明顯的total/2)。

open CsPerfs 
open Nessos.Streams 
open System.Diagnostics 
open System.Linq 
open System.Numerics 


// now() returns current time in milliseconds since start 
let now : unit -> int64 = 
    let sw = Stopwatch() 
    sw.Start() 
    fun() -> sw.ElapsedMilliseconds 

// time estimates the time 'action' repeated a number of times 
let time repeat action = 
    let inline cc i  = System.GC.CollectionCount i 

    let v     = action() 

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

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

    for i in 1..repeat do 
    action() |> ignore 

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

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

[<EntryPoint>] 
let main argv = 
    let count = 10000000 

    let outers = 
    [| 
     1000 
    |] 

    for outer in outers do 
    let inner = count/outer 

    let fsImperativeTest() = 
     let mutable sum = 0 
     for n = 0 to inner-1 do 
     sum <- sum + n % 2 
     sum 

    let fsLinqTest() = 
     Enumerable.Range(0, inner).Select(fun n -> n % 2).Sum() 
    let fsNessosTest() = 
     Stream.initInfinite id 
     |> Stream.take inner 
     |> Stream.map (fun n -> n % 2) 
     |> Stream.sum 
    let fsOpTest() = 
     let remainder x = x % 2 
     let sumORemainders (input:seq<int>) = 
      let mutable sum = 0 
      use en = input.GetEnumerator() 
      while (en.MoveNext()) do 
       sum <- sum + remainder en.Current 
      sum 
     let seqb = Seq.init inner id 
     sumORemainders seqb 
    let fsSseTest() = 
     let inc   = Vector<int>.One 
     let one   = Vector<int>.One 
     let mutable sum = Vector<int>.Zero 
     let mutable n = Vector<int> [|0..3|] 
     for n4 = 0 to inner/4-1 do 
     n <- n + inc 
     sum <- sum + (n &&& one) 
     sum.[0] + sum.[1] + sum.[2] + sum.[3] 
    let fsSeqTest() = 
     Seq.init inner id 
     |> Seq.map (fun n -> n % 2) 
     |> Seq.sum 
    let fsSeqVariantTest() = 
     seq { for n = 0 to inner-1 do yield n } 
     |> Seq.map (fun n -> n % 2) 
     |> Seq.sum 

    let csImperativeTest = 
     let f = Perfs.CsImperativeTest inner 
     fun() -> f.Invoke() 
    let csLinqTest = 
     let f = Perfs.CsLinqTest inner 
     fun() -> f.Invoke() 
    let csOpTest = 
     let f = Perfs.CsOpTest inner 
     fun() -> f.Invoke() 

    let tests = 
     [| 
     "fsImperativeTest" , fsImperativeTest 
     "fsLinqTest"  , fsLinqTest 
     "fsNessosTest"  , fsNessosTest 
     "fsOpTest"   , fsOpTest 
     "fsSeqTest"   , fsSeqTest 
     "fsSeqVariantTest" , fsSeqVariantTest 
     "fsSseTest"   , fsSseTest 
     "csImperativeTest" , csImperativeTest 
     "csLinqTest"  , csLinqTest 
     "csOpTest"   , csOpTest 
     |] 

    printfn "Test run - total count: %d, outer: %d, inner: %d" count outer inner 

    for name, test in tests do 
     printfn "Running %s..." name 
     let v, ms, cc0, cc1, cc2 = time outer test 
     printfn " it took %d ms - collection count is %d,%d,%d - result is %A" ms cc0 cc1 cc2 v 


    0 

匹配C#代碼:

Test run - total count: 10000000, outer: 1000, inner: 10000 
Running fsImperativeTest... 
    it took 20 ms - collection count is 0,0,0 - result is 5000 
Running fsLinqTest... 
    it took 124 ms - collection count is 0,0,0 - result is 5000 
Running fsNessosTest... 
    it took 56 ms - collection count is 0,0,0 - result is 5000 
Running fsOpTest... 
    it took 1320 ms - collection count is 661,0,0 - result is 5000 
Running fsSeqTest... 
    it took 1477 ms - collection count is 661,0,0 - result is 5000 
Running fsSeqVariantTest... 
    it took 512 ms - collection count is 0,0,0 - result is 5000 
Running fsSseTest... 
    it took 2 ms - collection count is 0,0,0 - result is 5000 
Running csImperativeTest... 
    it took 19 ms - collection count is 0,0,0 - result is 5000 
Running csLinqTest... 
    it took 122 ms - collection count is 0,0,0 - result is 5000 
Running csOpTest... 
    it took 58 ms - collection count is 0,0,0 - result is 5000 
Press any key to continue . . . 

Seq做的:

namespace CsPerfs 
{ 
    using System; 
    using System.Collections.Generic; 
    using System.Linq; 

    public static class Perfs 
    { 
     static int Remainder(int num) 
     { 
      return num % 2; 
     } 

     static int SumOfremainders(IEnumerable<int> list) 
     { 
      var sum = 0; 
      foreach (var num in list) 
      { 
       sum += Remainder(num); 
      } 
      return sum; 
     } 

     public static Func<int> CsOpTest (int count) 
     { 
     return() => SumOfremainders (Enumerable.Range(1, count)); 
     } 

     public static Func<int> CsImperativeTest (int count) 
     { 
     return() => 
      { 
      var sum = 0; 
      for (var n = 0; n < count; ++n) 
      { 
       sum += n % 2; 
      } 
      return sum; 
      }; 
     } 

     public static Func<int> CsLinqTest (int count) 
     { 
     return() => Enumerable.Range (0, count).Select (n => n % 2).Sum(); 
     } 
    } 
} 

看到.NET運行4.6.1 64位我的機器(Intel酷睿i5)上的性能數字最壞的還會消耗記憶。如果F#和C#代碼都使用LINQ,則沒有預期的實際區別。 Nessos是F#(和C#)的高性能數據管道,性能明顯更好。

「硬編碼」一個for循環做更好和最快的解決辦法是通過System.Numerics.Vectors使用SSE。不幸的是System.Numerics.Vectors不支持%這使得比較有點不公平。

所以差別與其說是語言問題,而是一個庫的問題。

相關問題