我进行了一些基准测试,以比较双打和浮动性能。我非常惊讶地看到双打比浮球快得多。
我看到了一些关于此的讨论,例如:
Is using double faster than float?
Are doubles faster than floats in c#?
他们中的大多数人表示,由于双精度优化等原因,双重和浮动性能可能相似。但是当我使用双打时,我看到了 x2的性能提升!这怎么可能?最糟糕的是,我使用的是一台32位机器,根据一些帖子确实可以更好地用于花车......
我使用C#进行了精确检查,但我发现类似的C ++实现具有类似的行为。
我曾经检查过的代码:
static void Main(string[] args)
{
double[,] doubles = new double[64, 64];
float[,] floats = new float[64, 64];
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
s.Restart();
CalcDoubles(doubles);
s.Stop();
long doubleTime = s.ElapsedMilliseconds;
s.Restart();
CalcFloats(floats);
s.Stop();
long floatTime = s.ElapsedMilliseconds;
Console.WriteLine("Doubles time: " + doubleTime + " ms");
Console.WriteLine("Floats time: " + floatTime + " ms");
}
private static void CalcDoubles(double[,] arr)
{
unsafe
{
fixed (double* p = arr)
{
for (int b = 0; b < 192 * 12; ++b)
{
for (int i = 0; i < 64; ++i)
{
for (int j = 0; j < 64; ++j)
{
double* addr = (p + i * 64 + j);
double arrij = *addr;
arrij = arrij == 0 ? 1.0f / (i * j) : arrij * (double)i / j;
*addr = arrij;
}
}
}
}
}
}
private static void CalcFloats(float[,] arr)
{
unsafe
{
fixed (float* p = arr)
{
for (int b = 0; b < 192 * 12; ++b)
{
for (int i = 0; i < 64; ++i)
{
for (int j = 0; j < 64; ++j)
{
float* addr = (p + i * 64 + j);
float arrij = *addr;
arrij = arrij == 0 ? 1.0f / (i * j) : arrij * (float)i / j;
*addr = arrij;
}
}
}
}
}
}
我正在使用非常弱的笔记本电脑:Intel Atom N455处理器(双核,1.67GHz,32位),2GB RAM。
答案 0 :(得分:10)
这看起来抖动优化器在这里丢球,它不会抑制浮动情况下的冗余存储。热代码是1.0f / (i * j)
计算,因为所有数组值都为0. x86抖动生成:
01062928 mov eax,edx ; eax = i
0106292A imul eax,esi ; eax = i * j
0106292D mov dword ptr [ebp-10h],eax ; store to mem
01062930 fild dword ptr [ebp-10h] ; convert to double
01062933 fstp dword ptr [ebp-10h] ; redundant store, convert to float
01062936 fld dword ptr [ebp-10h] ; redundant load
01062939 fld1 ; 1.0f
0106293B fdivrp st(1),st ; 1.0f / (i * j)
0106293D fstp dword ptr [ecx] ; arrij = result
x64抖动:
00007FFCFD6440B0 cvtsi2ss xmm0,r10d ; (float)(i * j)
00007FFCFD6440B5 movss xmm1,dword ptr [7FFCFD644118h] ; 1.0f
00007FFCFD6440BD divss xmm1,xmm0 ; 1.0f / (i * j)
00007FFCFD6440C1 cvtss2sd xmm0,xmm1 ; redundant store
00007FFCFD6440C5 cvtsd2ss xmm0,xmm0 ; redundant load
00007FFCFD6440C9 movss dword ptr [rax+r11],xmm0 ; arrij = result
我用“冗余”标记了多余的指令。优化器确实设法在 double 版本中消除它们,以便代码运行得更快。
冗余存储实际上存在于由C#编译器生成的IL中,优化器的工作是检测和删除它们。值得注意的是,x86和x64抖动都有这个缺陷,所以它看起来像是优化算法中的一般疏忽。
x64代码特别值得注意的是将float结果转换为double然后再转换为float,这表明底层问题是数据类型转换,它不知道如何抑制。你也可以在x86代码中看到它,冗余存储实际上是一个双浮点转换。在x86情况下,消除转换看起来很困难,因此很可能已经泄漏到x64抖动中。
请注意,x64代码的运行速度明显快于x86代码,因此请确保将平台目标设置为AnyCPU以获得简单的胜利。至少部分加速是优化器在提升整数乘法时的聪明才智。
确保测试真实数据,由于未初始化的数组内容,您的测量基本上无效。对于元素中的非零数据,差异不太明显,这使得除法更加昂贵。
另请注意你在双重案例中的错误,你不应该使用1.0f。
答案 1 :(得分:3)
来自C#规范:
可以以高于的精度执行浮点运算 操作的结果类型。例如,一些硬件 体系结构支持“扩展”或“长双”浮点 类型比双重类型具有更大的范围和精度,和 使用更高的值隐式执行所有浮点运算 精密型。只有在性能成本过高的情况下才能如此 使硬件架构执行浮点运算 精度较低,而不是需要实现 没有性能和精度,C#允许更高的精度 用于所有浮点运算的类型。以外 提供更精确的结果,这很少有任何可衡量的 效果。但是,在x * y / z形式的表达式中,其中 乘法产生的结果超出双倍范围,但是 随后的分裂将临时结果带回 双范围,表达式评估更高的事实 范围格式可能会导致生成有限结果而不是 无穷大。
在将值存储到数组之前,可能需要额外的指令将值转换为32位浮点数。
此外,如accepted answer中所述,您链接到的一个问题是,CLI规范要求在某些其他情况下截断64位(或80位)值。该答案还与此处的其他讨论有关:
http://weblog.ikvm.net/PermaLink.aspx?guid=f300c4e1-15b0-45ed-b6a6-b5dc8fb8089e