c# .net vb.net benchmarking

¿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; }