当前位置: 首页 > 知识库问答 >
问题:

为什么这个C++包装类没有内联?

乐正涵意
2023-03-14

我是否遗漏了包装类中的某些细节?

我有以下程序,在其中我定义了一个类,它包装并提供+运算符:

#include <cstdio>
#include <cstdlib>

#define INLINE __attribute__((always_inline)) inline

struct alignas(8) WrappedDouble {
    double value;

    INLINE friend const WrappedDouble operator+(const WrappedDouble& left, const WrappedDouble& right) {
        return {left.value + right.value};
    };
};

#define doubleType WrappedDouble // either "double" or "WrappedDouble"

int main() {
    int N = 100000000;
    doubleType* arr = (doubleType*)malloc(sizeof(doubleType)*N);
    for (int i = 1; i < N; i++) {
        arr[i] = arr[i - 1] + arr[i];
    }

    free(arr);
    printf("done\n");

    return 0;
}

我以为这会编译成同样的东西--它在做同样的计算,所有的东西都是内联的。

编辑-如果我使用g++而不是gcc,它将产生相同的输出。

编辑-我发布了错误的ASM版本(-O0而不是-O3),因此本节没有帮助。

我在我的Mac电脑上使用Xcode的gcc,在一个64位系统上。除了for-loop的主体之外,结果是相同的。

movq    -16(%rbp), %rax
movl    -20(%rbp), %ecx
subl    $1, %ecx
movslq  %ecx, %rdx
movsd   (%rax,%rdx,8), %xmm0    ## xmm0 = mem[0],zero
movq    -16(%rbp), %rax
movslq  -20(%rbp), %rdx
addsd   (%rax,%rdx,8), %xmm0
movq    -16(%rbp), %rax
movslq  -20(%rbp), %rdx
movsd   %xmm0, (%rax,%rdx,8)
movq    -40(%rbp), %rax
movl    -44(%rbp), %ecx
subl    $1, %ecx
movslq  %ecx, %rdx
shlq    $3, %rdx
addq    %rdx, %rax
movq    -40(%rbp), %rdx
movslq  -44(%rbp), %rsi
shlq    $3, %rsi
addq    %rsi, %rdx
movq    %rax, -16(%rbp)
movq    %rdx, -24(%rbp)
movq    -16(%rbp), %rax
movsd   (%rax), %xmm0           ## xmm0 = mem[0],zero
movq    -24(%rbp), %rax
addsd   (%rax), %xmm0
movsd   %xmm0, -8(%rbp)
movsd   -8(%rbp), %xmm0         ## xmm0 = mem[0],zero
movsd   %xmm0, -56(%rbp)
movq    -40(%rbp), %rax
movslq  -44(%rbp), %rdx
movq    -56(%rbp), %rsi
movq    %rsi, (%rax,%rdx,8)

共有1个答案

和魁
2023-03-14

它是内联的,但没有被优化,因为您使用-o0编译(默认值)。它生成用于一致调试的asm,允许您在任何行的断点处停止时修改任何C++变量。

这意味着编译器在每个语句之后从寄存器中溢出所有内容,并重新加载下一个语句所需的内容。因此,更多的语句来表达相同的逻辑=更慢的代码,无论它们是否在相同的函数中。为什么clang为这个简单的浮点和(带有-O0)产生低效的asm?更详细地解释。

通常-O0不会内联函数,但它确实尊重__attribute__((always_inline))

然后,它将这些指针arg溢出到堆栈内存中,因为它们是真正的变量,必须在内存中调试器可以修改它们。这就是movq%rax,-16(%rbp)%rdx...正在做的事情。

在重新加载和取消引用这些指针之后,addsd(添加标量双)结果本身会溢出到具有movsd%xmm0,-8(%rbp)的本地堆栈内存中。这不是一个命名变量,它是函数的返回值。

然后重新加载并再次复制到另一个堆栈位置,最后从堆栈中加载arri,以及运算符+double结果,并用movq%rsi,(%rax,%rdx,8)存储到arr[i]中。(是的,LLVM当时使用64位整数MOV复制double。以前使用SSE2MOVSD。)

返回值的所有这些副本都位于循环携带的依赖链的关键路径上,因为下一次迭代将读取arr[i-1]。这些~5或6个周期的存储转发延迟实际上相加,而3或4个周期的FPadd延迟。

显然这是非常低效的。启用优化后,gcc和clang就不会有内联和优化包装器的麻烦了。

