Введение: я пишу высокопроизводительный код на C #. Да, я знаю, что C ++ даст мне лучшую оптимизацию, но я все же предпочитаю использовать C #. Я не хочу обсуждать этот выбор. Скорее, я хотел бы услышать мнение тех, кто, как и я, пытается писать высокопроизводительный код на .NET Framework.
Вопросов:
- Почему оператор в приведенном ниже коде медленнее, чем вызов эквивалентного метода?
- Почему метод передает два двойных значения в приведенном ниже коде быстрее, чем эквивалентный метод, передающий структуру с двумя двойными значениями внутри? (A: старые JIT плохо оптимизируют структуры)
- Есть ли способ заставить компилятор .NET JIT обрабатывать простые структуры так же эффективно, как и члены структуры? (A: получите более новую JIT)
Думаю, я знаю: исходный компилятор .NET JIT не встраивал ничего, что связано со структурой. Причудливые заданные структуры следует использовать только там, где вам нужны небольшие типы значений, которые следует оптимизировать как встроенные, но это правда. К счастью, в .NET 3.5SP1 и .NET 2.0SP2 были внесены некоторые улучшения в оптимизатор JIT, включая улучшения встраивания, особенно для структур. (Я предполагаю, что они сделали это, потому что в противном случае новая структура Complex, которую они вводили, работала бы ужасно ... поэтому команда Complex, вероятно, обрушивалась на команду JIT Optimizer.) Итак, любая документация до .NET 3.5 SP1, вероятно, не слишком актуален для этого вопроса.
Что показывает мое тестирование: я подтвердил, что у меня есть более новый оптимизатор JIT, проверив, что файл C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll имеет версию> = 3053, и поэтому эти улучшения должны быть к оптимизатору JIT. Однако, даже с учетом этого, мои тайминги и взгляды на разборку показывают:
Созданный JIT код для передачи структуры с двумя двойными значениями намного менее эффективен, чем код, который напрямую передает два двойных значения.
Созданный JIT код для метода структуры передает this гораздо эффективнее, чем если бы вы передали структуру в качестве аргумента.
JIT по-прежнему встраивается лучше, если вы передаете две двойные, а не структуру с двумя двойными, даже с множителем из-за того, что вы явно находитесь в цикле.
Время: На самом деле, глядя на разборку, я понимаю, что большую часть времени в циклах происходит просто доступ к тестовым данным из списка. Разница между четырьмя способами выполнения одних и тех же вызовов кардинально отличается, если исключить служебный код цикла и доступ к данным. Я получаю от 5x до 20x ускорений за выполнение PlusEqual (double, double) вместо PlusEqual (Element). И от 10x до 40x для выполнения PlusEqual (double, double) вместо operator + =. Вау. Грустный.
Вот один набор таймингов:
Populating List<Element> took 320ms.
The PlusEqual() method took 105ms.
The 'same' += operator took 131ms.
The 'same' -= operator took 139ms.
The PlusEqual(double, double) method took 68ms.
The do nothing loop took 66ms.
The ratio of operator with constructor to method is 124%.
The ratio of operator without constructor to method is 132%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 64%.
If we remove the overhead time for the loop accessing the elements from the List...
The ratio of operator with constructor to method is 166%.
The ratio of operator without constructor to method is 187%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 5%.
Код:
namespace OperatorVsMethod
{
public struct Element
{
public double Left;
public double Right;
public Element(double left, double right)
{
this.Left = left;
this.Right = right;
}
public static Element operator +(Element x, Element y)
{
return new Element(x.Left + y.Left, x.Right + y.Right);
}
public static Element operator -(Element x, Element y)
{
x.Left += y.Left;
x.Right += y.Right;
return x;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(Element that)
{
this.Left += that.Left;
this.Right += that.Right;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(double thatLeft, double thatRight)
{
this.Left += thatLeft;
this.Right += thatRight;
}
}
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Stopwatch stopwatch = new Stopwatch();
// Populate a List of Elements to multiply together
int seedSize = 4;
List<double> doubles = new List<double>(seedSize);
doubles.Add(2.5d);
doubles.Add(100000d);
doubles.Add(-0.5d);
doubles.Add(-100002d);
int size = 2500000 * seedSize;
List<Element> elts = new List<Element>(size);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
int di = ii % seedSize;
double d = doubles[di];
elts.Add(new Element(d, d));
}
stopwatch.Stop();
long populateMS = stopwatch.ElapsedMilliseconds;
// Measure speed of += operator (calls ctor)
Element operatorCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorCtorResult += elts[ii];
}
stopwatch.Stop();
long operatorCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of -= operator (+= without ctor)
Element operatorNoCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorNoCtorResult -= elts[ii];
}
stopwatch.Stop();
long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(Element) method
Element plusEqualResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
plusEqualResult.PlusEqual(elts[ii]);
}
stopwatch.Stop();
long plusEqualMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(double, double) method
Element plusEqualDDResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
}
stopwatch.Stop();
long plusEqualDDMS = stopwatch.ElapsedMilliseconds;
// Measure speed of doing nothing but accessing the Element
Element doNothingResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
double left = elt.Left;
double right = elt.Right;
}
stopwatch.Stop();
long doNothingMS = stopwatch.ElapsedMilliseconds;
// Report results
Assert.AreEqual(1d, operatorCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, operatorNoCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualDDResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, doNothingResult.Left, "The operator += did not compute the right result!");
// Report speeds
Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS);
Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS);
Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);
// Compare speeds
long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
operatorCtorMS -= doNothingMS;
operatorNoCtorMS -= doNothingMS;
plusEqualMS -= doNothingMS;
plusEqualDDMS -= doNothingMS;
Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
}
}
}
IL: (также известный как то, во что компилируется часть вышеперечисленного)
public void PlusEqual(Element that)
{
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,30h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[04C87B7Ch],0
0000001d je 00000024
0000001f call 753081B1
00000024 nop
this.Left += that.Left;
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 fld qword ptr [ebp+8]
0000002b fadd qword ptr [eax]
0000002d fstp qword ptr [eax]
this.Right += that.Right;
0000002f mov eax,dword ptr [ebp-3Ch]
00000032 fld qword ptr [ebp+10h]
00000035 fadd qword ptr [eax+8]
00000038 fstp qword ptr [eax+8]
}
0000003b nop
0000003c lea esp,[ebp-0Ch]
0000003f pop ebx
00000040 pop esi
00000041 pop edi
00000042 pop ebp
00000043 ret 10h
public void PlusEqual(double thatLeft, double thatRight)
{
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,30h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[04C87B7Ch],0
0000001d je 00000024
0000001f call 75308159
00000024 nop
this.Left += thatLeft;
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 fld qword ptr [ebp+10h]
0000002b fadd qword ptr [eax]
0000002d fstp qword ptr [eax]
this.Right += thatRight;
0000002f mov eax,dword ptr [ebp-3Ch]
00000032 fld qword ptr [ebp+8]
00000035 fadd qword ptr [eax+8]
00000038 fstp qword ptr [eax+8]
}
0000003b nop
0000003c lea esp,[ebp-0Ch]
0000003f pop ebx
00000040 pop esi
00000041 pop edi
00000042 pop ebp
00000043 ret 10h
источник
Ответы:
Я получаю совсем другие результаты, гораздо менее драматические. Но не использовал тестовый раннер, я вставил код в приложение консольного режима. Результат 5% составляет ~ 87% в 32-битном режиме, ~ 100% в 64-битном режиме, когда я пробую.
Выравнивание критически важно для двойников, среда выполнения .NET может обещать только выравнивание 4 на 32-битной машине. Мне кажется, что исполнитель тестов запускает методы тестирования с адресом стека, который выровнен по 4 вместо 8. Штраф за несовпадение становится очень большим, когда двойник пересекает границу строки кэша.
источник
Мне трудно воспроизвести ваши результаты.
Я взял твой код:
Когда я это сделал, я получил следующие тайминги, которые сильно отличаются от ваших. Во избежание сомнений выложу именно тот код, который использовал.
Вот мои тайминги
Populating List<Element> took 527ms. The PlusEqual() method took 450ms. The 'same' += operator took 386ms. The 'same' -= operator took 446ms. The PlusEqual(double, double) method took 413ms. The do nothing loop took 229ms. The ratio of operator with constructor to method is 85%. The ratio of operator without constructor to method is 99%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 91%. If we remove the overhead time for the loop accessing the elements from the List... The ratio of operator with constructor to method is 71%. The ratio of operator without constructor to method is 98%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 83%.
И это мои правки в ваш код:
namespace OperatorVsMethod { public struct Element { public double Left; public double Right; public Element(double left, double right) { this.Left = left; this.Right = right; } public static Element operator +(Element x, Element y) { return new Element(x.Left + y.Left, x.Right + y.Right); } public static Element operator -(Element x, Element y) { x.Left += y.Left; x.Right += y.Right; return x; } /// <summary> /// Like the += operator; but faster. /// </summary> public void PlusEqual(Element that) { this.Left += that.Left; this.Right += that.Right; } /// <summary> /// Like the += operator; but faster. /// </summary> public void PlusEqual(double thatLeft, double thatRight) { this.Left += thatLeft; this.Right += thatRight; } } public class UnitTest1 { public static void Main() { Stopwatch stopwatch = new Stopwatch(); // Populate a List of Elements to multiply together int seedSize = 4; List<double> doubles = new List<double>(seedSize); doubles.Add(2.5d); doubles.Add(100000d); doubles.Add(-0.5d); doubles.Add(-100002d); int size = 10000000 * seedSize; List<Element> elts = new List<Element>(size); stopwatch.Reset(); stopwatch.Start(); for (int ii = 0; ii < size; ++ii) { int di = ii % seedSize; double d = doubles[di]; elts.Add(new Element(d, d)); } stopwatch.Stop(); long populateMS = stopwatch.ElapsedMilliseconds; // Measure speed of += operator (calls ctor) Element operatorCtorResult = new Element(1d, 1d); stopwatch.Reset(); stopwatch.Start(); for (int ii = 0; ii < size; ++ii) { operatorCtorResult += elts[ii]; } stopwatch.Stop(); long operatorCtorMS = stopwatch.ElapsedMilliseconds; // Measure speed of -= operator (+= without ctor) Element operatorNoCtorResult = new Element(1d, 1d); stopwatch.Reset(); stopwatch.Start(); for (int ii = 0; ii < size; ++ii) { operatorNoCtorResult -= elts[ii]; } stopwatch.Stop(); long operatorNoCtorMS = stopwatch.ElapsedMilliseconds; // Measure speed of PlusEqual(Element) method Element plusEqualResult = new Element(1d, 1d); stopwatch.Reset(); stopwatch.Start(); for (int ii = 0; ii < size; ++ii) { plusEqualResult.PlusEqual(elts[ii]); } stopwatch.Stop(); long plusEqualMS = stopwatch.ElapsedMilliseconds; // Measure speed of PlusEqual(double, double) method Element plusEqualDDResult = new Element(1d, 1d); stopwatch.Reset(); stopwatch.Start(); for (int ii = 0; ii < size; ++ii) { Element elt = elts[ii]; plusEqualDDResult.PlusEqual(elt.Left, elt.Right); } stopwatch.Stop(); long plusEqualDDMS = stopwatch.ElapsedMilliseconds; // Measure speed of doing nothing but accessing the Element Element doNothingResult = new Element(1d, 1d); stopwatch.Reset(); stopwatch.Start(); for (int ii = 0; ii < size; ++ii) { Element elt = elts[ii]; double left = elt.Left; double right = elt.Right; } stopwatch.Stop(); long doNothingMS = stopwatch.ElapsedMilliseconds; // Report speeds Console.WriteLine("Populating List<Element> took {0}ms.", populateMS); Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS); Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS); Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS); Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS); Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS); // Compare speeds long percentageRatio = 100L * operatorCtorMS / plusEqualMS; Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio); percentageRatio = 100L * operatorNoCtorMS / plusEqualMS; Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio); percentageRatio = 100L * plusEqualDDMS / plusEqualMS; Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio); operatorCtorMS -= doNothingMS; operatorNoCtorMS -= doNothingMS; plusEqualMS -= doNothingMS; plusEqualDDMS -= doNothingMS; Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List..."); percentageRatio = 100L * operatorCtorMS / plusEqualMS; Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio); percentageRatio = 100L * operatorNoCtorMS / plusEqualMS; Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio); percentageRatio = 100L * plusEqualDDMS / plusEqualMS; Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio); } } }
источник
Запуск .NET 4.0 здесь. Я скомпилировал "Any CPU", ориентируясь на .NET 4.0 в режиме выпуска. Выполнение было из командной строки. Он работал в 64-битном режиме. У меня немного другое время.
Populating List<Element> took 442ms. The PlusEqual() method took 115ms. The 'same' += operator took 201ms. The 'same' -= operator took 200ms. The PlusEqual(double, double) method took 129ms. The do nothing loop took 93ms. The ratio of operator with constructor to method is 174%. The ratio of operator without constructor to method is 173%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 112%. If we remove the overhead time for the loop accessing the elements from the List ... The ratio of operator with constructor to method is 490%. The ratio of operator without constructor to method is 486%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 163%.
В частности,
PlusEqual(Element)
немного быстрее, чемPlusEqual(double, double)
.Какой бы ни была проблема в .NET 3.5, похоже, что в .NET 4.0 ее нет.
источник
Как и @Corey Kosak, я только что запустил этот код в VS 2010 Express как простое консольное приложение в режиме выпуска. Я получаю совсем другие цифры. Но у меня также есть Fx4.5, так что это может быть не результат для чистого Fx4.0.
Populating List<Element> took 435ms. The PlusEqual() method took 109ms. The 'same' += operator took 217ms. The 'same' -= operator took 157ms. The PlusEqual(double, double) method took 118ms. The do nothing loop took 79ms. The ratio of operator with constructor to method is 199%. The ratio of operator without constructor to method is 144%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 108%. If we remove the overhead time for the loop accessing the elements from the List ... The ratio of operator with constructor to method is 460%. The ratio of operator without constructor to method is 260%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 130%.
Изменить: и теперь запустить из строки cmd. Это имеет значение и меньше вариаций в числах.
источник
В дополнение к различиям JIT-компилятора, упомянутым в других ответах, еще одно различие между вызовом метода структуры и оператором структуры заключается в том, что вызов метода структуры будет передаваться
this
какref
параметр (и может быть записан для принятия других параметров в качествеref
параметров), в то время как Оператор структуры передаст все операнды по значению. Стоимость передачи структуры любого размера в качествеref
параметра является фиксированной, независимо от размера структуры, в то время как стоимость передачи более крупных структур пропорциональна размеру структуры. Нет ничего плохого в использовании больших структур (даже сотен байтов), если можно избежать их ненужного копирования ; в то время как ненужные копии часто можно предотвратить с помощью методов, их нельзя предотвратить с помощью операторов.источник
Не уверен, что это актуально, но вот цифры для 64-разрядной версии .NET 4.0 в 64-разрядной версии Windows 7. Моя версия mscorwks.dll - 2.0.50727.5446. Я просто вставил код в LINQPad и запустил его оттуда. Вот результат:
Populating List<Element> took 496ms. The PlusEqual() method took 189ms. The 'same' += operator took 295ms. The 'same' -= operator took 358ms. The PlusEqual(double, double) method took 148ms. The do nothing loop took 103ms. The ratio of operator with constructor to method is 156%. The ratio of operator without constructor to method is 189%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 78%. If we remove the overhead time for the loop accessing the elements from the List ... The ratio of operator with constructor to method is 223%. The ratio of operator without constructor to method is 296%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 52%.
источник
Я бы предположил, что когда вы обращаетесь к членам структуры, она фактически выполняет дополнительную операцию для доступа к члену, ЭТОЙ указатель + смещение.
источник
Может, вместо List использовать double [] с «известными» смещениями и приращениями индекса?
источник