51 votes

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

Arrière-plan

Lors de l'exécution de tests de référence, ce matin, mes collègues et j'ai découvert des choses étranges concernant l'exécution de code C# vs VB.NET code.

Nous avons commencé la comparaison de C# vs Delphi Prism calcul de nombres premiers, et a constaté que Prism est environ 30% plus rapide. J'ai pensé CodeGear code optimisé plus lors de la génération d'IL (l' exe a été environ deux fois plus grand que C#'s et a eu toutes sortes de différents IL in.)

J'ai décidé d'écrire un test en VB.NET ainsi, en supposant que Microsoft compilateurs finirait par écrit essentiellement les mêmes IL pour chaque langue. Cependant, le résultat a été plus choquant: le code a couru plus de trois fois plus lent sur le C# que VB avec la même opération!

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

Repères

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

Ordinateur Spécifications et des Notes

  • Intel Core 2 Quad 6600 @ 2.4 Ghz

Chaque machine que j'ai testé il y a une différence notable dans les résultats de l'analyse comparative entre C# et VB.NET.

Les deux applications de console ont été compilé en mode Release, mais sinon aucun projet de paramètres ont été modifiés de la valeur par défaut généré 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

Le 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 le C#'exécution de Math.Sqrt() plus lent que VB.NET?

82voto

Gabe Points 49718

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

Utiliser cette boucle va même augmenter le score:

         Int32 sqrt = (int)Math.Sqrt(suspectPrime)
        for (Int32 i = 2; i <= sqrt; i++) { 
            if (suspectPrime % i == 0) 
                return; 
        }
 

11voto

Otávio Décio Points 44200

Je suis d’accord avec l’affirmation selon laquelle le code C # est calculable à chaque itération et voici la preuve directement 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);
}
 

Ce qui pointe un peu vers un code générant 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.

9voto

Matthew Whited Points 12255

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

... 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
 

8voto

48klocs Points 3749

Si vous êtes prêt à utiliser VS2010, vous pouvez tirer parti de PLINQ et rendre C # (probablement aussi VB.Net) plus rapide.

Changer cette boucle pour ...

 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 en mode relâchement permet de gagner un peu plus de temps.

3voto

tzaman Points 13190

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++) {
 

laissé tomber le temps passé sur ma machine de 26 secondes à 7. quelque chose.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X