2017-02-06 24 views
8

Zrobiłem bardzo proste programy testowe C# i F #.Dlaczego występuje różnica wydajności między LINQ (C#) a 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 MS f # 1797 MS

że wykonane drugą wersję z F #, które działają podobnie niż 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 

ale wynik nie znacząco (1650ms)

zmienione Nie rozumiem dużej różnicy w szybkości między dwoma językami.

Dwa programy mają bardzo podobny kod IL, oba używają IEnumerable, ponadto F # zastępuje wywołanie funkcji operacją.

Przepiszę kod C# na podstawie kodu f # IL.

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

Kod IL dwóch programów jest taki sam, ale prędkość nadal jest bardzo różna. znalazłem Il difference dzięki za Foggy Finder

Powolne kodu

[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]; 
     } 
    } 

Szybko kodu

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

Nie zgadzam się, że pytanie jest duplikatem. Drugie pytanie jest otwarte, podczas gdy jest ono o wiele bardziej szczegółowe. Jednak uważam, że PO powinien przeformułować pytanie tak, aby zawierało "Seq/LINQ", ponieważ jest to bardziej prawdopodobne, że problem ("LINQ" jest powolny "Seq" jest wolniejszy) – FuleSnabel

+0

Wynika to z faktu, że kod C# i F # są w rzeczywistości różne. –

+0

to: 'let seqb = Seq.init 10000000 (zabawa n-> n)' nie jest równe 'var nums = Enumerable.Range (1, 10000000);' –

Odpowiedz

8

Głównym powodem PO widzi różnicy wydajności jest bo Seq.init w F # jest powolny. Powodem jest to, że w każdej iteracji Seq.upto (który używa Seq.init) przydziela nowy obiekt Lazy<_>. Możesz to zobaczyć w Seqsource. Jeśli masz niską funkcję narzutową, taką jak fun n -> n % 2, koszt nowego obiektu Lazy<_>, a także jego ocena (blokowanie i odblokowywanie muteksu) zajmuje znaczną ilość czasu.

Drugim powodem, dla którego OP widzi różnicę w wydajności, jest to, że Seq w F # jest ogólnie wolny. To jest naprawione przez manofstick w tym PR

Z INPLACE PR F # Seq będzie wykonywał bardzo dobrze w porównaniu do alternatywnych rozwiązań, które istnieje (pewne szczegóły here)

Z tym wszystkim powiedział, że przygotowano kilka porównań różnych sposobów wykonaj obliczenia opublikowane przez użytkownika (poza oczywistym 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 

Dopasowanie kod C#:

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

Liczby wydajności widoczne na moim komputerze (Intel Core i5) działa na .NET 4.6.1 64bit:

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 robi to najgorsze, a także pochłania pamięć. Jeśli oba kody F # i C# używają LINQ, nie ma żadnych rzeczywistych różnic, jak się spodziewano. Nessos to wydajny system przesyłania danych dla F # (i C#), który działa znacznie lepiej.

"Twardy kod" a dla pętli for jeszcze lepiej, a najszybszym rozwiązaniem jest użycie SSE poprzez System.Numerics.Vectors. Niestety System.Numerics.Vectors nie obsługuje %, co czyni porównanie nieco nieuczciwym.

Różnica polega nie tyle na problemie językowym, co na bibliotece.