bra*_*ing 12 c# performance jit numerical-methods
我有三种情况来测试类的相对性能,具有继承性和结构的类.这些将用于紧密循环,因此性能很重要.Dot产品被用作2D和3D几何中的许多算法的一部分,我在实际代码上运行了分析器.以下测试表明我见过的真实世界性能问题.
通过循环和点积的应用得到的结果为1亿次
ControlA 208 ms ( class with inheritence )
ControlB 201 ms ( class with no inheritence )
ControlC 85 ms ( struct )
Run Code Online (Sandbox Code Playgroud)
测试正在运行,没有打开调试和优化.我的问题是,在这种情况下,类是什么导致它们如此缓慢?
我假设JIT仍然可以内联所有的调用,类或结构,所以实际上结果应该是相同的.请注意,如果我禁用优化,那么我的结果是相同的.
ControlA 3239
ControlB 3228
ControlC 3213
Run Code Online (Sandbox Code Playgroud)
如果重新运行测试,它们总是在彼此相差20ms内.
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;
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
这个dotnet小提琴只是编译的证明.它没有显示性能差异.
我试图向供应商解释为什么他们选择使用类而不是小数字类型的结构是一个坏主意.我现在有测试用例来证明它,但我不明白为什么.
注意:我已尝试在调试器中设置断点,并启用JIT优化,但调试器不会中断.在关闭JIT优化的情况下查看IL并没有告诉我什么.
在@pkuderov的回答之后,我拿了他的代码并玩了它.我改变了代码,发现如果我强迫通过内联
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Dot(Point3Class a)
{
return a.X * a.X + a.Y * a.Y + a.Z * a.Z;
}
Run Code Online (Sandbox Code Playgroud)
点积的结构和类之间的差异消失了.为什么有些设置不需要属性,但对我来说,目前尚不清楚.但是我并没有放弃.供应商代码仍然存在性能问题,我认为DotProduct不是最好的例子.
我修改了@pkuderov的代码来实现Vector Add它将创建结构和类的新实例.结果在这里
https://gist.github.com/bradphelan/9b383c8e99edc38068fcc0dccc8a7b48
在示例中,我还修改了代码以从数组中选择伪随机向量,以避免实例在寄存器中出现问题(我希望).
结果表明:
对于
Vector Add 类,DotProduct性能相同或者更快,我认为创建新对象的任何东西都比较慢.
添加类/类2777ms添加struct/struct 2457ms
DotProd类/类1909ms DotProd struct/struct 2108ms
如果有人想试试,那么完整的代码和结果就在这里.
对于向量添加示例,其中向量数组被加在一起,结构版本将累加器保持在3个寄存器中
var accStruct = new Point3Struct(0, 0, 0);
for (int i = 0; i < n; i++)
accStruct = Point3Struct.Add(accStruct, pointStruct[(i + 1) % m]);
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
但对于基于类的矢量版本,它每次都会读取和写出累加器到主存,这是低效的
var accPC = new Point3Class(0, 0, 0);
for (int i = 0; i < n; i++)
accPC = Point3Class.Add(accPC, pointClass[(i + 1) % m]);
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
更新
在花了一些时间思考问题之后,我认为我同意@DavidHaim 的观点,即由于缓存,内存跳转开销不是这里的情况。
另外,我还在您的测试中添加了更多选项(并删除了第一个带有继承的选项)。所以我有:
Dot(cl, cl)- 初始方法Dot(cl)- 这是“平方积”Dot(cl.X, cl.Y, cl.Z, cl.X, cl.Y, cl.Z)又名 Dot(cl.xyz)- 传递字段Dot(st, st)- 最初的Dot(st)- 方形产品Dot(st.X, st.Y, st.Z, st.X, st.Y, st.Z)又名 Dot(st.xyz) - 传递字段Dot(st6)- 想要检查结构的大小是否重要Dot(x, y, z, x, y, z)又名 Dot(xyz) - 只是局部 const 双变量。结果时间为:
...而且我不太确定为什么会看到这些结果。
也许对于普通基元类型,编译器会进行更积极的寄存器传递优化,也许它更确定生命周期边界或恒定性,然后再次进行更积极的优化。也许是某种循环展开。
我认为我的专业知识还不够:)但是,我的结果仍然与你的结果相反。
您可以在此处找到完整的测试代码以及我的机器上的结果以及生成的 IL 代码。
在 C# 中,类是引用类型,结构是值类型。一个主要影响是值类型可以(大多数情况下都是!)分配在堆栈上,而引用类型始终分配在堆上。
因此,每次访问引用类型变量的内部状态时,都需要取消引用指向堆中内存的指针(这是一种跳转),而对于值类型,它已经在堆栈中,甚至优化到寄存器中。
我认为您因此看到了差异。
PS 顺便说一句,“大多数时候”我指的是拳击;它是一种用于将值类型对象放置在堆上的技术(例如,将值类型转换为接口或用于动态方法调用绑定)。