Why is there a performance difference between LINQ (C #) and Seq (f #)

I made very simple test programs C # and 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

I made a second version from F #, which works similarly to 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 

but the result has not changed much (1650 ms)

I do not understand the big difference in speed between the two languages.

Two programs have very similar IL code, both use IEnumerable, with F # replacing a function call with an operation.

I rewrote C # code based on f # IL code.

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

The IL code of the two programs is the same, but the speed is still very different. I found IL difference thanks to Foggy Finder

Slow code

 [CompilationMapping(SourceConstructFlags.Module)] public static class Program { [Serializable] internal class seqb@18 : FSharpFunc<int, int> { internal seqb@18() { } public override int Invoke(int n) { return n; } } [CompilationMapping(SourceConstructFlags.Value)] public static IEnumerable<int> seqb { get { return $Program.seqb@18; } } 

Quick code

 [CompilationMapping(SourceConstructFlags.Module)] public static class Program { [CompilationMapping(SourceConstructFlags.Value)] public static int[] seqb { get { return $Program.seqb@20; } } 
+8
performance c # linq f #
source share
1 answer

The main reason the OP sees a performance difference is because Seq.init in F # is slow. The reason for this is that at each iteration, Seq.upto (which uses Seq.init ) allocates a new Lazy<_> object. You can see it in Seq source . If you have a low utility function, for example fun n -> n % 2 , the cost of a new Lazy<_> object, as well as its evaluation (locking and unlocking mutexes) takes a considerable amount of time.

The second reason OP sees performance differences is because Seq in F # is generally slow. This is fixed by manofstick in this PR

With PR inplace F # Seq will work very well compared to alternatives that exist (some details here )

With all of this in mind, I prepared several comparisons of the various ways the user posted calculations (other than the obvious 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 

Corresponding C # code:

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

Performance indicators on my computer (Intel Core I5) running on .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 does the worst and also consumes memory. If both the F # and C # codes use LINQ, then there is no real difference as expected. Nessos is a high-performance data pipeline for F # (and C #), which is significantly better.

The hard coding for loop makes everything better, and the fastest solution is to use SSE through System.Numerics.Vectors . Unfortunately, System.Numerics.Vectors does not support % , which makes the comparison a little unfair.

Thus, the difference lies not so much in the language problem as in the problem with the library.

+8
source share

All Articles