我注意到包裹单个浮子的结构比直接使用浮子的速度明显慢,大约是一半的性能。
using System;
using System.Diagnostics;
struct Vector1 {
public float X;
public Vector1(float x) {
X = x;
}
public static Vector1 operator +(Vector1 a, Vector1 b) {
a.X = a.X + b.X;
return a;
}
}
但是,在添加一个额外的"额外"字段后,似乎会发生一些魔术,并且性能再次变得更加合理:
struct Vector1Magic {
public float X;
private bool magic;
public Vector1Magic(float x) {
X = x;
magic = true;
}
public static Vector1Magic operator +(Vector1Magic a, Vector1Magic b) {
a.X = a.X + b.X;
return a;
}
}
我用来基准测试的代码如下:
class Program {
static void Main(string[] args) {
int iterationCount = 1000000000;
var sw = new Stopwatch();
sw.Start();
var total = 0.0f;
for (int i = 0; i < iterationCount; i++) {
var v = (float) i;
total = total + v;
}
sw.Stop();
Console.WriteLine("Float time was {0} for {1} iterations.", sw.Elapsed, iterationCount);
Console.WriteLine("total = {0}", total);
sw.Reset();
sw.Start();
var totalV = new Vector1(0.0f);
for (int i = 0; i < iterationCount; i++) {
var v = new Vector1(i);
totalV += v;
}
sw.Stop();
Console.WriteLine("Vector1 time was {0} for {1} iterations.", sw.Elapsed, iterationCount);
Console.WriteLine("totalV = {0}", totalV);
sw.Reset();
sw.Start();
var totalVm = new Vector1Magic(0.0f);
for (int i = 0; i < iterationCount; i++) {
var vm = new Vector1Magic(i);
totalVm += vm;
}
sw.Stop();
Console.WriteLine("Vector1Magic time was {0} for {1} iterations.", sw.Elapsed, iterationCount);
Console.WriteLine("totalVm = {0}", totalVm);
Console.Read();
}
}
具有基准结果:
Float time was 00:00:02.2444910 for 1000000000 iterations.
Vector1 time was 00:00:04.4490656 for 1000000000 iterations.
Vector1Magic time was 00:00:02.2262701 for 1000000000 iterations.
编译器/环境设置:OS:Windows 10 64位工具链:VS2017框架:.NET 4.6.2目标:任何CPU都喜欢32位
如果将64位设置为目标,我们的结果更可预测,但比我们在32位目标上看到的vector1magic所看到的明显差:
Float time was 00:00:00.6800014 for 1000000000 iterations.
Vector1 time was 00:00:04.4572642 for 1000000000 iterations.
Vector1Magic time was 00:00:05.7806399 for 1000000000 iterations.
对于真正的巫师,我在此处包括了IL的转储:https://pastebin.com/sz2qlgex
进一步的调查表明,这似乎是特定于Windows运行时的,因为单声道编译器会产生相同的IL。
在单声道运行时,与RAW Float相比,两个结构变体的性能速度较慢约2倍。这与我们在.NET上看到的性能有很大不同。
这里发生了什么?
*请注意,此问题最初包括有缺陷的基准过程(感谢Max Payne指出此问题(,并已更新以更准确地反映时间。
JIT具有称为" struct促进"的优化,它可以用多个本地人有效地替换struct局部或参数,一个用于每个结构的字段。
单个结构包裹的浮子的结构促进是禁用的。原因有点晦涩,但大致:
- 简单地包装原始类型的结构被视为传递给或从呼叫返回的结构大小的整数值
- 在促销分析期间,JIT无法分辨结构是否曾经传给或从通话中返回。
- 将int重新分类为float(反之亦然(所需的代码序列被认为在运行时很昂贵。
- 因此,结构没有被提升,因此在浮点场上的访问和操作较慢。
大致来说,JIT优先考虑降低呼叫站点的成本,而不是改善使用场地的成本。有时(如上所述,在运营成本占主导地位的情况下(这不是正确的电话。
正如您所看到的,如果您使结构更大,则经过和返回结构更改的规则(现在通过参考返回(,并且此解散促销。
在CORECLR资源中,您可以在Compiler::lvaShouldPromoteStructVar
中看到此逻辑。
这不应该发生。显然,这是某种遗漏,迫使JIT无法正如应有的那样工作。
struct Vector1 //Works fast in 32 Bit
{
public double X;
}
struct Vector1 //Works fast in 64 Bit and 32 Bit
{
public double X;
public double X2;
}
您还必须致电: console.writeline(total(; 将时间完全增加到矢量1Magic时间,这是有道理的。问题仍然存在,为什么vector1如此慢。
也许结构未针对sizeof(foo(&lt进行优化。在64位模式下64位。
看来这是7年前的出现:为什么16字节是C#?
CIL代码是相同的(实际上(。但是X86汇编代码不是。
我认为,这是JIT编译器优化的一些功能。
编译器生成Vector1
的汇编代码。
c#(评论中部分组装x86(:
var totalV = new Vector1(0.0f);
/*
01300576 fldz
01300578 fstp dword ptr [ebp-14h]
*/
for (int i = 0; i < iterationCount; i++)
{
var v = new Vector1(i);
/*
0130057D mov dword ptr [ebp-4Ch],ecx ; ecx - is index "i"
01300580 fild dword ptr [ebp-4Ch]
01300583 fstp dword ptr [ebp-4Ch]
01300586 fld dword ptr [ebp-4Ch]
*/
totalV += v;
/*
01300589 lea eax,[ebp-14h]
0130058C mov eax,dword ptr [eax]
0130058E lea edx,[ebp-18h]
01300591 mov dword ptr [edx],eax
01300593 fadd dword ptr [ebp-18h]
01300596 fstp dword ptr [ebp-18h]
01300599 mov eax,dword ptr [ebp-18h]
0130059C mov dword ptr [ebp-14h],eax
*/
}
编译器生成Vector1Magic
的汇编代码。
c#(评论中部分组装x86(:
var totalVm = new Vector1Magic(0.0f);
/*
01300657 mov byte ptr [ebp-20h],1 ; here's assignment "magic=true"
0130065B fldz
0130065D fstp dword ptr [ebp-1Ch]
*/
for (int i = 0; i < iterationCount; i++)
{
var vm = new Vector1Magic(i);
/*
01300662 mov dword ptr [ebp-4Ch],edx ; edx - is index "i"
01300665 fild dword ptr [ebp-4Ch]
01300668 fstp dword ptr [ebp-4Ch]
0130066B fld dword ptr [ebp-4Ch]
*/
totalVm += vm;
/*
0130066E movzx ecx,byte ptr [ebp-20h] ; here's some work with "unused" magic field
01300672 fld dword ptr [ebp-1Ch]
01300675 faddp st(1),st
01300677 fstp dword ptr [ebp-1Ch]
0130067A mov byte ptr [ebp-20h],cl ; here's some work with "unused" magic field
*/
}
显然,此ASM障碍会影响性能:
;Vector1
01300589 lea eax,[ebp-14h]
0130058C mov eax,dword ptr [eax]
0130058E lea edx,[ebp-18h]
01300591 mov dword ptr [edx],eax
01300593 fadd dword ptr [ebp-18h]
01300596 fstp dword ptr [ebp-18h]
01300599 mov eax,dword ptr [ebp-18h]
0130059C mov dword ptr [ebp-14h],eax
;Vector1Magic
0130066E movzx ecx,byte ptr [ebp-20h] ; here's some work with "unused" magic field
01300672 fld dword ptr [ebp-1Ch]
01300675 faddp st(1),st
01300677 fstp dword ptr [ebp-1Ch]
0130067A mov byte ptr [ebp-20h],cl ; here's some work with "unused" magic field
JIT编译器在具有一个字段和几个字段的结构上进行操作不同。可能会在所有字段的Vector1Magic
操作中(也没有"未使用"(。