简体   繁体   中英

Bad F# code performance on simple loop compared to C# - Why?

I was wondering why I get such disparate results between apparently equal algorithms in C# and F#.

F# code variants:

open System
{ 1I..(bigint (Int32.MaxValue / 100)) } |> Seq.sum

let mutable sum = 0I
for i in 1I..(bigint (Int32.MaxValue / 100)) do
    sum <- sum + i
sum

let sum = ref 0I
for i in 1I..(bigint (Int32.MaxValue / 100)) do
    sum := !sum + i
sum

Full F# code (4s):

[<EntryPoint>]
let main argv = 
    let sw = new Stopwatch()
    sw.Start()
    printfn "%A" ({ 1I..(bigint (Int32.MaxValue / 100)) } |> Seq.sum)
    sw.Stop()
    printfn "took %A" sw.Elapsed
    Console.ReadKey() |> ignore
    0

Full C# code (22s):

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    BigInteger sum = new BigInteger(0);
    BigInteger max = new BigInteger(Int32.MaxValue / 100);
    Console.WriteLine(max);
    for (BigInteger i = new BigInteger(1); i <= max; ++i)
    {
        sum += i;
    }
    sw.Stop();
    Console.WriteLine(sum);
    Console.WriteLine(sw.Elapsed);
    Console.ReadKey();
}

The F# code takes more than 22s on any of its variants (I assumed different implementations would yield different running times but that doesn't seem to be the case). On the other hand, the C# code seems to be way faster. Both yield the same final sum result, so I guess the algorithms are equivalent. I double checked and the F# code seems to be compiled with the --optimize+ flag.

Am I doing something wrong?

Converting the F# code from

{ 1I..(bigint (Int32.MaxValue / 100)) } |> Seq.sum;;
Real: 00:00:14.014, CPU: 00:00:14.196, GC gen0: 1743, gen1: 0

to

let mutable t = 1I
let mutable res = 0I
let max = bigint (Int32.MaxValue / 100)
while t < max do             
    res <- res + t
    t <- t + 1I;;
Real: 00:00:05.379, CPU: 00:00:05.450, GC gen0: 748, gen1: 0

Approxiamately triples the speed, and is also closer to the original C# code.

The most likely reason is that both {...} and for i in ... create a dummy seq . By removing this, you avoid the seq overhead.

EDIT

For some reason F# is generating a ridiculous amount of IL for this code, and is using a really weird comparison.

If we explicitly force the comparison, the speed doubles (which is a little ridiculous)

This code gets pretty much exactly the same speed as the C# for me (on mono).

let mutable t = 1I
let mutable res = 0I
let max = (bigint (Int32.MaxValue / 100));;
while System.Numerics.BigInteger.op_GreaterThan(max,t) do             
     res <- res + t;t<-System.Numerics.BigInteger.op_Increment(t) 
printfn "%A" res

but is unnecersarrily verbose.

I will probably file a compiler bug about this.

This is the fastest/shortest functional version I could come up with - it cheats a little by using a sequence of ints. It's roughly as fast as John Palmer's version on Mono.

{1..(System.Int32.MaxValue/100)} |> Seq.sumBy (fun x -> bigint(x)) |> printfn "%A"

I also made a functional version of what John Palmer did with one exception in that it includes the max value in the sum to match the above sequence based version:

let rec sum (cnt:bigint) (acc:bigint) (max:bigint) =
    if bigint.op_LessThanOrEqual(cnt,max) then
        sum (bigint.op_Increment(cnt)) (acc+cnt) max
    else
        acc

sum 1I 0I (bigint (System.Int32.MaxValue / 100)) |> printfn "%A" 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM