C ++,如何优化浮点算术运算?

时间:2011-04-19 12:54:18

标签: c++ optimization floating-point

在极限情况下,在x86架构上测试简单的算术运算时,我发现了一个令人惊讶的行为:

const double max = 9.9e307; // Near std::numeric_limits<double>::max()
const double init[] = { max, max, max };

const valarray<double> myvalarray(init, 3);
const double mysum = myvalarray.sum();
cout << "Sum is " << mysum << endl;             // Sum is 1.#INF
const double myavg1 = mysum/myvalarray.size();
cout << "Average (1) is " << myavg1 << endl;    // Average (1) is 1.#INF
const double myavg2 = myvalarray.sum()/myvalarray.size();
cout << "Average (2) is " << myavg2 << endl;    // Average (2) is 9.9e+307

(在发布模式下使用MSVC测试,以及通过Codepad.org测试gcc.MSVC的调试模式将平均值(2)设置为#INF。)

我希望平均值(2)等于平均值​​(1),但在我看来,C ++内置除法运算符已被编译器优化,并以某种方式阻止累积达到#INF。 /> 简而言之:大数字的平均值不会产生#INF

我在MSVC上观察到与std算法相同的行为:

const double mysum = accumulate(init, init+3, 0.);
cout << "Sum is " << mysum << endl;             // Sum is 1.#INF
const double myavg1 = mysum/static_cast<size_t>(3);
cout << "Average (1) is " << myavg1 << endl;    // Average (1) is 1.#INF
const double myavg2 = accumulate(init, init+3, 0.)/static_cast<size_t>(3);
cout << "Average (2) is " << myavg2 << endl;    // Average (2) is 9.9e+307

(但这次,gcc将平均值(2)设置为#INFhttp://codepad.org/C5CTEYHj。)

  1. 有人会解释如何实现这种“效果”吗?
  2. 这是一个“功能”吗?或者,我可以将此视为“意外行为”,而不仅仅是“令人惊讶”吗?
  3. 由于

4 个答案:

答案 0 :(得分:4)

只是猜测,但是:可能是平均值(2)直接在浮点寄存器中计算,浮点寄存器的宽度为80位,溢出时间晚于64位存储器,用于存储器中的双精度数。您应该检查代码的反汇编,看看是否确实如此。

答案 1 :(得分:2)

这是一种功能,或者至少是故意的。 基本上,x86上的浮点寄存器有更多 精度和范围比双倍(15位指数,而不是 11,64位matissa,而不是52)。 C ++标准允许 使用更高的精度和中间值范围,和 几乎所有英特尔的编译器都会这样做 情况;性能差异很大。 是否获得扩展精度取决于何时 以及编译器是否溢出到内存。 (保存结果 命名变量需要编译器将其转换为实际 双精度,至少按照标准。) 更糟糕的是,我见过的是一些基本上做过的代码:

return p1->average() < p2->average()

average()在内部表上执行您期望的操作 在数据中。在某些情况下,p1p2实际上会指出 到同一个元素,但返回值仍然是真的; 其中一个函数调用的结果将溢出 内存(并截断为double),另一个的结果 留在浮点记录中。

(该函数被用作sort的排序函数, 由于这种影响,结果代码崩溃了 没有定义足够严格的订购标准,而且 传递给它的范围之外的sort代码。)

答案 2 :(得分:1)

在某些情况下,编译器可以使用比声明类型隐含的更宽泛的类型,但是AFAIK,这不是其中之一。

因此我认为我们的效果类似于Gcc bug 323的效果,但不应该使用额外的精度。

x86有80位FP内部寄存器。虽然gcc倾向于以最大精度使用它们(因此bug 323),但我的理解是MSVC将精度设置为53位,64位的精度加倍。但加长显着性不是80位FP的唯一差异,指数范围也增加。和IIRC一样,x86中没有设置强制使用64位双倍范围。

现在,

键盘似乎无法访问,或者我在没有80位长的双重架构上测试代码。

答案 3 :(得分:1)

g++ -O0 -g -S test.cpp  -o test.s0
g++ -O3 -g -S test.cpp  -o test.s3

比较test.s [03]表明确实valarray :: sum甚至没有被再次调用。我没有长时间关注它,但以下片段似乎是定义的片段:

    .loc 3 16 0 ; test.s0

    leal    -72(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZNKSt8valarrayIdE3sumEv
    fstpl   -96(%ebp)
    leal    -72(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZNKSt8valarrayIdE4sizeEv
    movl    $0, %edx
    pushl   %edx
    pushl   %eax
    fildq   (%esp)
    leal    8(%esp), %esp
    fdivrl  -96(%ebp)
    fstpl   -24(%ebp)

    .loc 3 17 0

    .loc 1 16 0 ; test.s3
    faddl   16(%eax)
    fdivs   LC3
    fstpl   -336(%ebp)
LVL6:
LBB449:
LBB450:
    .loc 4 514 0