Почему C# выполняет Математику. Sqrt () более медленно, чем VB.NET?

Фон

В то время как рабочие оценочные испытания этим утром, мои коллеги и я обнаружили, что некоторые странные вещи относительно производительности C# кодируют по сравнению с кодом VB.NET.

Мы начали сравнивать C# по сравнению с Призмой Delphi, вычисляющей простые числа, и нашли, что Призма была приблизительно на 30% быстрее. Я изобразил оптимизированный код CodeGear больше при генерации IL ( exe было приблизительно вдвое более большим, чем C# и имел все виды другого IL в нем.)

Я решил записать тест в VB.NET также, предположив, что компиляторы Microsoft закончат тем, что писали по существу тот же IL для каждого языка. Однако результат там был более шокирующим: код работал больше чем в три раза медленнее на C#, чем VB с той же операцией!

Сгенерированный IL отличался, но не чрезвычайно так, и я не достаточно хорош при чтении его для понимания различий.

Сравнительные тесты

Я включал код для каждого ниже. На моей машине VB находит 348 513 начал приблизительно за 6,36 секунд. C# находит то же количество начал за 21,76 секунды.

Компьютерные спецификации и примечания

  • Intel Core 2 Quad 6600 2.4 ГГц

Каждая машина, на которой я протестировал, существует заметное различие в результатах сравнительного теста между C# и VB.NET.

Оба из консольных приложений были скомпилированы в режиме Release, но иначе никакие настройки проекта не были изменены от значений по умолчанию, сгенерированных 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?

51
задан Ry- 23 March 2012 в 13:45
поделиться

6 ответов

Реализация 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; 
        }
82
ответ дан 7 November 2019 в 10:01
поделиться

Я согласен с утверждением, что код 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 в определении цикла.

11
ответ дан 7 November 2019 в 10:01
поделиться

Вот декомпилированный IL из циклов for. Если вы сравните эти два, вы увидите, что 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
9
ответ дан 7 November 2019 в 10:01
поделиться

По касательной, если вы работаете с VS2010, вы можете воспользоваться преимуществами PLINQ и сделать C # (возможно, также VB.Net) быстрее.

Измените цикл for на ...

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

range.AsParallel()
    .ForAll(i => testIfPrimeSerial(i));

Я перешел с 7,4 на 4,6 секунды на моей машине. Перемещение его в режим выпуска позволяет сэкономить немного больше времени.

8
ответ дан 7 November 2019 в 10:01
поделиться

Обычно нет. Оба они компилируются в байт-код CLR (Common Language Runtime). Это похоже на JVM (виртуальная машина Java).

-1
ответ дан 7 November 2019 в 10:01
поделиться

Разница в цикле; ваш код C # вычисляет квадратный корень на каждой итерации. Изменение этой единственной строки с:

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

на:

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

снизило время на моей машине с 26 секунд до 7. что-то.

3
ответ дан 7 November 2019 в 10:01
поделиться