Подтвердить что ты не робот

Почему С# выполняет Math.Sqrt() медленнее, чем VB.NET?

Фон

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

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

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

Сгенерированный ИЛ был другим, но не очень-то таким, и я недостаточно хорош, чтобы прочитать его, чтобы понять различия.

Бенчмарки

Я включил код для каждого ниже. На моей машине VB находит 348513 простых чисел примерно в 6,36 секунд. С# находит такое же количество простых чисел в 21.76 секунд.

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

  • Intel Core 2 Quad 6600 @2.4Ghz

Каждая машина, на которой я тестировал, имеет заметную разницу в результатах теста между С# и 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

Код С#

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

    }
}

Почему выполнение С# Math.Sqrt() медленнее, чем VB.NET?

4b9b3361

Ответ 1

Реализация С# пересчитывает Math.Sqrt(suspectPrime) каждый раз через цикл, тогда как VB только вычисляет ее в начале цикла. Это связано с характером структуры управления. В С# for представляет собой просто причудливый цикл while, а в VB - отдельная конструкция.

Используя этот цикл, вы получите еще больше очков:

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

Ответ 2

Я согласен с утверждением, что код С# вычисляет 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);
}

Версия С#:

 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 в определении цикла.

Ответ 3

Вот декомпилированный IL из циклов for. Если вы сравните два, вы увидите, что VB.Net выполняет только Math.Sqrt(...) onces, а С# проверяет его на каждом проходе. Чтобы исправить это, вам нужно сделать что-то вроде 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

... С#...

.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 и быстрее сделать С# (возможно, VB.Net).

Измените это для цикла...

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

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

Я пошел с 7.4 → 4.6 секунды на моей машине. Перемещение его в режим освобождения немного увеличивает время ожидания.

Ответ 5

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

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

в

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

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

Ответ 6

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