哪种方法是获取一个数的绝对值最快的方式?

51

如何实现一个返回数字绝对值的操作,哪种方法最快?

x=root(x²)
或者
if !isPositive(x):
    x=x*(-1)

实际上,这个问题可以翻译为,“if有多快(以及为什么)?”

我的大学编程教授总是告诉我要避免使用if,因为它们非常慢,但我总是忘记问有多慢和为什么。这里是否有人知道呢?


这是绝对值,不是模数。 - kquinn
至少在罗马尼亚,我们使用英语中“modulus”/“module”的等效词来表示“绝对值”。我认为这种现象在其他语言中也很普遍。 - Eduard - Gabriel Munteanu
啊,在美式英语中,绝对值是指在数轴上距离0的距离。也就是说,-4的绝对值是4,12的绝对值是12。 - Perchik
6
尽管维基百科似乎提到在表示“绝对值”时使用“模数”一词:http://en.wikipedia.org/wiki/Absolute_value - Eduard - Gabriel Munteanu
6
我认为这些英语纯粹主义者无法区分模和取模。"Modulus"是一个有效的英语术语,可用于指代实数或复数的绝对值。 - Violet Giraffe
1
平方/平方根方法也容易发生溢出。 - user5483294
16个回答

0

我正在使用C语言进行8088/8086的复古图形编程,调用abs()函数会消耗时间,因此我已经将其替换为:

/* assuming 'i' is int; this WILL NOT WORK on floating point */
if (i < 0) {
    i = ~i + 1;
}

这样做更快的原因是因为它本质上将汇编中的一个CALL交换成了JNE。调用方法会改变一些寄存器,推送更多的寄存器,将参数推送到堆栈上,并且可能会清空预取队列。而且,这些操作需要在函数结束时被撤销,所有这些对CPU来说都非常昂贵。

2
任何现代编译器都可以将 abs 内联到编译效率至少与此相同的代码中(例如,在现代 x86 上使用 neg/cmov)。自己执行 2 的补码位操作并没有什么用处;你可能会选择使用 i = -i,因为 x86 有一个比 NOT / INC 更快的 neg 指令(如果你有一个天真的编译器,它无法识别 2 的补码恒等式并将其优化回 negsub)。 - Peter Cordes

0

什么更快很大程度上取决于你所针对的编译器和CPU。在大多数CPU和所有编译器上,x = (x>=0)? x:-x; 是获取绝对值最快的方法,但实际上,通常标准函数已经提供了这个解决方案(例如fabs())。它被编译成比较后跟条件赋值指令(CMOV),而不是条件跳转。但有些平台缺乏该指令。尽管如此,英特尔(但不是微软或GCC)编译器会自动将if()转换为条件赋值,并且甚至会尝试优化周期(如果可能)。

总体而言,分支代码比条件赋值慢,如果CPU使用统计预测,则if()的平均速度可能较慢,如果操作重复多次并且条件结果不断变化。像英特尔这样的CPU将开始计算两个分支,并且会删除无效的分支,在大型if()主体或大量周期的情况下,这可能是关键的。

现代英特尔CPU上的sqr()和sqrt()是单个内置指令,速度不慢,但它们不精确,加载寄存器也需要时间。

相关问题:为什么CPU分支指令很慢?

很可能,教授希望学生对这个问题进行研究,这是一个半挑衅性的问题/任务,如果学生能够独立思考并寻找额外的来源,那么只会有好处。

gcc会将if转换为无分支的CMOV。参见gcc优化标志-O3使代码比-O2慢,其中对于排序数据它会适得其反。在x86上,sqrt是一条指令,但速度较慢,并且仅适用于float/double/long double,而不适用于整数。吞吐量/延迟数字类似于(但比)FP除法更慢:浮点除法与浮点乘法 - Peter Cordes
整数乘法非常快,但这几乎与abs无关。只需使用mov / neg/cmov即可在3个uop和2个周期延迟内完成。 - Peter Cordes

0
如果你只是比较两个数的绝对值(例如,在比较之后不需要任何一个数的绝对值),那么只需将这两个数都平方,使它们变为正数(去除每个数的符号),较大的平方将大于较小的平方。

0

我在想,这个解决方案是否有问题。

  • 没有分支
  • 没有位宽相关的移位
  • 没有位操作
  • 没有架构依赖性
  • 没有编译器依赖性
  • 可选:对于INT_MIN没有未定义行为

也许指令太多了?

我的解决方案

xabs = (x < 0)*(-x) + (x >=0)*x
  • 2个整数比较
  • 2个乘法运算

旧解决方案

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
  • 5个整数比较
  • 3个整数乘法

不幸的是,我从未进行过速度比较。因此,我不知道它是否真的比……更快。

if ( x < 0 )
{
  if ( x >= -INT_MAX )
  {
    x = -x;
  }
  else
  {
    x = INT_MAX;
  }
}
     

0

为了完整起见,如果您正在处理浮点数,可以始终执行类似于 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 函数很可能会执行这个确切的操作,最好在可用时使用它。如果你很幸运,这个函数将只是几条指令,并且会被内联。


-1

对于负数列表:

如果您的内存中存储了零,则可以简单地使用0-x,其中x是负数。

或者如果您的内存中没有存储零:

x-x-x,其中x是负数。

或者,为了清晰起见,使用括号:

(x) - (x) - (x) => (-n) - (-n) - (-n),其中x=-n

即从自身减去负数以得到零,然后从零减去它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接