用于高性能加法和乘法的常量形式

Forms of constants for high performance addition and multiplication for double

本文关键字:常量 高性能 用于      更新时间:2023-10-16

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

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

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

与1024被例如1023替换时相比。不确定如果我们想添加到int中,什么是最好的,但我对此不感兴趣。我对a是二重的情况感兴趣。常数(例如2的幂)的形式是什么,我们可以有效地将相乘到一个二重?常数是任意的,只需要足够大就可以防止下溢。

这可能不仅限于C和C++,但我不知道还有什么更合适的标签。

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

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

二次幂相乘没有舍入误差,因为有效位(值的小数部分)不会改变,所以新的有效位是可以精确表示的。(除非乘以小于1的值,有效位的位可以被推到低于浮点类型的限制,从而导致下溢。对于常见的IEEE 754双格式,只有当值小于0x1p-1022时才会发生这种情况。)

不要将分割用于缩放(或用于反转先前缩放的效果)。相反,乘以倒数。(要删除以前的0x1p57缩放,请乘以0x1p-57。)这是因为除法指令在大多数现代处理器上都很慢。例如,30次循环并不罕见。

首先在并集中获得双精度,并选择"范围"的"指数"指数"的"范围"后一个尾数位

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;

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

也许你应该退一步思考一下算法在做什么。在您的示例中,您有一个双嵌套循环。。。这意味着"somefunction()"可能会被多次调用。"双"的常见表示形式是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的幂,需要乘以最终答案才能正确缩放。

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

您可以使用标准的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

在千兆赫处理器上,通过优化这种方式(移位与算术),您可以节省1或2纳秒。然而,从存储器加载和存储所需的时间大约为100纳秒,而到磁盘的时间为10毫秒。与优化缓存使用率和磁盘活动相比,担心算术运算毫无意义。它在任何真正的生产程序中都不会有什么不同。

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