Why does C # execute Math.Sqrt () more slowly than VB.NET?

Background

Today, after conducting control tests, my colleagues and I discovered some strange things related to the performance of C # code and VB.NET code.

We started comparing C # and Delphi Prism, calculating prime numbers, and found that Prism was about 30% faster. I realized that CodeGear optimized the code when generating IL ( exe was about twice as large as C # and had all kinds of different ILs in it).

I decided to write a test in VB.NET, believing that Microsoft compilers would eventually write essentially the same IL for each language. However, the result was more shocking: the code worked more than three times slower in C # than VB with the same operation!

The generated IL was different, but not very so, and I'm not good enough to read it to understand the differences.

Benchmarks

I have included the code for each below. On my machine, VB finds 348513 primes in about 6.36 seconds. C # finds the same number of primes in 21.76 seconds.

Computer specifications and notes

  • Intel Core 2 Quad 6600 @ 2.4Ghz

Each machine I tested on has a noticeable difference in test results between C # and VB.NET.

Both console applications were compiled in Release mode, but otherwise the project settings were not changed from the default values ​​created by 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 

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

Why is the execution of C # Math.Sqrt() slower than VB.NET?

+49
c # benchmarking
Jun 11 '10 at 20:22
source share
6 answers

The C # implementation recounts Math.Sqrt(suspectPrime) each time through the loop, while VB only calculates it at the beginning of the loop. This is due to the nature of the governance structure. In C #, for is just a fancy while , while in VB, it's a separate construct.

Using this cycle, you will get even more points:

  Int32 sqrt = (int)Math.Sqrt(suspectPrime) for (Int32 i = 2; i <= sqrt; i++) { if (suspectPrime % i == 0) return; } 
+81
Jun 11 '10 at 20:28
source share

I agree with the statement that C # code computes sqrt at each iteration, and here is the proof right from Reflector:

VB Version:

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

C # Version:

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

Which view indicates the VB generation code that works better, even if the developer is naive enough to invoke the sqrt call in the loop definition.

+11
Jun 11 '10 at 20:34
source share

Here is a decompiled IL from for loops. If you compare the two, you will see that VB.Net only Math.Sqrt(...) onces, and C # checks it on each pass. To fix this, you need to do something like var sqrt = (int)Math.Sqrt(suspectPrime); as others have suggested.

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

... FROM#...

 .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
Jun 11 '10 at 20:50
source share

Turn off by tangent, if you are working with VS2010, you can use PLINQ and make C # faster (possibly VB.Net).

Change this for the loop ...

 var range = Enumerable.Range(2, 5000000); range.AsParallel() .ForAll(i => testIfPrimeSerial(i)); 

I went from 7.4 → 4.6 seconds on my car. Moving it to release mode slightly increases the wait time.

+8
Jun 11 '10 at 20:45
source share

Difference in the cycle; your c # code calculates the square root at each iteration. Change this line:

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

at

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

time on my car decreased from 26 seconds to 7.something.

+3
Jun 11 '10 at
source share

Generally not. They are both compiled into CLR (Common Language Runtime) bytecode. This is similar to the JVM (Java Virtual Machine).

-one
Jun 11 '10 at
source share



All Articles