它们还通过将ARR[i]结果保留在寄存器中进行优化,以便在下一次迭代中用作ARR[i-1]结果。这避免了~6个循环的存储转发延迟,如果它使asm像源一样,则该延迟将在循环内。

double tmp = arr[0];   // kept in XMM0

for(...) {
   tmp += arr[i];   // no re-read of mmeory
   arr[i] = tmp;
}

我更喜欢Intel语法,但是Godbolt编译器浏览器可以给你AT&T语法,就像你的问题一样。

# gcc8.2 -O3 -march=haswell -Wall
.LC1:
    .string "done"
main:
    sub     rsp, 8
    mov     edi, 800000000
    call    malloc                  # return value in RAX

    vmovsd  xmm0, QWORD PTR [rax]   # load first elmeent
    lea     rdx, [rax+8]            # p = &arr[1]
    lea     rcx, [rax+800000000]    # endp = arr + len

.L2:                                   # do {
    vaddsd  xmm0, xmm0, QWORD PTR [rdx]   # tmp += *p
    add     rdx, 8                        # p++
    vmovsd  QWORD PTR [rdx-8], xmm0       # p[-1] = tmp
    cmp     rdx, rcx
    jne     .L2                        # }while(p != endp);

    mov     rdi, rax
    call    free
    mov     edi, OFFSET FLAT:.LC0
    call    puts
    xor     eax, eax
    add     rsp, 8
    ret

Clang展开了一点,正如我所说的,它不需要初始化它的tmp

# just the inner loop from clang -O3
# with -march=haswell it unrolls a lot more, so I left that out.
# hence the 2-operand SSE2 addsd instead of 3-operand AVX vaddsd
.LBB0_1:                                # do {
    addsd   xmm0, qword ptr [rax + 8*rcx - 16]
    movsd   qword ptr [rax + 8*rcx - 16], xmm0
    addsd   xmm0, qword ptr [rax + 8*rcx - 8]
    movsd   qword ptr [rax + 8*rcx - 8], xmm0
    addsd   xmm0, qword ptr [rax + 8*rcx]
    movsd   qword ptr [rax + 8*rcx], xmm0
    add     rcx, 3                            # i += 3
    cmp     rcx, 100000002
    jne     .LBB0_1                      } while(i!=100000002)

在现代OS X系统上,Apple Xcode的gcc实际上是伪装的clang/llvm。

 类似资料:
  • 问题内容: 我有一个flex项目,它也是一个flex容器,问题是即使添加了,它的flex项目也拒绝包装。 谁能为我解决这个问题,或指出我做错了什么。 问题答案: 嵌套容器中的伸缩项目按 百分比 调整大小。 由于百分比长度基于父级的长度,因此 没有理由进行包装 。它们将始终是父级的40%,即使父级的宽度为1%。 如果您使用其他长度单位,例如或,它们将自动换行。

  • 我尝试了一切,但图像不会显示出来,我试图使图像变小但没有用,我试图改变路径,我试图改变图像的位置但没有帮助,我试图在互联网上搜索但一无所获。 我看到的只是空白的图形用户界面,没有文本和图像。如果你能帮我,你会帮我一个大忙。 代码如下:

  • 问题内容: 我正在尝试制作多个具有相同高度的正方形行(每行3个)。 我为此编写了一些HTML和CSS,但所有框都在同一行上。 这是我到目前为止的内容: 当我用此页面加载页面时,所有框都出现了,但它们都在一行上,超过了父div的100%宽度。 任何帮助深表感谢。 问题答案: flex容器的初始设置为 。 这意味着,当您创建一个伸缩容器(通过应用元素或将其应用于元素)时,所有子元素(“伸缩项”)都限于

  • 问题内容: 我知道包装器类是什么,它们将原始类型(例如int,double等)包装到各自类的对象中。 但是,为什么我们首先需要包装器类?为什么不简单地在我们拥有原始类型的地方使用它们呢? 问题答案: 几种可能的原因: 这样就可以有一个空值 包含在集合中 将一般/多态与其他对象一起视为对象

  • 为什么这个文件本身? https://github.com/powturbo/TurboPFor-Integer-Compression 简单的

  • 我无法理解的是,为什么当我使用Eclipse将所有依赖项提取到jar中的文件夹结构中时,jar不再作为一个正确的Spring Boot应用程序运行。 为了重复我在这里所做的事情,只需使用一个简单的Spring Boot应用程序,并从Eclipse中选择以下内容: 导出-->Runnable JAR-->选择Main Class-->将所需库提取到Jar中。 我只是想了解Spring实际上是如何通过