【问题标题】:How to operate (fast) on mantissa and exponent part of double or float at c++?如何在c ++中对double或float的尾数和指数部分进行操作(快速)?
【发布时间】:2016-01-09 19:40:56
【问题描述】:

我使用 c++ 来计算各种类型的特殊函数(例如 Lambert 函数、求值求逆的迭代方法等)。在许多情况下,直接使用尾数和指数显然有更好的方法。

我找到了很多如何提取尾数和指数部分的答案,但是所有这些都只是“计算速度不是很有效的学术案例”,对我来说有点没用(我使用尾数和指数操作的动机在于提高计算速度)。有时我需要调用一些特定的函数大约十亿次(非常昂贵的计算),所以每次保存的计算工作都很好。并且使用将尾数作为双精度返回的“freexp”不是很合适。

我的问题是(针对具有 IEEE 754 浮点的 c++ 编译器):

1) 如何读取 float/double 尾数的特定位?

2) 如何将整个尾数读入 float/double 的整数/字节?

3) 与 1)、2) 相同的问题。

4) 写作题与 1)、2)、3) 相同。

如果我直接使用尾数或指数,我的动机是更快的计算。我想一定有一个非常简单的解决方案。

【问题讨论】:

  • 在网上看一下IEE754,一切都在详细解释。我严重怀疑你真的需要这样的东西......
  • 有一个非常简单的,不可移植的解决方案。如果我可以这么说,你会做一些奇怪的事情。我不会去那里。使用float / double 编写高效的代码,让编译器和 FPU 完成剩下的工作。
  • 这是一个众所周知的直接处理位的示例,以便快速计算平方根的倒数。也许它会激发你的工作:betterexplained.com/articles/…
  • 这显示了位模式:en.wikipedia.org/wiki/IEEE_754-1985
  • 我建议使用浮点处理器或浮点硬件辅助来加快您的运算速度。否则考虑使用 Fixed Point 表示法。

标签: c++ ieee-754 exponent numerical-computing mantissa


【解决方案1】:

在许多情况下,直接使用尾数和指数显然有更好的方法。

我知道从我的信号处理工作中感觉非常好,但事实是指数和尾数不能简单地用作单独的数字; IEEE754 规定了一些特殊情况,以及偏移量等。

我想一定有一个非常简单的解决方案。

工程经验告诉我:以“简单解决方案”结尾的句子通常不正确。

“学术案例”

然而,这绝对不是真的(我会在最后提到一个例子)。

IEEE754 浮点数优化在现实世界中的使用非常可靠。但是,我发现随着后来的 x86 处理器执行 SIMD(单指令,多数据)的能力以及浮点与大多数“位移”操作一样快的总体事实,我通常怀疑您不明智试着自己做一点。

一般来说,由于 IEEE754 是一种标准,您会在任何地方找到有关它如何存储在您的特定架构中的文档。如果你看过,你至少应该找到解释如何做 1) 和 2) 的维基百科文章(它不像你想象的那样静态)。

更重要的是: 不要试图比你的编译器更聪明。你可能不会,除非你明确知道如何向量化多个相同的操作。

试验您的特定编译器的数学优化。如前所述,如今他们通常做的不多。 CPU 在进行浮点计算时并不比在整数上慢,这是必然的。

我宁愿看看你的算法并在那里寻找优化的潜力。

另外,当我在这里的时候,让我们介绍一下 VOLK(内核向量优化库),它主要是一个用于信号处理的数学库。 http://libvolk.org 有一个概述。查看以 32f 开头的 kernels,例如 32f_expfast。您会注意到有不同的实现,通用的和 CPU 优化的,每个 SIMD 指令集都不同。

【讨论】:

  • 我同意 c++ 编译器可能比我聪明的说法,但是,这里有一个快速日志算法的简单示例: int log2_evil(double d) { return ((reinterpret_cast( d) >> 52) & 0x7ff) - 1023;与标准日志实现相比,它需要大约 11 倍的计算成本(如果您没有高精度要求)。我只是不确定提到的 alogritm 是如何工作的。我想避免意外行为(不仅对于众所周知的日志,而且对于我的复杂计算)所以这就是我提出 4 个问题的原因。顺便说一句,感谢您对 VOLK 的提及!
  • @MarekBasovník:尝试使用 VOLK 的算法。例如,SSE4.1 实现中的volk_32f_log2_32f.h 需要 118 毫秒的计算量,而我的 PC 上的通用 math.h/log2() 2916 毫秒。这比“无聊”的 11 倍加速更令人印象深刻,因为它的准确性要高得多。虽然我认为我只是在通用实现中看到了一个性能错误。
【解决方案2】:

您可以将 fp 值的地址复制到 unsigned char* 中,并将生成的指针视为覆盖 fp 值的数组的地址。

【讨论】:

  • 这会导致未定义的行为。
  • 有一种方法可以避免与标准中的类型双关语规则发生冲突,不过:blog.regehr.org/archives/959
  • @Novelocrat 问题是严格的别名,这里不适用。 char *unsigned char * 是允许为任何其他类型设置别名的异常,特别是用于检查对象表示。如果你真的必须做那些低级的黑客,它们是最好的方法。 en.cppreference.com/w/cpp/language/reinterpret_cast 有一个关于别名规则的部分。
  • @RaphaelAddile 仍然是未定义的行为。机器可能是大端、小端或混合端。
  • @MarkWeston 未定义。字节序是实现定义的。这意味着我们不会通过使用它来召唤恶魔。我们只需要查找实现文档(或以编程方式测试字节顺序,这很容易)。
【解决方案3】:

在 C 或 C++ 中,如果 x 是 IEEE 双精度,那么如果 L 是 64 位长整数,则表达式

L = *((long *) &x);

将允许直接访问这些位。 如果s 是表示符号的字节(0 = '+',1 = '-'),e 是表示无偏指数的整数,f 是表示小数位的 long int 则

s = (byte)(L >> 63);

e = ((int)(L >> 52) & 0x7FF) - 0x3FF;

f = (L & 0x000FFFFFFFFFFFFF);

(如果 f 是规范化数字,即不是 0、非规范化、inf 或 NaN,则最后一个表达式应添加 0x0010000000000000 以允许 IEEE 双精度格式中的隐式高位 1 位。 )

将符号、指数和分数重新打包成双精度是类似的:

L = (s

x = *((双 *) &L);

上面的代码在用 64 位代码编译的 64 位机器上只生成很少的机器指令,没有子程序调用。对于 32 位代码,有时会调用执行 64 位算术运算,但好的编译器通常会生成内联代码。无论哪种情况,这种方法都非常快。

类似的方法适用于 C#,使用 L = bitConverter.DoubleToInt64Bits(x);x = BitConverter.Int64BitsToDouble(L); 或在允许不安全代码的情况下与上述完全相同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-06
    • 2016-01-06
    • 2010-09-28
    • 2020-07-11
    • 1970-01-01
    • 2011-05-28
    相关资源
    最近更新 更多