Почему C # выполняет Math.Sqrt () медленнее, чем VB.NET? - PullRequest
50 голосов
/ 12 июня 2010

Фон

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

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

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

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

Тесты

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

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

  • Intel Core 2 Quad 6600 @ 2,4 ГГц

У каждой машины, на которой я тестировал, есть заметные различия в результатах тестов между 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?

Ответы [ 6 ]

82 голосов
/ 12 июня 2010

Реализация 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; 
        }
11 голосов
/ 12 июня 2010

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

9 голосов
/ 12 июня 2010

Вот декомпилированный 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
8 голосов
/ 12 июня 2010

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

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

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

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

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

3 голосов
/ 12 июня 2010

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

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

до:

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

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

0 голосов
/ 12 июня 2010

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

...