【问题标题】:Are C++ floating point constants always stored in static memory?C++ 浮点常量是否总是存储在静态内存中?
【发布时间】:2015-11-19 17:56:16
【问题描述】:

在他的书中Optimizing Software in C++Agner Fog 给出了以下示例:

字符串常量和浮点常量存储在静态中 记忆。示例:

// Example 7.2
a = b * 3.5;
c = d + 3.5;

在这里,常数 3.5 将被存储在静态内存中。大多数编译器 将认识到这两个常数是相同的,因此只有一个 需要存储常量。

是否所有浮点常量总是存储在静态内存中?

为什么不能将它们存储在堆栈中?在他给出的例子中说。

【问题讨论】:

  • 常量只是程序代码的一部分(类似于函数等...)
  • 他们将如何进入堆栈?价值必须来自某个地方
  • 它是特定于实现的(取决于 ABI、处理器、编译器、优化标志)
  • 堆栈是你在优化之前应该了解的东西。
  • @DieterLucking 下面几行他接着说:“整数常量通常包含在指令代码中。”所以据他说,浮点不是程序代码的一部分。

标签: c++ floating-point constants


【解决方案1】:

谢谢你的例子。你是对的,当使用 gcc 编译为 64 位模式并关闭优化 (-O0) 时,它将常量存储在代码中。如果编译为 32 位模式,它会将常量存储在内存中。

如果您打开优化 (-O3),那么计算会被简单地优化掉,因为它们没有被使用,并且 main 只返回 0。

如果你创建一个函数来进行一些浮点计算并返回一个浮点数或双精度,并在优化的情况下编译,那么你会看到浮点常量存储在静态内存中。

我试过了:

float test(float x) {
    return x*0.9f + 3.1f;
}

g++ -c -S -O3 test.cpp

猫测试.s

得到:

mulss   .LC0(%rip), %xmm0
addss   .LC1(%rip), %xmm0
ret

其中 .LC0 和 .LC1 是只读数据段中的常量。 x86 和 x86-64 指令集(不幸的是)没有将立即常数存储在浮点寄存器或向量寄存器中的指令。它必须首先将常量存储在通用寄存器中,然后将其传输到 xmm 寄存器,这不是最优的(除了数据缓存负载很重但代码缓存没有负载的情况)。

【讨论】:

    【解决方案2】:

    我根据您的伪代码创建了一个简单的工作示例:

    int main()
    {
      double b = 3.0;
      double d = 3.14;
      double a = b * 3.5;
      double c = d + 3.5;
      return 0;
    }
    

    然后编译它:

    g++ -O0 -o const const.cc
    

    然后在调试器中使用它:

    gdb const
    ...
    (gdb) disass main
    Dump of assembler code for function main:
       0x00000000004004ed <+0>: push   %rbp
       0x00000000004004ee <+1>: mov    %rsp,%rbp
       0x00000000004004f1 <+4>: movabs $0x4008000000000000,%rax
       0x00000000004004fb <+14>:    mov    %rax,-0x20(%rbp)
       0x00000000004004ff <+18>:    movabs $0x40091eb851eb851f,%rax
       0x0000000000400509 <+28>:    mov    %rax,-0x18(%rbp)
       0x000000000040050d <+32>:    movsd  -0x20(%rbp),%xmm1
       0x0000000000400512 <+37>:    movsd  0xae(%rip),%xmm0        # 0x4005c8
       0x000000000040051a <+45>:    mulsd  %xmm1,%xmm0
       0x000000000040051e <+49>:    movsd  %xmm0,-0x10(%rbp)
       0x0000000000400523 <+54>:    movsd  -0x18(%rbp),%xmm1
       0x0000000000400528 <+59>:    movsd  0x98(%rip),%xmm0        # 0x4005c8
       0x0000000000400530 <+67>:    addsd  %xmm1,%xmm0
       0x0000000000400534 <+71>:    movsd  %xmm0,-0x8(%rbp)
       0x0000000000400539 <+76>:    mov    $0x0,%eax
       0x000000000040053e <+81>:    pop    %rbp
       0x000000000040053f <+82>:    retq   
    End of assembler dump.
    (gdb) b main
    Breakpoint 1 at 0x4004f1
    (gdb) r
    Starting program: /home/sasha/stackoverflow/const 
    
    Breakpoint 1, 0x00000000004004f1 in main ()
    (gdb) p  *(double*)0x4005c8
    $2 = 3.5
    

    因此,您从所有这些中看到,常量 3.5 存储在地址 0x4005c8 处,该地址距离 main 结束仅 136 个字节。两次都使用相同的地址来引用它,尽管它在反汇编中看起来不同 - 第一次为0xae(%rip),第二次为movsd 0x98(%rip)。这是因为rip 的值随着执行的进行而不断变化——它是指令指针。

    objdump -t的帮助下,您可以看到上述地址属于.rodata部分。

    请注意,3.0 和 3.14 常量是在没有内存引用的情况下显式编码的:

    movabs $0x4008000000000000,%rax
    

    movabs $0x40091eb851eb851f,%rax
    

    显然,由于它们没有重复,gcc 决定将它们存储在内存中是不值得的。

    更新:正如 JSF 所指出的,不使用存储的内存而不是立即常量的决定确实取决于使用情况。我验证如果我将double d = 3.14 更改为double d = b + 3.14 3.14 将成为内存引用。

    我使用-O0 来保持简单。通过在这样一个简单的示例中进行优化,程序集被优化为可以达到相同结果但与足以说明这一点的有用内容相去甚远的方式。

    至少在gcc 的情况下,答案似乎确实是“视情况而定”。但是,如果你好奇,与其相信一本书或一篇博文,不如做一个简单的例子并把它拆开——你直接从马嘴里得到它,你会学到更多。

    【讨论】:

    • 所以答案是,这取决于?
    • 请注意double d = 3.14;double c = d + 3.5; 之间的巨大差异,我的意思是3.5 被使用了两次。在 x86_64 体系结构中,与用于浮点计算的浮点常数相比,编译器对于简单存储到变量中的浮点常数有更广泛的实际选择。当然,优化器可能会变成另一个。但是在您显示的代码中,编译器选择了一种表示常量的方式来简单地存储,这对于直接使用的常量不起作用。
    • 我不同意你对-O0 的描述,虽然优化通常会使代码“怪异”,-O0 也会导致愚蠢的代码生成,这是毫无意义的冗长并故意做愚蠢的事情,因此不会不能给人有代表性的印象。在合理的代码中,不会发生从 GPR 通过堆栈到 XMM 的奇怪舞蹈。
    • 在我查看的优化代码中,“奇怪的舞蹈”很常见。它不会发生在优化的琐碎代码中。但在复杂的代码中,通过使用该变量将常量赋值给局部变量的路径往往超出优化器的能力范围。
    • 我不信任任何人,否则我不会问这个问题。另一方面,这是一本知名且受人尊敬的书,因此如果其中有误导性,值得对其进行建设性的批评。
    猜你喜欢
    • 2016-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多