Pourquoi C # exécute-t-il les mathématiques.Sqrt() plus lentement que VB.NET?

Contexte

En exécutant des tests de référence ce matin, mes collègues et moi avons découvert des choses étranges concernant les performances du code C # vs. VB.NET code.

Nous avons commencé à comparer C # VS Delphi Prism calcul des nombres premiers, et a constaté que Prism était environ 30% plus rapide. J'ai pensé que CodeGear optimisait davantage le code lors de la génération D'IL (le exe était environ deux fois plus gros que C#et avait toutes sortes d'IL différents.)

J'ai décidé d'écrire un test dans VB.NET as Eh bien, en supposant que les compilateurs de Microsoft finiraient par écrire essentiellement le même IL pour chaque langue. Cependant, le résultat était plus choquant: le code a couru plus de trois fois plus lentement sur C# que VB avec la même opération!

L'IL généré était différent, mais pas extrêmement, et je ne suis pas assez bon à le lire pour comprendre les différences.

Repères

J'ai inclus le code pour chacun ci-dessous. Sur ma machine, VB trouve 348513 nombres premiers dans environ 6.36 secondes. C# trouve le même nombre de nombres premiers dans 21.76 secondes.

Spécifications et Notes de L'ordinateur

  • Intel Core 2 Quad 6600 @ 2.4 Ghz

Chaque machine sur laquelle j'ai testé il y a une différence notable dans les résultats de référence entre C# et VB.NET.

Les deux applications de la console ont été compilées en mode Release, mais sinon aucun paramètre de projet n'a été modifié par rapport aux paramètres par défaut générés par Visual Studio 2008.

VB.NET code

Imports System.Diagnostics

Module Module1

    Private temp As List(Of Int32)
    Private sw As Stopwatch
    Private totalSeconds As Double

    Sub Main()
        serialCalc()
    End Sub

    Private Sub serialCalc()
        temp = New List(Of Int32)()
        sw = Stopwatch.StartNew()
        For i As Int32 = 2 To 5000000
            testIfPrimeSerial(i)
        Next
        sw.Stop()
        totalSeconds = sw.Elapsed.TotalSeconds
        Console.WriteLine(String.Format("{0} seconds elapsed.", totalSeconds))
        Console.WriteLine(String.Format("{0} primes found.", temp.Count))
        Console.ReadKey()
    End Sub

    Private Sub testIfPrimeSerial(ByVal suspectPrime As Int32)
        For i As Int32 = 2 To Math.Sqrt(suspectPrime)
            If (suspectPrime Mod i = 0) Then
                Exit Sub
            End If
        Next
        temp.Add(suspectPrime)
    End Sub

End Module

Code C #

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

namespace FindPrimesCSharp {
    class Program {
        List<Int32> temp = new List<Int32>();
        Stopwatch sw;
        double totalSeconds;


        static void Main(string[] args) {

            new Program().serialCalc();

        }


        private void serialCalc() {
            temp = new List<Int32>();
            sw = Stopwatch.StartNew();
            for (Int32 i = 2; i <= 5000000; i++) {
                testIfPrimeSerial(i);
            }
            sw.Stop();
            totalSeconds = sw.Elapsed.TotalSeconds;
            Console.WriteLine(string.Format("{0} seconds elapsed.", totalSeconds));
            Console.WriteLine(string.Format("{0} primes found.", temp.Count));
            Console.ReadKey();
        }

        private void testIfPrimeSerial(Int32 suspectPrime) {
            for (Int32 i = 2; i <= Math.Sqrt(suspectPrime); i++) {
                if (suspectPrime % i == 0)
                    return;
            }
            temp.Add(suspectPrime);
        }

    }
}

Pourquoi l'exécution de C#de Math.Sqrt() est-elle plus lente que VB.NET?

49
demandé sur Ry- 2010-06-12 00:22:05

6 réponses

L'implémentation C# recalcule Math.Sqrt(suspectPrime) chaque fois dans la boucle, tandis que VB ne la calcule qu'au début de la boucle. Ceci est simplement dû à la nature de la structure de contrôle. En C#, for est juste une boucle while de fantaisie, tandis que dans VB c'est une construction séparée.

L'utilisation de cette boucle égalisera le score:

        Int32 sqrt = (int)Math.Sqrt(suspectPrime)
        for (Int32 i = 2; i <= sqrt; i++) { 
            if (suspectPrime % i == 0) 
                return; 
        }
81
répondu Gabe 2010-06-11 20:36:38

Je suis d'accord avec l'affirmation selon laquelle le code C# calcule sqrt à chaque itération et voici la preuve tout droit sortie de Reflector:

Version VB:

