Sammenligne kjøretider mellom F# og C#


torsdag 11. april 2013 C# F# Ytelse

Dette er en oppfølger til blogposten Litt F#-kode for å måle kjøretid, hvor jeg blant annet målte tiden på ulike løsninger på Euler1-problemet. Det kom som en stor overraskelse at den rekursive varianten var såpass mye raskere enn å bruke løkker og muterbare variabler.

Spørsmålet jeg da stod igjen med var hvor rask den rekursive F#-varianten var i forhold til en løsning i C# – det har jeg nå undersøkt, og det er disse resultatene jeg skal presentere her. Og mot slutten kommer det en overraskelse!

Et testprogram

Jeg valgte å lage et testprogram i C#. Programmet inneholder tre ulike varianter av Euler1: En som bruker en for-løkke, en som bruker Linq, og en crazy variant som bruker GOTO. I Main-metoden kjører jeg disse tre samt de fire orginale variantene fra F#-bloggposten.

public class Program
{
    public static void CSharpTest1()
    {
        long result = 0;
        for (int i = 0; i < 1000000; i++)
            if (i % 3 == 0 || i % 5 == 0)
                result += i;
        Console.WriteLine("\nResult by for loop (C#): " 
            + result);
    }
    public static void CSharpTest2()
    {
        Console.WriteLine("\nResult by Enumerable (C#): "
            + Enumerable.Range(1, 1000000)
                .Where(n => n % 3 == 0 || n % 5 == 0)
                .Select(Convert.ToInt64)
                .Sum());
    }

    public static void CSharpTest3()
    {
        int i = 0;
        long result = 0;
    Loop:
        if (++i > 1000000) 
            goto End;
        if (i % 3 == 0 || i % 5 == 0)
            result += i;
        goto Loop;
    End:
        Console.WriteLine("\nResult by goto (C#): "
            + result);
    }

    static void Main(string[] args)
    {
        Benchmark.Measure("C# for loop ", 
                          Program.CSharpTest1);
        Benchmark.Measure("C# sequence ", 
                          Program.CSharpTest2);
        Benchmark.Measure("C# goto     ", 
                          Program.CSharpTest3);
        Benchmark.Measure("F# mutable  ", 
                          MyFSharpProject.Program.test1);
        Benchmark.Measure("F# sequence ", 
                          MyFSharpProject.Program.test2);
        Benchmark.Measure("F# ref cell ", 
                          MyFSharpProject.Program.test3);
        Benchmark.Measure("F# recursion", 
                          MyFSharpProject.Program.test4);

        Console.ReadLine();
    }
}

Resultatet

Og nå er du vel spent? Kjøringen av dette testprogrammet gav følgende output, presentert som en graf:

Measurements

Resultatet er nedslående. Det er forsåvidt greit nok – og ikke uventet – at en rekursiv løsning er treigere enn en for-løkke i C#, selv om F# har tail call optimization. Men hvorfor er sekvens-løsningen i F# så veldig mye mere tidkrevende enn sekvens-løsningen i C#?

For sikkerhets skyld forsøkte jeg også å kjøre alle variantene fra en F# executable (man vet jo aldri), men det gav akkurat det samme resultatet.

Og det var da det slo meg: Jeg kompilerer testene mine i DEBUG modus! Kan det hende kjøretiden vil ble bedre i RELEASE? Jeg er ikke vandt til at det har så mye å si i C#, men kanskje det er anderledes i F#?

RELEASE-modus

Så jeg endret prosjektene til å kompilere i RELEASE-modus, og forsøkte C#-testprogrammet på nytt. Ingen merkbar endring!

Ok – hva om jeg tester fra F# igjen da? Og da ble resultatet anderledes!

Measurements2

I RELEASE-modus på F# runtime ble plutselig den rekursive algoritmen nøyaktig like rask som for-løkken i C#. Kompilatoren har altså sansynligvis vært i stand til å transformere rekursjonen til mer eller mindre den samme IL-koden som for-løkken. De andre variantene ble også betydelig raskere, selv om de fortsatt er et stykke fra den optimale løsningen.

Så det er på tide med en Note To Self: Mål alltid performance i RELEASE. Kompileringsmodus har mye å si for optimalisering av F#-kode.


comments powered by Disqus