В то время как рабочие оценочные испытания этим утром, мои коллеги и я обнаружили, что некоторые странные вещи относительно производительности 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 секунды.
Каждая машина, на которой я протестировал, существует заметное различие в результатах сравнительного теста между C# и VB.NET.
Оба из консольных приложений были скомпилированы в режиме Release, но иначе никакие настройки проекта не были изменены от значений по умолчанию, сгенерированных Visual Studio 2008.
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);
}
}
}
Почему имеет выполнение C# Math.Sqrt()
медленнее, чем VB.NET?
Реализация 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;
}
Я согласен с утверждением, что код 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 в определении цикла.
Вот декомпилированный 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
По касательной, если вы работаете с VS2010, вы можете воспользоваться преимуществами PLINQ и сделать C # (возможно, также VB.Net) быстрее.
Измените цикл for на ...
var range = Enumerable.Range(2, 5000000);
range.AsParallel()
.ForAll(i => testIfPrimeSerial(i));
Я перешел с 7,4 на 4,6 секунды на моей машине. Перемещение его в режим выпуска позволяет сэкономить немного больше времени.
Обычно нет. Оба они компилируются в байт-код CLR (Common Language Runtime). Это похоже на JVM (виртуальная машина Java).
Разница в цикле; ваш код C # вычисляет квадратный корень на каждой итерации. Изменение этой единственной строки с:
for (Int32 i = 2; i <= Math.Sqrt(suspectPrime); i++) {
на:
var lim = Math.Sqrt(suspectPrime);
for (Int32 i = 2; i <= lim; i++) {
снизило время на моей машине с 26 секунд до 7. что-то.