【问题标题】:Why does C# execute Math.Sqrt() more slowly than VB.NET?为什么 C# 执行 Math.Sqrt() 比 VB.NET 慢?
【发布时间】:2011-03-02 20:14:05
【问题描述】:

背景

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

我们开始比较 C# 与 Delphi Prism 计算素数,发现 Prism 快了大约 30%。我认为 CodeGear 在生成 IL 时对代码进行了更多优化(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

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

    }
}

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

【问题讨论】:

  • 这两个代码示例在一个小点上有很大的不同,导致这个基准完全错误。
  • @I__:我知道你在那里做了什么。
  • @matt 我看到l_在那里做了什么。
  • 这个确切的问题(VB.Net 的 For...To 循环的评估一次行为)最近在这里出现:stackoverflow.com/q/52607611

标签: c# .net vb.net benchmarking


【解决方案1】:

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

【讨论】:

  • kjack:我不了解 VB6,但在 VB.Net 中,终止值仅在循环开始时评估(根据规范的第 10.9.2 节),所以有无需使用临时变量。
  • @kjack BASIC 和 (IIRC) FORTRAN 只计算一次 for 边界。你需要检查你的实现,但这样做是微不足道的(有一个边界,它是一个打印一些东西的函数)
  • 这让我很惊讶。怀疑Prime是一个循环不变量,那么优化器不应该将计算提升到循环之外吗?
  • @彼得。如果你使用for(..) 就像foreach(...) 一样。目前可以做for(; iterator.MoveNext();),但如果它只评估一次条件就不会。 VB.Net 中的 For 是范围评估,而 C# 中的 for 是条件循环。
  • Peter Ruderman:是的,它是一个循环不变量,但是优化器需要知道 Math.Sqrt 是一个纯函数(没有副作用并且总是返回相同的值),这可能超出了它的范围.
【解决方案2】:

我同意 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);
}

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

这有点表明 VB 生成的代码性能更好,即使开发人员天真地在循环定义中调用 sqrt 也是如此。

【讨论】:

  • 我想知道他们执行了什么样的分析来确保表达式没有引起副作用。
  • @ChaosPandion - 我同意并且对此有些不安。对我来说这不是问题,因为我本能地从不将函数调用放在循环控制中。
  • 他们的 for 实际上并没有说它必须在每次运行时评估表达式。您只是说,从x to y 开始,并为每个循环将递增的值分配给变量i。它没有说,我继续直到 i 高于/等于表达式。语义与 C# 非常不同。
  • VB 的 for 循环实际上被定义为在循环开始时只计算一次表达式。使用复杂表达式作为终止条件是非常安全的,因为它保证只运行一次。
  • @ChaosPandion,我使用过其他几种具有这种结构而不是 C 方式的语言。
【解决方案3】:

这是从 for 循环中反编译的 IL。如果你比较这两者,你会看到 VB.Net 只执行一次Math.Sqrt(...),而 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

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

【讨论】:

    【解决方案4】:

    切入点,如果您正在使用 VS2010 启动并运行,您可以利用 PLINQ 并使 C#(可能还有 VB.Net)更快。

    将 for 循环更改为...

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

    我在我的机器上从 7.4 -> 4.6 秒开始。将其移至释放模式可以节省更多时间。

    【讨论】:

    • 一个很好的切线 - 并行功能实际上是我们之前运行的基准测试的一部分,它们确实令人印象深刻!
    • @Gabe:我有一台小巧的双核笔记本电脑。我不知道CPU是什么。
    【解决方案5】:

    区别在于循环;您的 C# 代码在每次迭代中计算平方根。改变这一行:

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

    到:

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

    将我机器上的时间从 26 秒降至 7.something。

    【讨论】:

      【解决方案6】:

      一般不会。它们都编译为 CLR(公共语言运行时)字节码。这类似于 JVM(Java 虚拟机)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-10-04
        • 2012-02-19
        • 2017-11-04
        • 1970-01-01
        • 1970-01-01
        • 2021-05-08
        相关资源
        最近更新 更多