【问题标题】:Forms of constants for high performance addition and multiplication for double用于双精度加法和乘法的常量形式
【发布时间】:2012-08-02 03:26:11
【问题描述】:

我需要在循环中有效地将一些常量添加或乘以 double 类型的结果,以防止下溢。例如,如果我们有 int,则乘以 2 的幂将很快,因为编译器将使用位移位。有没有一种常量形式可以实现高效的double 加法和乘法?

编辑:似乎没有多少人理解我的问题,为我的草率道歉。我将添加一些代码。 如果a是一个int,这个(乘以2的幂)会更高效

int a = 1;
for(...)
    for(...)
        a *= somefunction() * 1024;

当 1024 被替换为 1023 时。如果我们想添加到 int 中,不确定什么是最好的,但这不符合我的兴趣。我对a 是双倍的情况感兴趣。我们可以有效地将乘以双精度数的常量形式(例如 2 的幂)是什么?常数是任意的,只要足够大以防止下溢即可。

这可能不仅限于 C 和 C++,但我不知道更合适的标记。

【问题讨论】:

  • 有一个代码示例可能会有所帮助。顺便说一句,您认为整数的位移比乘以 2 的幂更快的假设是不正确的。如果你写x*4,编译器会自动把它转换成x<<2
  • 听起来你正在做过早的优化,这通常不是一个好主意,而是让编译器为你处理更好。 “我们应该忘记小的效率,比如说大约 97% 的时间:过早的优化是万恶之源”,Donald Knuth
  • @jahhaj 我知道。我想说乘以 2 或 4 比乘以 3 快。
  • @ggg,如果你的代码中有这样的常量,现代编译器通常能够自己进行这种优化。您是否检查过编译器生成的汇编程序,例如使用-S?您是否将编译器升级到最新版本?您是否确保拥有适合您平台的最佳优化选项,例如 -O3 -march=native ?您是否对代码进行了基准测试?
  • 考虑到问题的总体水平,这几乎可以肯定是过早优化的情况。集中精力让代码正确。只有当它通过所有测试时(写出可以准确复现的测试),才考虑它对于普通用户来说是否太慢。

标签: c++ c optimization underflow


【解决方案1】:

在大多数现代处理器上,只需乘以 2 的幂(例如,x *= 0x1p10; 乘以 210x *= 0x1p-10; 除以 210)将快速且无错误(除非结果大到足以溢出或小到足以下溢)。

对于某些浮点运算,有些处理器具有“提前输出”功能。也就是说,当某些位为零或满足其他标准时,它们会更快地完成指令。但是,浮点加法、减法和乘法通常在大约四个 CPU 周期内执行,因此即使没有提前输出,它们也相当快。此外,大多数现代处理器一次执行多条指令,因此在发生乘法时其他工作会继续进行,并且它们是流水线的,因此通常可以在每个 CPU 周期中开始(并结束)一次乘法。 (有时更多。)

乘以 2 的幂没有舍入误差,因为有效数字(值的小数部分)不会改变,因此新有效数字是完全可表示的。 (除了乘以小于 1 的值,有效数的位可能会被压低到浮点类型的限制以下,导致下溢。对于常见的 IEEE 754 双精度格式,直到值小于才会发生这种情况0x1p-1022。)

不要使用除法进行缩放(或反转先前缩放的效果)。相反,乘以倒数。 (要删除之前的 0x1p57 缩放,乘以 0x1p-57。)这是因为大多数现代处理器上的除法指令都很慢。例如,30 个周期并不罕见。

【讨论】:

  • 许多我不知道的新事物。谢谢。
【解决方案2】:

在现代处理器中,浮点加法和乘法通常需要几个周期。

