#c# #.net #performance #optimization #jit
#c# #.net #Производительность #оптимизация #jit
Вопрос:
Вступление: я пишу высокопроизводительный код на C #. Да, я знаю, что C дал бы мне лучшую оптимизацию, но я все равно предпочитаю использовать C #. Я не хочу обсуждать этот выбор. Скорее, я хотел бы услышать от тех, кто, как и я, пытается писать высокопроизводительный код на .NET Framework.
Вопросы:
- Почему оператор в приведенном ниже коде медленнее, чем эквивалентный вызов метода??
- Почему метод передает два дубля в приведенном ниже коде быстрее, чем эквивалентный метод, передающий структуру, внутри которой есть два дубля? (A: старые JIT плохо оптимизируют структуры)
- Есть ли способ получить .ЧИСТЫЙ компилятор JIT для обработки простых структур так же эффективно, как и члены структуры? (A: получить более новый JIT)
Что я думаю, я знаю: оригинал .ЧИСТЫЙ JIT-компилятор не будет вставлять ничего, что связано со структурой. Причудливые заданные структуры следует использовать только там, где вам нужны небольшие типы значений, которые должны быть оптимизированы как встроенные, но true . К счастью, в .NET 3.5SP1 и .NET 2.0SP2 они внесли некоторые улучшения в оптимизатор JIT, включая улучшения встраивания, особенно для структур. (Я предполагаю, что они сделали это, потому что в противном случае новая сложная структура, которую они вводили, работала бы ужасно… так что команда Complex, вероятно, набрасывалась на команду оптимизаторов JIT.) Итак, любая документация до .NET 3.5 SP1, вероятно, не слишком актуальна для этой проблемы.
Что показывает мое тестирование: я убедился, что у меня есть более новый JIT-оптимизатор, проверив, что C:WindowsMicrosoft.NETFrameworkv2.0.50727mscorwks.dll файл имеет версию> = 3053 и поэтому должен иметь эти улучшения в JIT-оптимизаторе. Однако, даже при этом, каковы мои тайминги и взгляды на разборку, которые показывают оба:
Созданный JIT-кодом код для передачи структуры с двумя двойниками намного менее эффективен, чем код, который напрямую передает два двойника.
Созданный JIT-кодом код для метода struct передается в «this» гораздо эффективнее, чем если бы вы передали struct в качестве аргумента.
JIT по-прежнему будет лучше, если вы передадите два дубля, а не передадите структуру с двумя дублями, даже с множителем из-за того, что он явно находится в цикле.
Тайминги: на самом деле, глядя на разборку, я понимаю, что большую часть времени в циклах выполняется просто доступ к тестовым данным из списка. Разница между четырьмя способами выполнения одних и тех же вызовов резко отличается, если вы учитываете служебный код цикла и доступ к данным. Я получаю от 5 до 20 раз ускорения для выполнения PlusEqual(double, double) вместо PlusEqual(Element). И от 10 до 40 раз для выполнения 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
Комментарии:
1. Вау, на это следует ссылаться как на пример того, как может выглядеть хороший вопрос о Stackoverflow! Только автоматически сгенерированные комментарии могут быть опущены. К сожалению, я знаю слишком мало, чтобы действительно погрузиться в проблему, но мне действительно нравится вопрос!
2. Я не думаю, что модульный тест — хорошее место для запуска теста.
3. Почему структура должна быть быстрее, чем два удвоения? В .NET struct НИКОГДА не равен сумме размеров его элементов. Так что по определению он больше, поэтому по определению он должен быть медленнее при нажатии на стек, а затем всего 2 двойных значения. Если компилятор встроит параметр struct в строку 2 с двойной памятью, что, если внутри метода вы хотите получить доступ к этой структуре с помощью отражения. Где будет информация о времени выполнения, связанная с этим объектом struct ? Не так ли, или я что-то упускаю?
4. @Tigran: Вам нужны источники для этих утверждений. Я думаю, вы ошибаетесь. Только когда тип значения помещается в коробку, метаданные должны храниться вместе со значением. В переменной со статическим типом структуры нет накладных расходов.
5. Я думал, что не хватает только сборки. И теперь вы добавили это (обратите внимание, это ассемблер x86, а НЕ MSIL).
Ответ №1:
Я получаю совсем другие результаты, гораздо менее драматичные. Но я не использовал тестовый бегун, я вставил код в приложение консольного режима. Результат 5% составляет ~ 87% в 32-разрядном режиме, ~ 100% в 64-разрядном режиме, когда я пытаюсь это сделать.
Выравнивание имеет решающее значение для двойников, .NET runtime может обещать выравнивание только 4 на 32-разрядной машине. Мне кажется, что тестировщик запускает методы тестирования с адресом стека, который выровнен по 4 вместо 8. Штраф за смещение становится очень большим, когда double пересекает границу строки кэша.
Комментарии:
1. Почему . NET может в принципе добиться успеха при выравнивании только 4 двойников? Выравнивание выполняется с помощью 4-байтовых блоков на 32-битной машине. В чем проблема?
2. Почему время выполнения выравнивается только до 4 байт на x86? Я думаю, что он может выровняться до 64 бит, если потребуется дополнительная осторожность, когда неуправляемый код вызывает управляемый код. Хотя спецификация имеет только слабые гарантии выравнивания, реализации должны иметь возможность выравнивать более строго. (Спецификация: «8-байтовые данные правильно выровнены, когда они хранятся на той же границе, которая требуется базовому оборудованию для атомарного доступа к собственному int»)
3. @Code — Ну, это может быть, генераторы кода C делают это, выполняя математические вычисления для указателя стека в прологе функции. Дрожание x86 просто не работает. Это гораздо важнее для родных языков, поскольку выделение массивов в стеке встречается гораздо чаще, и у них есть распределитель кучи, который выравнивается до 8, поэтому никогда не хотелось бы делать выделение стека менее эффективным, чем выделение кучи. Мы застряли с выравниванием 4 из 32-разрядной кучи gc.
Ответ №2:
У меня возникли некоторые трудности с воспроизведением ваших результатов.
Я взял ваш код:
- сделал это отдельным консольным приложением
- создана оптимизированная (релизная) сборка
- увеличен коэффициент «размера» с 2,5 м до 10 м
- запустил его из командной строки (вне IDE)
Когда я это сделал, я получил следующие тайминги, которые сильно отличаются от ваших. Во избежание сомнений я опубликую именно тот код, который использовал.
Вот мои тайминги
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);
}
}
}
Комментарии:
1. Я только что сделал то же самое, мои результаты больше похожи на ваши. Пожалуйста, укажите платформу и тип процессора.
2. Очень интересно! У меня были другие, которые проверяли мои результаты … вы первый, кто отличается. Первый вопрос к вам: каков номер версии файла, который я упоминаю в своем сообщении … C:WindowsMicrosoft.NETFrameworkv2.0.50727mscorwks.dll … в документах Microsoft указано, что у вас есть версия JIT Optimizer. (Если я могу просто сказать своим пользователям обновить свой .NET, чтобы увидеть большие ускорения, я буду счастливым туристом. Но я предполагаю, что это будет не так просто.)
3. Я работал в Visual Studio … работал на Windows XP SP3 … на виртуальной машине VMware… на Intel Core i7 с частотой 2,7 ГГц. Но меня интересуют не абсолютные времена… это соотношения… Я бы ожидал, что все эти три метода будут работать одинаково, что они сделали для Corey, но НЕ для меня.
4. В моих свойствах проекта указано: Конфигурация: релиз; Платформа: активная (x86); Целевая платформа: x86
5. Что касается вашего запроса на получение версии mscorwks… Извините, вы хотели, чтобы я запустил эту штуку против .NET 2.0? Мои тесты были на .NET 4.0
Ответ №3:
Запуск .NET 4.0 здесь. Я скомпилировал с «любым процессором», ориентируясь на .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.
Комментарии:
1. Да, ответ на Structs выглядит так: «получить более новый JIT». Но, как я спросил в ответе Хенка, почему методы намного быстрее, чем операторы? Оба ваших метода в 5 раз быстрее, чем любой из ваших операторов… которые делают то же самое. Здорово, что я снова могу использовать структуры… но грустно, что мне все еще приходится избегать операторов.
2. Джим, мне было бы очень интересно узнать версию файла C:WindowsMicrosoft.NETFrameworkv2.0.50727mscorwks.dll в вашей системе … если новее, чем у меня (.3620), но старше, чем у Кори (.5446), то это может объяснить, почему ваши операторы все еще работают медленно, как у меня, а у Кори — нет.
3. @Brian: версия файла 2.0.50727.4214.
4. Спасибо! Итак, мне нужно убедиться, что у моих пользователей есть 4214 или более поздняя версия для оптимизации структуры и 5446 или более поздняя версия для оптимизации оператора. Мне нужно добавить некоторый код, чтобы проверить это при запуске и дать несколько предупреждений. Еще раз спасибо.
Ответ №4:
Как и @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. Это имеет значение и меньше различий в числах.
Комментарии:
1. Да, похоже, что более поздний JIT исправил проблему со структурой, но мой вопрос о том, почему методы намного быстрее, чем операторы, остается. Посмотрите, насколько быстрее оба метода PlusEqual, чем эквивалентный оператор = . И также интересно, насколько быстрее -=, чем = … ваши тайминги — первые, где я это видел.
2. Хенк, мне было бы очень интересно узнать версию файла C:WindowsMicrosoft.NETFrameworkv2.0.50727mscorwks.dll в вашей системе … если новее, чем у меня (.3620), но старше, чем у Кори (.5446), то это может объяснить, почему ваши операторы все еще работают медленно, как у меня, а у Кори — нет.
3. Я могу найти только версию .50727, но я не уверен, подходит ли это для Fx40 / Fx45?
4. Вы должны зайти в Свойства и щелкнуть вкладку Версии, чтобы увидеть остальную часть номера версии.
Ответ №5:
В дополнение к различиям компилятора JIT, упомянутым в других ответах, еще одно различие между вызовом метода struct и оператором struct заключается в том, что вызов метода struct будет передаваться this
как ref
параметр (и может быть записан для принятия других параметров ref
в качестве параметров), в то время как оператор struct будет передавать все операнды по значению. Стоимость передачи структуры любого размера в качестве ref
параметра фиксирована, независимо от того, насколько велика структура, в то время как стоимость передачи больших структур пропорциональна размеру структуры. Нет ничего плохого в использовании больших структур (даже сотен байт), если можно избежать их ненужного копирования; в то время как ненужные копии часто можно предотвратить при использовании методов, их нельзя предотвратить при использовании операторов.
Комментарии:
1. Хммм… что ж, это может многое объяснить! Итак, если оператор достаточно короткий, чтобы он был встроен, я предполагаю, что он не будет делать ненужные копии. Но если нет, и ваша структура состоит из более чем одного слова, вы можете не захотеть реализовывать ее как оператор, если скорость имеет решающее значение. Спасибо за понимание.
2. Кстати, одна вещь, которая меня немного раздражает, когда на вопросы о скорости отвечают «сравните это!», Заключается в том, что такой ответ игнорирует тот факт, что во многих случаях важно, занимает ли операция обычно 10 или 20 единиц, но может ли небольшое изменение обстоятельств привести к тому, что она займет 1 мс или 10 мс. Важно не то, насколько быстро что-то выполняется на машине разработчика, а то, будет ли операция когда-либо достаточно медленной, чтобы иметь значение ; если метод X выполняется в два раза быстрее, чем метод Y на большинстве машин, но на некоторых машинах он будет в 100 раз медленнее, метод Y может быть лучшим выбором.
3. Конечно, здесь мы говорим всего о 2 двойных… небольшие структуры. Передача двух двойных файлов в стек, где к ним можно быстро получить доступ, не обязательно медленнее, чем передача «this» в стек, а затем необходимость разыменования, чтобы использовать их для работы с ними .. но это может привести к различиям. Однако в этом случае он должен быть встроенным, поэтому оптимизатор JIT должен получить точно такой же код.
Ответ №6:
Не уверен, что это актуально, но вот цифры для 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%.
Комментарии:
1. Интересно… похоже, что оптимизации, которые были добавлены в 32b JIT Optimizer, еще не попали в 64b JIT Optimizer… ваши соотношения по-прежнему очень похожи на мои. Разочаровывает… но полезно знать.
Ответ №7:
Я бы предположил, что, когда вы обращаетесь к членам структуры, фактически выполняется дополнительная операция для доступа к члену, указателю THIS offset .
Комментарии:
1. Ну, с объектом класса вы были бы абсолютно правы … потому что методу просто был бы передан указатель «this». Однако со структурами это не должно быть так. Структура должна быть передана в методы в стеке. Итак, первый double должен находиться там, где будет находиться указатель «this», а второй double — в позиции сразу после него … оба, возможно, являются регистрами в CPU. Итак, JIT должен использовать максимум смещения.
Ответ №8:
Может быть, вместо списка вам следует использовать double[] с «хорошо известными» смещениями и приращениями индекса?