如何实现一个返回数字绝对值的操作,哪种方法最快?
x=root(x²)
或者if !isPositive(x):
x=x*(-1)
实际上,这个问题可以翻译为,“if
有多快(以及为什么)?”
我的大学编程教授总是告诉我要避免使用if
,因为它们非常慢,但我总是忘记问有多慢和为什么。这里是否有人知道呢?
如何实现一个返回数字绝对值的操作,哪种方法最快?
x=root(x²)
或者if !isPositive(x):
x=x*(-1)
实际上,这个问题可以翻译为,“if
有多快(以及为什么)?”
我的大学编程教授总是告诉我要避免使用if
,因为它们非常慢,但我总是忘记问有多慢和为什么。这里是否有人知道呢?
我正在使用C语言进行8088/8086的复古图形编程,调用abs()
函数会消耗时间,因此我已经将其替换为:
/* assuming 'i' is int; this WILL NOT WORK on floating point */
if (i < 0) {
i = ~i + 1;
}
CALL
交换成了JNE
。调用方法会改变一些寄存器,推送更多的寄存器,将参数推送到堆栈上,并且可能会清空预取队列。而且,这些操作需要在函数结束时被撤销,所有这些对CPU来说都非常昂贵。abs
内联到编译效率至少与此相同的代码中(例如,在现代 x86 上使用 neg
/cmov
)。自己执行 2 的补码位操作并没有什么用处;你可能会选择使用 i = -i
,因为 x86 有一个比 NOT
/ INC
更快的 neg
指令(如果你有一个天真的编译器,它无法识别 2 的补码恒等式并将其优化回 neg
或 sub
)。 - Peter Cordes什么更快很大程度上取决于你所针对的编译器和CPU。在大多数CPU和所有编译器上,x = (x>=0)? x:-x; 是获取绝对值最快的方法,但实际上,通常标准函数已经提供了这个解决方案(例如fabs())。它被编译成比较后跟条件赋值指令(CMOV),而不是条件跳转。但有些平台缺乏该指令。尽管如此,英特尔(但不是微软或GCC)编译器会自动将if()转换为条件赋值,并且甚至会尝试优化周期(如果可能)。
总体而言,分支代码比条件赋值慢,如果CPU使用统计预测,则if()的平均速度可能较慢,如果操作重复多次并且条件结果不断变化。像英特尔这样的CPU将开始计算两个分支,并且会删除无效的分支,在大型if()主体或大量周期的情况下,这可能是关键的。
现代英特尔CPU上的sqr()和sqrt()是单个内置指令,速度不慢,但它们不精确,加载寄存器也需要时间。
相关问题:为什么CPU分支指令很慢?
很可能,教授希望学生对这个问题进行研究,这是一个半挑衅性的问题/任务,如果学生能够独立思考并寻找额外的来源,那么只会有好处。sqrt
是一条指令,但速度较慢,并且仅适用于float/double/long double,而不适用于整数。吞吐量/延迟数字类似于(但比)FP除法更慢:浮点除法与浮点乘法。 - Peter Cordesabs
无关。只需使用mov
/ neg
/cmov
即可在3个uop和2个周期延迟内完成。 - Peter Cordes我在想,这个解决方案是否有问题。
INT_MIN
没有未定义行为也许指令太多了?
我的解决方案
xabs = (x < 0)*(-x) + (x >=0)*x
旧解决方案
xtest = (x < 0)*x; // xtest = x if is negative, otherwise zero
xabs = (x - xtest) - xtest; // Order of instructions taken into account
负 INT_MIN
的未定义行为
如果您的值在算法中没有受限,则可以添加一个检查未定义行为(对 INT_MIN
取反)。
但这会使它变得更加复杂。
也许,有人可以找到更简单的逻辑。
xabs = (x < -INT_MAX)*INT_MAX // x < -INT_MAX < 0 --> xabs = INT_MAX
+ ((x >= -INT_MAX)&&(x < 0))*(-x) // -INT_MAX =< x < 0 --> xabs = -x
+ (x >= 0)*x // 0 <= x --> xabs = +x
不幸的是,我从未进行过速度比较。因此,我不知道它是否真的比……更快。
if ( x < 0 )
{
if ( x >= -INT_MAX )
{
x = -x;
}
else
{
x = INT_MAX;
}
}
为了完整起见,如果您正在处理浮点数,可以始终执行类似于 n * sign(n)
的操作,其中 sign
是一个函数,如果数字是正数则返回+1,如果是负数则返回-1。在C语言中,这将类似于 copysign(1.0, n)
或 (n > 0) - (n < 0)
。
大多数机器现在使用IEEE 754作为其浮点格式,因此您可以直接清除符号位:
float fabs(float x) {
char *c = &x;
c[0] &= 7;
return *(float *)c;
}
考虑到 abs
函数很可能会执行这个确切的操作,最好在可用时使用它。如果你很幸运,这个函数将只是几条指令,并且会被内联。
对于负数列表:
如果您的内存中存储了零,则可以简单地使用0-x
,其中x
是负数。
或者如果您的内存中没有存储零:
x-x-x
,其中x
是负数。
或者,为了清晰起见,使用括号:
(x) - (x) - (x)
=> (-n) - (-n) - (-n)
,其中x=-n
即从自身减去负数以得到零,然后从零减去它。