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?
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;
}
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.
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
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.
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.
Généralement non. Ils compilent tous les deux en code octet CLR (Common Language Runtime). Ceci est similaire à une JVM (machine virtuelle Java).