C#基本操作时间如何随数字大小而变化?
它的上下文是一个函数,每个帧几乎需要运行一次,因此在性能方面非常关键。此函数包含一个循环及其内部的操作C#基本操作时间如何随数字大小而变化?,c#,performance,optimization,multiplication,micro-optimization,C#,Performance,Optimization,Multiplication,Micro Optimization,它的上下文是一个函数,每个帧几乎需要运行一次,因此在性能方面非常关键。此函数包含一个循环及其内部的操作 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
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
}
private int MyFunction(int number)
{
//代码
对于(int i=0;i对于典型的处理器,将两个32位整数相乘将花费相同的周期量,而不管这些整数中的数据如何
我确实注意到两个代码中都有一个问题。当你将两个int相乘时,它返回一个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
}
private int MyFunction(int number)
{
//代码
对于(int i=0;i
例如,5*5的执行速度会比5000*5000快吗
对于编译时常量,5*x
比5000*x
便宜,因为前者可以通过leaeax、[rdi+rdi*4]
实现
但对于运行时变量,唯一具有数据相关性能的整数指令是除法。这适用于任何主流CPU:流水线非常重要,即使某些情况下可以以较低的延迟运行,它们通常也不会,因为这会使调度更加困难。(不能让同一个执行单元在同一个周期内产生两个结果;相反,CPU只想知道,在一个周期内输入数据肯定会在3个周期后得到答案。)
(对于FP,同样只有division和sqrt在正常CPU上具有数据相关的性能。)
如果分支方向不同,则使用整数或具有任何数据相关分支的FP的代码可能会慢得多。(例如,对于二进制搜索,分支预测在一个跳转序列上“训练”;使用另一个键搜索会慢得多,因为它至少会预测失误一次。)
作为记录,使用Math.Pow
而不是integer*
的建议是愚蠢的。简单地将整数转换为double
并返回比用整数乘法自身相乘要慢
Adam的答案链接了一个在一个大数组上循环的基准测试,可以实现自动矢量化。SSE/AVX2只有32位整数乘法。
64位占用更多的内存带宽。这也是为什么它显示16位和8位整数的加速。因此它发现在Haswell CPU上以半速度运行的c=a*b
,但这不适用于循环情况
在标量代码中,imul r64、r64
在英特尔主流CPU(至少是Nehalem)和Ryzen()上的性能与imul r32、r32
相同。均为1 uop、3周期延迟、1/时钟吞吐量
这是唯一的AMD推土机系列,AMD Atom和Silvermont,其中64位标量乘法较慢。(当然,假设64位模式!在32位模式下,使用64位整数较慢。)
优化你的循环
对于固定值number
,编译器可以并将其优化为inum+=number
,而不是重新计算i*number
。这称为,因为加法运算比乘法运算“弱”(稍微便宜)
for(...) {
var value = i * number
var valuePow2 = value * value;
}
可以编译成asm,其功能如下
var value = 0;
for(...) {
var valuePow2 = value * value;
...
value += number;
}
您可以尝试用这种方式手工编写,以防编译器不为您编写
但是整数乘法非常便宜,尤其是在现代CPU上完全流水线。它的延迟比add稍高,并且可以在更少的端口上运行(通常每个时钟吞吐量只有1个,而add只有4个),但您说您正在使用valuePow2
进行重要的工作。这应该会让无序执行隐藏延迟
如果您检查asm,并且编译器正在使用一个单独的循环计数器递增1,那么您也可以尝试手持编译器来优化循环,以使用value
作为循环计数器
var maxval = number * 10000;
for (var value = 0; i <= maxval; value += number) {
var valuePow2 = value * value;
...
}
var maxval=数量*10000;
对于(var value=0;i您不能基于直觉、猜测、评论或文档进行优化。请使用探查器来衡量性能。为什么不使用Math.Pow
?这是最快的速度。基于基本操作的数字类型中包含的数字的大小,性能应该不会有任何差异。FundamentallY,你的两个循环是相同的:它们每个都有两个乘法。考虑使用LIQPAD来计时小的代码段,但是实现精确的时间不是那么简单。而且,没有分析,你可能正在优化错误的代码。@ ZEL0,因为从我所看到的,数学。当Pow知道的时候,它比直接乘法慢得多。n和是2在你的例子中,乘法和Math.Pow
是不等价的,因为你没有使用double
数学。Pow
只对double实现。你使用的是int
。要使用Math.Pow
,你必须先转换成double,然后在完成后再转换回来。这是两个或者三个不需要直接乘法的转换操作。这是您提出的一个非常有趣的观点,有可能将64位乘法转换为32位。不幸的是,尽管它适用于我问题中的代码,但它已经被大大简化(和更改)对于这个问题,这在我的实际案例中是不可能的。不过,我会记住这些信息,因为我可能会在将来遇到它起作用的案例,或者能够修改一个以使其起作用。@KaitoKid:Adam链接的基准是在一个大数组上循环的,可以自动矢量化。(并且SSE/AVX2只有32位整数乘法。64位占用更多的内存带宽)。在标量码中,imul r64,r64
具有相同的性能
var value = 0;
for(...) {
var valuePow2 = value * value;
...
value += number;
}
var maxval = number * 10000;
for (var value = 0; i <= maxval; value += number) {
var valuePow2 = value * value;
...
}