¿Por qué C#ejecuta Math.Sqrt() más lentamente que VB.NET?
benchmarking (6)
Fondo
Mientras ejecutaba pruebas de referencia esta mañana, mis colegas y yo descubrimos algunas cosas extrañas sobre el rendimiento del código C # en comparación con el código VB.NET.
Comenzamos comparando C # contra Delphi Prism calculando números primos, y encontramos que Prism era aproximadamente 30% más rápido. Pensé que CodeGear optimizaba más el código cuando generaba IL (el exe
era aproximadamente dos veces más grande que C # y tenía todo tipo de IL diferentes).
Decidí escribir una prueba en VB.NET también, suponiendo que los compiladores de Microsoft terminarían escribiendo esencialmente el mismo IL para cada idioma. Sin embargo, el resultado fue más impactante: ¡ el código corrió más de tres veces más lento en C # que en VB con la misma operación!
El IL generado fue diferente, pero no extremadamente, y no soy lo suficientemente bueno para leerlo para entender las diferencias.
Puntos de referencia
He incluido el código para cada uno a continuación. En mi máquina, VB encuentra 348513 primos en aproximadamente 6,36 segundos. C # encuentra el mismo número de primos en 21.76 segundos.
Especificaciones y notas de la computadora
- Intel Core 2 Quad 6600 a 2.4Ghz
Cada máquina en la que he probado presenta una diferencia notable en los resultados de referencia entre C # y VB.NET.
Ambas aplicaciones de la consola se compilaron en modo de lanzamiento, pero de lo contrario no se cambiaron las configuraciones del proyecto de los valores predeterminados generados por Visual Studio 2008.
Código VB.NET
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
C # Code
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);
}
}
}
¿Por qué la ejecución de C # de Math.Sqrt()
más lenta que VB.NET?
Aquí está el IL decompilado de los bucles for. Si compara los dos, verá que VB.Net solo hace los Math.Sqrt(...)
onces mientras que C # lo comprueba en cada pase. Para arreglar esto, necesitarías hacer algo como var sqrt = (int)Math.Sqrt(suspectPrime);
como otros han sugerido.
... 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
Desactivado en una tangente, si está funcionando con VS2010, puede aprovechar PLINQ y hacer que C # (probablemente también VB.Net) sea más rápido.
Cambia eso por bucle para ...
var range = Enumerable.Range(2, 5000000);
range.AsParallel()
.ForAll(i => testIfPrimeSerial(i));
Pasé de 7.4 -> 4.6 segundos en mi máquina. Moverlo al modo de lanzamiento se afeita un poco más de tiempo además de eso.
Estoy de acuerdo con la afirmación de que el código C # está computando sqrt en cada iteración y aquí está la prueba directamente de Reflector:
Versión 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);
}
Versión 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);
}
Lo que indica que VB genera un código que funciona mejor incluso si el desarrollador es lo suficientemente ingenuo como para llamar a sqrt en la definición del bucle.
Generalmente no Ambos compilan a CLR (Common Language Runtime) byte-code. Esto es similar a una JVM (Java Virtual Machine).
La diferencia está en el ciclo; su código C # está calculando la raíz cuadrada en cada iteración. Cambiando esa línea de:
for (Int32 i = 2; i <= Math.Sqrt(suspectPrime); i++) {
a:
var lim = Math.Sqrt(suspectPrime);
for (Int32 i = 2; i <= lim; i++) {
redujo el tiempo en mi máquina de 26 segundos a 7. algo.
La implementación de C # está recalculando Math.Sqrt(suspectPrime)
cada vez a través del ciclo, mientras que VB solo lo calcula al comienzo del ciclo. Esto solo se debe a la naturaleza de la estructura de control. En C #, for
es solo un bucle de fantasía, mientras que en VB es una construcción separada.
Usar este ciclo igualará el puntaje:
Int32 sqrt = (int)Math.Sqrt(suspectPrime)
for (Int32 i = 2; i <= sqrt; i++) {
if (suspectPrime % i == 0)
return;
}