如何在处理负零的情况下有效比较两个浮点数的符号

19

给定两个浮点数,我正在寻找一种高效的方法来检查它们是否具有相同的符号,假设如果其中任何一个值为零(+0.0或-0.0),则应视为具有相同的符号。

例如,

  • SameSign(1.0, 2.0) 应返回 true
  • SameSign(-1.0, -2.0) 应返回 true
  • SameSign(-1.0, 2.0) 应返回 false
  • SameSign(0.0, 1.0) 应返回 true
  • SameSign(0.0, -1.0) 应返回 true
  • SameSign(-0.0, 1.0) 应返回 true
  • SameSign(-0.0, -1.0) 应返回 true

C++中一个朴素但正确的实现SameSign是:

bool SameSign(float a, float b)
{
    if (fabs(a) == 0.0f || fabs(b) == 0.0f)
        return true;

    return (a >= 0.0f) == (b >= 0.0f);
}

假设采用IEEE浮点数模式,这里有一个 SameSign 的变体,它能编译成无分支代码(至少在Visual C++ 2008中是如此):

bool SameSign(float a, float b)
{
    int ia = binary_cast<int>(a);
    int ib = binary_cast<int>(b);

    int az = (ia & 0x7FFFFFFF) == 0;
    int bz = (ib & 0x7FFFFFFF) == 0;
    int ab = (ia ^ ib) >= 0;

    return (az | bz | ab) != 0;
}

假设已经定义了binary_cast

template <typename Target, typename Source>
inline Target binary_cast(Source s)
{
    union
    {
        Source  m_source;
        Target  m_target;
    } u;
    u.m_source = s;
    return u.m_target;
}

我正在寻找两个东西:

  1. 使用位运算、FPU 技巧甚至 SSE 指令集的更快、更高效的 SameSign 实现。

  2. 将 SameSign 扩展到三个值的高效方法。

编辑:

我对 SameSign 的三种变体进行了一些性能测试(原问题描述的两个变体以及 Stephen 的变体)。每个函数在一个由 101 个随机填充为 -1.0、-0.0、+0.0 和 +1.0 的浮点数数组中的所有相邻值上运行 200-400 次。每个测量重复 2000 次,并保留最小时间(以消除所有缓存效应和系统引起的减速)。代码使用 Visual C++ 2008 SP1 编译,启用最大优化和 SSE2 代码生成。测试在 Core 2 Duo P8600 2.4 Ghz 上完成。

以下是计时结果,不包括从数组获取输入值、调用函数和检索结果的开销(这些开销为 6-7 个时钟周期):

  • Naive 变体:15 个时钟周期
  • Bit magic 变体:13 个时钟周期
  • Stephen 的变体:6 个时钟周期

有特定的编程语言/平台吗? - Michael Myers
嘿,感谢你的好问题 :) 最好使用x86上的C/C++。 - François Beaune
可能是重复的问题:比较两个浮点数是否都为负数或正数。链接:http://stackoverflow.com/questions/2013680/comparing-two-floats-to-see-if-theyre-both-negative-or-both-positive - ChrisF
3个回答

24
如果您不需要支持无穷大,您可以直接使用:
inline bool SameSign(float a, float b) {
    return a*b >= 0.0f;
}

在大多数现代硬件上,它实际上非常快,并且完全可移植。但是,在(零,无穷大)情况下,它不会正常工作,因为零乘以无穷大等于NaN,并且比较将返回false,无论符号如何。当a和b都很小时,它还将在某些硬件上产生异常停顿。


确实,这对于两个值的情况很有效,并具有适当的语义。我唯一担心的是,在三个值的情况下,它需要三次乘法(a * b >= 0.0f && a * c >= 0.0f && b * c >= 0.0f)。 - François Beaune
@François:是的,三值情况是个有趣的难题。我需要再想一下。 - Stephen Canon
2
这是否准确?对我来说,这似乎是显而易见的解决方案,但我也需要一个精确的结果,无论舍入误差如何。在我看来,a*b可能会向0上舍入,然后这个函数计算出错误的值。不过我不确定。 - migle
1
明白了。这不是精确的。ab的符号总是正确的,但由于结果可能舍入为-0,比较>= 0将返回true,这将是错误的结果。请注意,我不是在谈论SameSign(-0,1),而是ab被舍入为-0。让我提出另一个答案。 - migle
微观优化,使用-O3编译。inline bool SameSign(const float& a, const& float b) { return !(a*b < 0.0f); } - Chris

5
也许可以这样表达:

也许类似这样:

inline bool same_sign(float a, float b) {
    return copysignf(a,b) == a;
}

请参阅copysign的man页面,了解它的更多信息(还要检查-0!= +0)

如果您有C99函数,则可能是这样

inline bool same_sign(float a, float b) {
    return signbitf(a) == signbitf(b);
}

作为附注,在gcc上,copysign和signbit都是内置函数,因此它们应该很快。如果您想确保正在使用内置版本,则可以执行__builtin_signbitf(a)。
编辑:这也应该易于扩展到3值情况(实际上这两种情况都应该...)。
inline bool same_sign(float a, float b, float c) {
    return copysignf(a,b) == a && copysignf(a,c) == a;
}

// trust the compiler to do common sub-expression elimination
inline bool same_sign(float a, float b, float c) {
    return signbitf(a) == signbitf(b) && signbitf(a) == signbitf(c);
}

// the manpages do not say that signbit returns 1 for negative... however
// if it does this should be good, (no branches for one thing...)
inline bool same_sign(float a, float b, float c) {
    int s = signbitf(a) + signbitf(b) + signbitf(c);
    return !s || s==3;
}

1
关于signbit的小提示:该宏返回一个int,man手册指出:“如果x的符号位设置,则返回非零值。”这意味着Spudd86的bool same_sign()不能保证在signbit为两个不同的非零int的情况下对于两个不同的负值起作用。
首先转换为bool确保正确的返回值:
inline bool same_sign(float a, float b) {
    return (bool)signbitf(a) == (bool)signbitf(b);
}

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