У меня есть три случая для проверки относительной производительности классов, классов с наследованием и структур. Они должны использоваться для плотных циклов, поэтому показатели производительности. Точечные продукты используются как часть многих алгоритмов в 2D и 3D-геометрии, и я запускаю профилировщик на реальном коде. Нижеприведенные тесты свидетельствуют о реальных проблемах производительности, которые я видел.
Результаты за 100000000 раз через цикл и применение точечного продукта дают
ControlA 208 ms ( class with inheritence )
ControlB 201 ms ( class with no inheritence )
ControlC 85 ms ( struct )
Тесты выполнялись без отладки и оптимизации. Мой вопрос: что это за классы в этом случае, которые заставляют их быть настолько медленными?
Я предположил, что JIT все равно сможет встраивать все вызовы, класс или структуру, поэтому результаты должны быть идентичными. Обратите внимание, что если я отключу оптимизацию, то мои результаты будут идентичными.
ControlA 3239
ControlB 3228
ControlC 3213
Они всегда находятся в пределах 20 мс друг от друга, если тест повторно запускается.
Изучаемые классы
using System;
using System.Diagnostics;
public class PointControlA
{
public double X
{
get;
set;
}
public double Y
{
get;
set;
}
public PointControlA(double x, double y)
{
X = x;
Y = y;
}
}
public class Point3ControlA : PointControlA
{
public double Z
{
get;
set;
}
public Point3ControlA(double x, double y, double z): base (x, y)
{
Z = z;
}
public static double Dot(Point3ControlA a, Point3ControlA b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
}
public class Point3ControlB
{
public double X
{
get;
set;
}
public double Y
{
get;
set;
}
public double Z
{
get;
set;
}
public Point3ControlB(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
public static double Dot(Point3ControlB a, Point3ControlB b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
}
public struct Point3ControlC
{
public double X
{
get;
set;
}
public double Y
{
get;
set;
}
public double Z
{
get;
set;
}
public Point3ControlC(double x, double y, double z):this()
{
X = x;
Y = y;
Z = z;
}
public static double Dot(Point3ControlC a, Point3ControlC b)
{
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
}
}
Тест Script
public class Program
{
public static void TestStructClass()
{
var vControlA = new Point3ControlA(11, 12, 13);
var vControlB = new Point3ControlB(11, 12, 13);
var vControlC = new Point3ControlC(11, 12, 13);
var sw = Stopwatch.StartNew();
var n = 10000000;
double acc = 0;
sw = Stopwatch.StartNew();
for (int i = 0; i < n; i++)
{
acc += Point3ControlA.Dot(vControlA, vControlA);
}
Console.WriteLine("ControlA " + sw.ElapsedMilliseconds);
acc = 0;
sw = Stopwatch.StartNew();
for (int i = 0; i < n; i++)
{
acc += Point3ControlB.Dot(vControlB, vControlB);
}
Console.WriteLine("ControlB " + sw.ElapsedMilliseconds);
acc = 0;
sw = Stopwatch.StartNew();
for (int i = 0; i < n; i++)
{
acc += Point3ControlC.Dot(vControlC, vControlC);
}
Console.WriteLine("ControlC " + sw.ElapsedMilliseconds);
}
public static void Main()
{
TestStructClass();
}
}
Эта dotnet скрипка является доказательством компиляции. Он не показывает различий в производительности.
Я пытаюсь объяснить поставщику, почему их выбор использовать классы вместо структур для небольших числовых типов - это идея bad. У меня теперь есть тестовый пример, чтобы доказать это, но я не могу понять, почему.
ПРИМЕЧАНИЕ. Я попытался установить контрольную точку в отладчике с включенной оптимизацией JIT, но отладчик не сломается. Глядя на IL с оптимизацией JIT, я ничего не говорю.
ИЗМЕНИТЬ
После ответа @pkuderov я взял его код и сыграл с ним. Я изменил код и обнаружил, что, если я принудительно вставляю через
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Dot(Point3Class a)
{
return a.X * a.X + a.Y * a.Y + a.Z * a.Z;
}
разница между структурой и классом для точечного произведения исчезла. Почему с некоторыми настройками атрибут не нужен, но для меня это было неясно. Однако я не сдавался. По-прежнему существует проблема производительности с кодом поставщика, и я думаю, что DotProduct не лучший пример.
Я изменил код @pkuderov для реализации Vector Add
, который создаст новые экземпляры структур и классов. Результаты здесь
https://gist.github.com/bradphelan/9b383c8e99edc38068fcc0dccc8a7b48
В этом примере я также модифицировал код, чтобы выбрать псевдослучайный вектор из массива, чтобы избежать проблемы с экземплярами, хранящимися в регистрах (надеюсь).
Результаты показывают, что:
Производительность DotProduct идентична или, возможно, быстрее для классов
Vector Add, и я предполагаю, что все, что создает новый объект, медленнее.
Добавить класс/класс 2777ms Добавить struct/struct 2457ms
Класс/класс DotProd 1909ms DotProd struct/struct 2108ms
Полный код и результаты здесь, если кто-то хочет попробовать его.
Изменить снова
Для вектора add example, где массив векторов суммируется вместе, версия struct сохраняет аккумулятор в 3 регистрах
var accStruct = new Point3Struct(0, 0, 0);
for (int i = 0; i < n; i++)
accStruct = Point3Struct.Add(accStruct, pointStruct[(i + 1) % m]);
тело asm
// load the next vector into a register
00007FFA3CA2240E vmovsd xmm3,qword ptr [rax]
00007FFA3CA22413 vmovsd xmm4,qword ptr [rax+8]
00007FFA3CA22419 vmovsd xmm5,qword ptr [rax+10h]
// Sum the accumulator (the accumulator stays in the registers )
00007FFA3CA2241F vaddsd xmm0,xmm0,xmm3
00007FFA3CA22424 vaddsd xmm1,xmm1,xmm4
00007FFA3CA22429 vaddsd xmm2,xmm2,xmm5
но для векторной версии на основе класса он каждый раз считывает и записывает аккумулятор в основную память, которая неэффективна
var accPC = new Point3Class(0, 0, 0);
for (int i = 0; i < n; i++)
accPC = Point3Class.Add(accPC, pointClass[(i + 1) % m]);
тело asm
// Read and add both accumulator X and Xnext from main memory
00007FFA3CA2224A vmovsd xmm0,qword ptr [r14+8]
00007FFA3CA22250 vmovaps xmm7,xmm0
00007FFA3CA22255 vaddsd xmm7,xmm7,mmword ptr [r12+8]
// Read and add both accumulator Y and Ynext from main memory
00007FFA3CA2225C vmovsd xmm0,qword ptr [r14+10h]
00007FFA3CA22262 vmovaps xmm8,xmm0
00007FFA3CA22267 vaddsd xmm8,xmm8,mmword ptr [r12+10h]
// Read and add both accumulator Z and Znext from main memory
00007FFA3CA2226E vmovsd xmm9,qword ptr [r14+18h]
00007FFA3CA22283 vmovaps xmm0,xmm9
00007FFA3CA22288 vaddsd xmm0,xmm0,mmword ptr [r12+18h]
// Move accumulator accumulator X,Y,Z back to main memory.
00007FFA3CA2228F vmovsd qword ptr [rax+8],xmm7
00007FFA3CA22295 vmovsd qword ptr [rax+10h],xmm8
00007FFA3CA2229B vmovsd qword ptr [rax+18h],xmm0