为什么C#比VB.NET更慢地执行Math.Sqrt()?

Mat*_*ler 51 .net c# vb.net benchmarking

背景

在今天早上运行基准测试时,我和我的同事发现了一些关于C#代码与VB.NET代码性能的奇怪之处.

我们开始比较C#与Delphi Prism计算质数,发现Prism的速度提高了约30%.在生成IL时,我认为CodeGear优化代码更多(exe大约是C#的两倍,并且有各种不同的IL.)

我决定在VB.NET中编写一个测试,假设微软的编译器最终会为每种语言编写基本相同的IL.然而,结果更令人震惊:C#的代码运行速度比VB运行速度快三倍以上!

生成的IL是不同的,但并非极端如此,而且我不太善于阅读它以理解差异.

基准

我已经在下面列出了每个代码.在我的机器上,VB在大约6.36秒内找到了348513个素数.C#在21.76秒内找到相同数量的素数.

计算机规格和注释

  • 英特尔酷睿2四核6600 @ 2.4Ghz

我在那里测试的每台机器在C#和VB.NET之间的基准测试结果上都有明显的差异.

两个控制台应用程序都是在发布模式下编译的,但是否则没有从Visual Studio 2008生成的默认值更改项目设置.

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
Run Code Online (Sandbox Code Playgroud)

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

    }
}
Run Code Online (Sandbox Code Playgroud)

为什么C#的执行Math.Sqrt()速度比VB.NET慢?

Gab*_*abe 82

C#实现Math.Sqrt(suspectPrime)每次循环都会重新计算,而VB只在循环开始时计算它.这仅仅是由于控制结构的性质.在C#中,for它只是一个奇特的while循环,而在VB中它是一个单独的构造.

使用这个循环甚至会得分:

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

  • Peter Ruderman:是的,它是一个循环不变量,但优化器需要知道Math.Sqrt是一个纯函数(没有副作用并且总是返回相同的值),这可能超出了它的肯定. (8认同)
  • 这对我来说非常令人惊讶.suspectPrime是一个循环不变量,所以优化器不应该将计算提升出循环吗? (3认同)
  • kjack:我不知道VB6,但在VB.Net中,终止值仅在循环开始时进行评估(根据规范的第10.9.2节),因此不需要使用临时变量. (2认同)

Otá*_*cio 11

我同意C#代码在每次迭代时计算sqrt的说法,这里是Reflector的直接证明:

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);
}
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

哪一种指向VB生成的代码即使开发人员天真到足以在循环定义中调用sqrt也能表现得更好.

  • 他们实际上并没有说它必须在每次运行时评估表达式.你只需说,从`x到y',每个循环将增量值赋给变量`i`.它没有说,我继续直到我高于/等于表达.语义与C#语言有很大不同. (8认同)
  • VB的for循环实际上被定义为在循环开始时恰好评估表达式一次.使用复杂表达式作为终止条件是非常安全的,因为它保证只运行一次. (4认同)

Mat*_*ted 9

这是来自for循环的反编译IL.如果你比较两者,你会看到VB.Net只执行Math.Sqrt(...)once而C#在每次传递时检查它.要解决这个问题,你需要像var sqrt = (int)Math.Sqrt(suspectPrime);其他人建议的那样做.

...... 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
Run Code Online (Sandbox Code Playgroud)

... 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
Run Code Online (Sandbox Code Playgroud)


48k*_*ocs 8

关闭切线,如果你已经启动并运行VS2010,你可以利用PLINQ并更快地制作C#(可能也是VB.Net).

将for for循环更改为......

var range = Enumerable.Range(2, 5000000);

range.AsParallel()
    .ForAll(i => testIfPrimeSerial(i));
Run Code Online (Sandbox Code Playgroud)

我在机器上从7.4 - > 4.6秒开始.将其移至发布模式可以节省更多时间.