Почему С# выполняет 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?
Ответы
Ответ 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).