private static void testIfPrimeSerial(int suspectPrime)
{
    int VB$t_i4$L0 = (int) Math.Round(Math.Sqrt((double) suspectPrime));
    for (int i = 2; i <= VB$t_i4$L0; i++)
    {
        if ((suspectPrime % i) == 0)
        {
            return;
        }
    }
    temp.Add(suspectPrime);
}

Version C#:

 private void testIfPrimeSerial(int suspectPrime)
{
    for (int i = 2; i <= Math.Sqrt((double) suspectPrime); i++)
    {
        if ((suspectPrime % i) == 0)
        {
            return;
        }
    }
    this.temp.Add(suspectPrime);
}

Qui pointe un peu vers le code de génération de VB qui fonctionne mieux même si le développeur est assez naïf pour avoir l'appel à sqrt dans la définition de la boucle.

11
répondu Otávio Décio 2010-06-11 20:34:12

Voici l'IL décompilé des boucles for. Si vous comparez les deux vous verrez VB.Net seulement les Math.Sqrt(...) onces pendant que C # le vérifie à chaque passage. Pour résoudre cette question, vous devez faire quelque chose comme var sqrt = (int)Math.Sqrt(suspectPrime);, comme d'autres l'ont suggéré.

... VB ...

.method private static void CheckPrime(int32 suspectPrime) cil managed
{
    // Code size       34 (0x22)
    .maxstack  2
    .locals init ([0] int32 i,
         [1] int32 VB$t_i4$L0)
    IL_0000:  ldc.i4.2
    IL_0001:  ldarg.0
    IL_0002:  conv.r8
    IL_0003:  call       float64 [mscorlib]System.Math::Sqrt(float64)
    IL_0008:  call       float64 [mscorlib]System.Math::Round(float64)
    IL_000d:  conv.ovf.i4
    IL_000e:  stloc.1
    IL_000f:  stloc.0
    IL_0010:  br.s       IL_001d

    IL_0012:  ldarg.0
    IL_0013:  ldloc.0
    IL_0014:  rem
    IL_0015:  ldc.i4.0
    IL_0016:  bne.un.s   IL_0019

    IL_0018:  ret

    IL_0019:  ldloc.0
    IL_001a:  ldc.i4.1
    IL_001b:  add.ovf
    IL_001c:  stloc.0
    IL_001d:  ldloc.0
    IL_001e:  ldloc.1
    IL_001f:  ble.s      IL_0012

    IL_0021:  ret
} // end of method Module1::testIfPrimeSerial

... C# ...

.method private hidebysig static void CheckPrime(int32 suspectPrime) cil managed
{
    // Code size       26 (0x1a)
    .maxstack  2
    .locals init ([0] int32 i)
    IL_0000:  ldc.i4.2
    IL_0001:  stloc.0
    IL_0002:  br.s       IL_000e

    IL_0004:  ldarg.0
    IL_0005:  ldloc.0
    IL_0006:  rem
    IL_0007:  brtrue.s   IL_000a

    IL_0009:  ret

    IL_000a:  ldloc.0
    IL_000b:  ldc.i4.1
    IL_000c:  add
    IL_000d:  stloc.0
    IL_000e:  ldloc.0
    IL_000f:  conv.r8
    IL_0010:  ldarg.0
    IL_0011:  conv.r8
    IL_0012:  call       float64 [mscorlib]System.Math::Sqrt(float64)
    IL_0017:  ble.s      IL_0004

    IL_0019:  ret
} // end of method Program::testIfPrimeSerial
9
répondu Matthew Whited 2010-06-11 20:50:24

Off sur une tangente, si vous êtes opérationnel avec VS2010, vous pouvez profiter de PLINQ et faire C# (probablement VB.Net aussi bien) plus vite.

Changez cela pour la boucle en...

var range = Enumerable.Range(2, 5000000);

range.AsParallel()
    .ForAll(i => testIfPrimeSerial(i));

Je suis passé de 7.4 -> 4.6 secondes sur ma machine. Le déplacer pour libérer le mode rase un peu plus de temps en plus de cela.

8
répondu 48klocs 2010-06-11 20:45:29

La différence est dans la boucle; votre code C # calcule la racine carrée à chaque itération. Changer cette ligne de:

for (Int32 i = 2; i <= Math.Sqrt(suspectPrime); i++) {

À:

var lim = Math.Sqrt(suspectPrime);
for (Int32 i = 2; i <= lim; i++) {

A fait passer le temps sur ma machine de 26 secondes à 7.quelque.

3
répondu tzaman 2010-06-11 20:31:41

Généralement non. Ils compilent tous les deux en code octet CLR (Common Language Runtime). Ceci est similaire à une JVM (machine virtuelle Java).

-1
répondu elsurudo 2010-06-11 22:33:35