这是一个函数,每帧需要运行一次,因此在性能方面非常关键。该函数包含一个循环及其中的操作。
private int MyFunction(int number)
{
// Code
for (int i = 0; i <= 10000; i++)
{
var value = i * number
var valuePow2 = value * value;
// Some code which uses valuePow2 several times
}
return 0; // Not actual line
}
现在,由于数学特性,我们知道(a * b)²等于a²*b²
所以,有可能将我的功能变为:
private int MyFunction(int number)
{
// Code
var numberPow2 = number * number;
for (int i = 0; i <= 10000; i++)
{
var iPow2 = i * i
var valuePow2 = numberPow2 * iPow2;
// Some code which uses valuePow2 several times
}
return 0; // Not actual line
}
直观地说,这似乎应该更快,因为数字2不变,现在只在循环外计算一次。至少,这对人类来说要快得多,因为x²操作在循环期间以更小的数量完成。
我想知道的是,在C#中,当你使用像int这样的类型时,乘法实际上会更快,数字更小吗?
例如,5 * 5的执行速度是否会超过5000 * 5000?
如果是这样,那么第二个版本更好,即使是一小部分,因此。
但是,如果对于给定的数据类型,时间是恒定的,那么函数的第一个版本更好,因为一半的计算将在较小的数字上完成,因为我在循环中执行相同数量的乘法,但是在第二个版本中,我在开始之前做了一次额外的乘法。
我知道,出于所有意图和目的,性能差异可以忽略不计。我在Code Review中建议了第二个版本,因为该函数很关键,我找不到任何文档来支持这两个视图。
例如,5 * 5的执行速度是否会超过5000 * 5000?
对于编译时常数,5 * x
比5000 * x
便宜,因为前者可以用lea eax, [rdi + rdi*4]
完成。
但对于运行时变量,唯一具有数据依赖性能的整数指令是除法。这适用于任何主流CPU:流水线技术非常重要,即使某些情况下可以以较低的延迟运行,它们通常也不会,因为这会使调度更加困难。 (你不能让相同的执行单元在同一个周期内产生2个结果;相反,CPU只是想知道在一个周期内输入输入肯定会导致3个周期后出现的答案。)
(对于FP,同样只有division和sqrt在普通CPU上具有数据相关的性能。)
如果分支以不同的方式使用具有任何数据相关分支的整数或FP的代码可能会慢得多。 (例如,对于二元搜索的一个跳跃序列,“训练”分支预测;使用另一个键进行搜索将会更慢,因为它将至少错误地预测一次。)
并且为了记录,使用Math.Pow
而不是整数*
的建议是疯狂的。简单地将整数转换为double
并返回比使用整数乘法本身更慢。
亚当的答案链接了一个基于大数组的基准,可以实现自动矢量化。 SSE / AVX2只有32位整数乘法。而64位需要更多的内存带宽。这也是为什么它显示16位和8位整数的加速比。所以它发现c=a*b
在Haswell CPU上以半速运行,但这不适用于你的循环情况。
在标量代码中,imul r64, r64
在英特尔主流CPU(至少是Nehalem)和Ryzen(imul r32, r32
)上与https://agner.org/optimize/具有相同的性能。 1个uop,3个周期延迟,1个/时钟吞吐量。
它只是AMD Bulldozer系列,AMD Atom和Silvermont,64位标量乘法更慢。 (当然假设是64位模式!在32位模式下,使用64位整数的速度较慢。)
对于number
的固定值,编译器可以并且将优化它到i*number
,而不是重新计算inum += number
。这被称为strength-reduction optimization,因为加法比乘法更“弱”(稍微便宜)。
for(...) {
var value = i * number
var valuePow2 = value * value;
}
可以编译成asm做类似的东西
var value = 0;
for(...) {
var valuePow2 = value * value;
...
value += number;
}
如果编译器没有为您执行此操作,您可以尝试以这种方式手动编写它。
但整数乘法非常便宜,并且在现代CPU上完全流水线化,尤其如此。它的延迟略高于添加,并且可以在更少的端口上运行(通常每个时钟吞吐量只有1个而不是4个用于添加),但是你说你在使用valuePow2
做了大量工作。这应该让无序执行隐藏延迟。
如果检查asm并且编译器正在使用递增1的单独循环计数器,您还可以尝试手动保持编译器优化循环以使用value
作为循环计数器。
var maxval = number * 10000;
for (var value = 0; i <= maxval; value += number) {
var valuePow2 = value * value;
...
}
如果number*10000
可以溢出,请小心,如果你需要它正确包装。在这种情况下,此循环将运行更少的迭代。 (除非number
是如此之大,以至于value += number
也包裹......)
对于典型的处理器,无论这些整数中的数据如何,乘以两个32位整数将占用相同的周期数。 Most current processors will take nearly twice the time to multiply 64-bit integers as it takes to multiply 32-bit integers.
我确实注意到你的两个代码都有问题。当你乘以两个整数时,它返回一个int类型。 var类型将类型设置为返回值。这意味着,valuePow2将是一个int。由于你的循环达到10000,如果数字是5或更大,那么你将溢出valuePow2。
如果您不想溢出int,可以将代码更改为
private int MyFunction(int number)
{
// Code
for (int i = 0; i <= 10000; i++)
{
long value = i * number; //64bit multiplication
long valuePow2 = value * value; //64bit multiplication
// Some code which uses valuePow2 several times
}
return 0; // Not actual line
}
修改后的代码应该更快,因为您可以将64位乘法更改为32位乘法
private int MyFunction(int number)
{
// Code
long numberPow2 = number * number; //64bit multiplication
for (int i = 0; i <= 10000; i++)
{
int iPow2 = i * i; //32bit multiplication
long valuePow2 = numberPow2 * iPow2; //64bit multiplication
// Some code which uses valuePow2 several times
}
return 0; // Not actual line
}
但是CPU中的电路和编译器的优化可以改变最终运行的循环次数。在一天结束时,你说得最好:
我知道,出于所有意图和目的,性能差异可以忽略不计。