也许你应该退后一步,想想算法在做什么。在您的示例中,您有一个双重嵌套循环......这意味着“somefunction()”可能会被多次调用。 “double”的常见表示形式是 IEEE,它使用 11 位作为指数,使用 52 位作为尾数(实际上是 53,因为除了零之外还有一个隐含的“1”)。这意味着您可以将数字表示为 53 位精度,范围从非常小的数字到非常大的数字 - 二进制“浮点”可以将 1024 (2^10) 位移动到数字 "1.0" 的左侧或右侧。 .. 如果“somefunction()”被调用一千次,它总是返回一个小于或等于 0.5 的数字,则您下溢(每次乘以 0.5,您将数字“a”减半,这意味着您移动二进制浮动指向左边。在 x86 上,您可以通过在控制寄存器中设置一个位来告诉处理器“将非规范化刷新为零” - 没有可移植的编程接口用于执行此操作,您使用 gcc

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

告诉处理器将非正规数刷新为零将使您的代码运行得更快,因为处理器不会尝试表示超出(小于)法线(次法线或非正规数)的数字。面对产生次法线的算法(这会导致精度损失),您似乎正试图保持精度。如何最好地处理这个取决于你是否控制“somefunction()”。如果您确实可以控制该函数,那么您可能会将其返回的值“标准化”为范围内的某个值

0.5 <= X <= 2.0

换句话说,返回值以 1.0 为中心,并单独跟踪 2 的幂,您需要乘以最终答案以正确缩放它。

【讨论】:

    【解决方案3】:

    如果您使用的是 SSE,将常量直接添加到指数字段是一个合理的技巧(在 FPU 代码中这非常糟糕) - 它通常具有两倍的吞吐量和 4 倍的延迟(除了具有浮点数的处理器->int 和/或 int->float 惩罚)。但既然你这样做只是为了防止非正规化,为什么不打开 FTZ(刷新为零)和 DAZ(非正规化为零)?

    【讨论】:

      【解决方案4】:

      在千兆赫处理器上,通过优化这种方式(移位与算术)可以节省 1 或 2 纳秒。但是,从内存加载和存储所需的时间约为 100 纳秒,而到磁盘则为 10 毫秒。与优化缓存使用和磁盘活动相比,担心算术运算是没有意义的。它永远不会对任何实际的生产程序产生影响。

      只是为了防止误解,我并不是说差异很小,所以不要担心,我是说它为零。您不能编写一个简单的程序,其中 ALU 时间的差异与 CPU 停止等待内存或 I/O 的时间不完全重叠。

      【讨论】:

        【解决方案5】:

        首先将你的双精度放在一个联合中,然后选择 "range""exponent" 部分。然后只移动 "exponent""range" 部分。寻找 IEEE 浮点标准。 不要忘记符号和最后的尾数位

        union int_add_to_double
        {
        double this_is_your_double_precision_float;
        struct your_bit_representation_of_double
            {
            int range_bit:53;//you can shift this to make range effect
            //i dont know which is mantissa bit. maybe it is first of range_bit. google it.
            int exponent_bit:10;   //exponential effect
            int sign_bit:1;     //take negative or positive
            }dont_forget_struct_name;
        }and_a_union_name;
        

        【讨论】:

        • 访问 union willy nilly 的成员通常是未定义的行为。
        • 不要忘记事后进行测试,以验证该技术是否确实加快了速度,足以使不可移植性值得。您可能会惊讶于您计算机的 FPU 的效率与“手动”旋转比特相比有多高效。
        • 不要忘记流水线延迟。是的,您每个周期触发一次除法,但通常需要 10-15 个周期才能获得结果。
        • 是的,但声称除法需要一个周期是误导性的。
        • 您正在混合指令的延迟和吞吐量。由于寄存器的宽度,使用 SIMD 单元会同时改变 + 增加速度。
        【解决方案6】:

        您可以使用标准的 frexp/ldexp 函数将 IEE 754 值分解为其组件:

        http://www.cplusplus.com/reference/clibrary/cmath/frexp/

        http://www.cplusplus.com/reference/clibrary/cmath/ldexp/

        这是一个简单的示例代码:

        #include <cmath>
        #include <iostream>
        
        int main ()
        {
          double value = 5.4321;
          int exponent;
        
          double significand = frexp (value , &exponent);
          double result = ldexp (significand , exponent+1);
        
          std::cout << value << " -> " << result  << "\n";
          return 0;
        }
        

        执行处理:http://ideone.com/r3GBy

        【讨论】:

        • 谢谢。这没有回答我的问题(我要求常量的形式),但已经实现了我的目标。
        • freexp 和 ldexp 可能比简单的浮点乘法花费更长的时间,因为它们必须对浮点编码执行整数运算。这涉及许多测试和算术运算,并且在某些现代处理器上,需要在处理器内部的浮点和整数单元之间移动数据。充其量,如果编译器识别出指数是一个常数,它可能会优化调用,用浮点乘法替换它。对于这个问题,一个简单的乘法就足够了。只需直接乘以 2 的精确幂,例如 0x1p1024。
        • @EricPostpischil 我完全同意,我更关心的是这些功能,因为 OP 依赖于上述联合技巧。现在,当然,乘法已经绰绰有余了。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-01-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多