【问题标题】:Is it possible to write Quake's fast InvSqrt() function in C#?是否可以在 C# 中编写 Quake 的快速 InvSqrt() 函数?
【发布时间】:2008-11-06 14:25:12
【问题描述】:

这只是为了满足我自己的好奇心。

有没有这样的实现:

float InvSqrt (float x)
{
   float xhalf = 0.5f*x;
   int i = *(int*)&x;
   i = 0x5f3759df - (i>>1);
   x = *(float*)&i;
   x = x*(1.5f - xhalf*x*x);
   return x;
}

在 C# 中?如果存在,请发布代码。

我想我应该提到我正在寻找一个“安全”的实现......无论哪种方式,BitConverter 代码都可以解决问题。联合的想法很有趣。我会测试它并发布我的结果。

编辑: 正如预期的那样,不安全的方法是最快的,其次是使用联合(在函数内部),然后是 BitConverter。这些函数执行了 10000000 次,我使用 System.Diagnostics.Stopwatch 类进行计时。计算结果显示在括号中。

Input: 79.67
BitConverter Method: 00:00:01.2809018 (0.1120187)
Union Method: 00:00:00.6838758 (0.1120187)
Unsafe Method: 00:00:00.3376401 (0.1120187)

为了完整起见,我测试了内置的 Math.Pow 方法和“naive”方法 (1/Sqrt(x))。

Math.Pow(x, -0.5): 00:00:01.7133228 (0.112034710535584)
1 / Math.Sqrt(x): 00:00:00.3757084 (0.1120347)

1 / Math.Sqrt() 之间的差异是如此之小,以至于我认为不需要求助于 C# 中的 Unsafe Fast InvSqrt() 方法(或任何其他不安全的方法)。除非一个真的需要从 CPU 中榨出最后一点汁液...... 1/Math.Sqrt() 也更准确。

【问题讨论】:

  • 为了完整起见,您还应该使用“1/math.Sqrt()”运行测试。
  • 我做到了,还有另一种情况。我会在验证结果后立即更新基准。
  • 我认为您应该使用更大的集合运行测试,这样它们将需要几秒钟才能完成。

标签: c#


【解决方案1】:

您应该能够使用 StructLayout 和 FieldOffset 属性为浮点数和整数等普通旧数据伪造联合。

[StructLayout(LayoutKind.Explicit, Size=4)]
private struct IntFloat {
    [FieldOffset(0)]
    public float floatValue;

    [FieldOffset(0)]
    public int intValue;

    // redundant assignment to avoid any complaints about uninitialized members
    IntFloat(int x) {
        floatValue = 0;
        intValue = x;
    }

    IntFloat(float x) { 
        intValue = 0;
        floatValue = x;
    }

    public static explicit operator float (IntFloat x) {
        return x.floatValue;
    }

    public static explicit operator int (IntFloat x) { 
        return x.intValue;
    }

    public static explicit operator IntFloat (int i) {
        return new IntFloat(i);
    }
    public static explicit operator IntFloat (float f) { 
        return new IntFloat(f);
    }
}

那么翻译 InvSqrt 就很简单了。

【讨论】:

  • 只是一个警告,我刚刚使用这个技巧实现了(只是为了好玩)FastInvSqrt,它比使用更直接的指针实现要慢得多。当然,这些天即使使用指针 FastInvSqrt 也比 1/Math.Sqrt(x) 慢...
  • 并不十分惊讶。感谢您的反馈! =)
  • 附带说明:github.com/ekmett/approximate/blob/master/cbits/fast.c 提供了近似的 log、exp、pow 等。这些至少在时钟周期方面似乎是一个胜利,但在 C# 中可能不是。 =)
  • 确认会慢一些,虽然不是很慢,很好的代码,很高兴知道 StructLayout 的东西。另请注意,重复返回之前的行会给您额外的精确度(也需要更多时间)。
【解决方案2】:

如果您想避免不安全的代码,请使用BitConverter

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = BitConverter.SingleToInt32Bits(x);
    i = 0x5f3759df - (i >> 1);
    x = BitConverter.Int32BitsToSingle(i);
    x = x * (1.5f - xhalf * x * x);
    return x;
}

上面的代码使用了 .NET Core 2.0 中引入的新方法。对于 .NET Framework,您必须回退到以下内容(执行分配):

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = BitConverter.ToInt32(BitConverter.GetBytes(x), 0);
    i = 0x5f3759df - (i >> 1);
    x = BitConverter.ToSingle(BitConverter.GetBytes(i), 0);
    x = x * (1.5f - xhalf * x * x);
    return x;
}

否则C#代码和你给的C代码一模一样,只是方法需要标记为不安全:

unsafe float InvSqrt(float x) { ... }

【讨论】:

  • 但这确实留下了一个问题:“使用 BitConverter 仍然很快吗?”
  • BitConverter 方法确实会导致两个数组分配,这可能是一个性能问题。 (不幸的是,没有 BitConverter.SingleToInt32Bits 方法,类似于 BitConverter.DoubleToInt64Bits,它应该既快速又可内联。)
  • 在 .NET Core BitConverter 中允许读写Span<T>。然后不需要进行任何分配。
  • @Sebazzz .NET Core 还在我 11 岁的评论中添加了我希望的缺失的 BitConverter.SingleToInt32Bits 方法。我已经更新了答案,因为它比使用 Span 相关方法要简单得多。
【解决方案3】:

在不安全模式下绝对有可能。请注意,尽管在 Quake 3 源代码中使用了常量 0x5f3759df,但 numerical research showed 常量 0x5f375a86 实际上为牛顿近似产生了更好的结果。

【讨论】:

    【解决方案4】:

    我不明白为什么不能使用 不安全的编译器选项

    【讨论】:

    • 如果有人统一编程,他有这个原因:由于安全原因,某些设备和网络播放器不支持不安全代码。
    猜你喜欢
    • 2020-03-23
    • 2013-06-08
    • 1970-01-01
    • 2021-03-21
    • 2015-09-22
    • 1970-01-01
    • 2010-12-20
    • 2020-06-20
    • 1970-01-01
    相关资源
    最近更新 更多