5

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

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

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

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

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

4

6 回答 6

4

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

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

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

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

于 2012-08-04T10:38:55.227 回答
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 的幂,您需要乘以最终答案以正确缩放它。

于 2012-08-04T14:53:37.027 回答
2

首先让你的双精度并选择“范围”“指数”部分。然后只移动“指数”“范围”部分。寻找 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;
于 2012-08-04T06:50:16.010 回答
1

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

于 2012-08-04T15:12:08.803 回答
0

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

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

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

于 2012-08-04T06:54:40.043 回答
0

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

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

于 2012-08-04T14:03:31.123 回答