最初我认为
Math.Sign
是正确的方法,但经过测试后,似乎它对 -0.0
和 +0.0
的处理方式相同。
这是一个糟糕的黑客方法:
private static readonly long NegativeZeroBits =
BitConverter.DoubleToInt64Bits(-0.0);
public static bool IsNegativeZero(double x)
{
return BitConverter.DoubleToInt64Bits(x) == NegativeZeroBits;
}
基本上这是测试 -0.0 的精确位模式,但无需对其进行硬编码。
经过一番搜索,我终于找到了 C# 规范的第 7.7.2 节,并提出了这个解决方案。
private static bool IsNegativeZero(double x)
{
return x == 0.0 && double.IsNegativeInfinity(1.0 / x);
}
public static bool IsNegativeZero(double value) {
if (value != 0) return false;
int index = BitConverter.IsLittleEndian ? 7 : 0;
return BitConverter.GetBytes(value)[index] == 0x80;
}
编辑:正如OP指出的,这在发布模式下不起作用。 x86 JIT 优化器认真对待 if() 语句并直接加载零,而不是加载
value。这确实更具性能。但这会导致负零丢失。需要对代码进行调整以防止出现这种情况:
public static bool IsNegativeZero(double value) {
int index = BitConverter.IsLittleEndian ? 7 : 0;
if (BitConverter.GetBytes(value)[index] != 0x80) return false;
return value == 0;
}
顺便说一句,这是 x86 抖动的典型行为,它在优化浮点代码时不能很好地处理极端情况。 x64 抖动在这方面要好得多。尽管可以说没有比赋予负零意义更糟糕的极端情况了。预先警告。
x == 0 && 1 / x < 0
Equals
上的
struct
将进行按位比较,而不是对其成员调用
Equals
:
struct Negative0
{
double val;
public static bool Equals(double d)
{
return new Negative0 { val = -0d }.Equals(new Negative0 { val = d });
}
}
Negative0.Equals(0); // false
Negative0.Equals(-0.0); // true
bool IsNegative(double value)
{
const ulong SignBit = 0x8000000000000000;
return ((ulong)BitConverter.DoubleToInt64Bits(value) & SignBit) == SignBit;
}
[StructLayout(LayoutKind.Explicit)]
private struct DoubleULong
{
[FieldOffset(0)]
public double Double;
[FieldOffset(0)]
public readonly ulong ULong;
}
bool IsNegative(double value)
{
var du = new DoubleULong { Double = value };
return ((du.ULong >> 62) & 2) == 2;
}
后者在调试方面提供了大约 50% 的性能提升。一旦以发布模式编译并从命令行运行
没有显着差异。
我也无法使用不安全的代码来提高性能,但这可能是由于我缺乏经验。
我有点困惑,在坐在这里超过 13 年(在撰写本文时)之后,这个问题有所有这些优秀(并且有些复杂)的答案,但不是最“直接”(IMO)的答案:
static bool IsNegativeZero(double d) => Double.IsNegative(d) && d == 0.0;
请注意,根据 IEEE 规范,-0.0 相当于 +0.0,因此无需编写 d == -0.0
,尽管这也可以。