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

2024-05-01

EDIT- 我的构建系统出了问题。我还在弄清楚到底是什么,但是gcc产生了奇怪的结果(尽管它是.cpp文件),但是一旦我使用了g++然后它按预期工作。


对于我一直遇到麻烦的事情来说,这是一个非常精简的测试用例,其中使用数字包装类(我认为会内联)使我的程序慢了 10 倍。

这与优化级别无关(尝试过-O0 and -O3).

我在包装类中遗漏了一些细节吗?


C++

我有以下程序,其中我定义了一个类,该类包装了double并提供+操作员:

#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;
}

我认为这会编译成相同的东西 - 它执行相同的计算,并且所有内容都是内联的。

然而,事实并非如此——无论优化级别如何,它都会产生更大且更慢的结果。

(这个特定的结果不是显著地速度较慢,但​​我的实际用例包括更多算术。)

EDIT- 我知道这不是构建我的数组元素。我认为这可能会产生更少的 ASM,这样我可以更好地理解它,但如果有问题我可以更改它。

EDIT- 我也知道我应该使用new[]/delete[]。很遗憾gcc拒绝编译它,即使它是在.cpp文件。这是我的构建系统被搞砸的症状,这可能是我的实际问题。

EDIT- 如果我使用g++代替gcc,它产生相同的输出。


EDIT- 我发布了错误版本的 ASM (-O0代替-O3),所以本节没有帮助。

Assembly

我在 Mac 上的 64 位系统上使用 XCode 的 gcc。除了 for 循环体之外,结果是相同的。

这是它为循环体生成的内容 ifdoubleType is double:

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)

The WrappedDouble版本更长:

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)

It is内联,但没有优化掉,因为你编译了-O0(默认)。这会生成用于一致调试的 asm,允许您modify任何 C++ 变量在任何行的断点处停止。

这意味着编译器在每个语句之后都会溢出寄存器中的所有内容,并重新加载下一个语句所需的内容。因此,表达相同逻辑的语句越多 = 代码速度越慢,无论它们是否在同一个函数中。为什么 clang 对于这个简单的浮点和(使用 -O0)会产生低效的 asm? https://stackoverflow.com/questions/53366394/why-does-clang-produce-inefficient-asm-for-this-simple-floating-point-sum-with更详细地解释。

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

最终分配的 C 循环优化帮助 https://stackoverflow.com/questions/32000917/c-loop-optimization-help-for-final-assignment/32001196#32001196解释了为什么进行基准测试或调整-O0完全没有意义。这两个版本对于性能来说都是可笑的垃圾。


如果没有内联,就会有一个call在循环内调用它的指令。

asm 实际上是在寄存器中创建指针const WrappedDouble& left and right。 (效率非常低,使用多条指令而不是一条指令lea. The addq %rdx, %rax是其中一个的最后一步。)

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

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

然后重新加载并再次复制到另一个堆栈位置,最后arr and i从堆栈加载,以及double的结果operator+,并且被存储到arr[i] with movq %rsi, (%rax,%rdx,8)。 (是的,LLVM 使用了 64 位整数mov复制一个double那时。早期使用SSE2movsd.)

返回值的所有这些副本都位于循环携带依赖链的关键路径上,因为下一次迭代读取arr[i-1].与 3 或 4 周期 FP 相比,大约 5 或 6 周期存储转发延迟确实会增加add潜伏。


显然这是大规模地效率低下。启用优化后,gcc 和 clang 可以毫无困难地内联和优化您的包装器。

他们还通过保留arr[i]结果在一个寄存器中用作arr[i-1]导致下一次迭代。这避免了大约 6 个周期的存储转发延迟,如果它使 asm 与源一样,则该延迟将出现在循环内。

即优化后的 asm 看起来有点像 C++:

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

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

有趣的是, clang 不费心去初始化它的tmp (xmm0) 在循环之前,因为您不必费心初始化数组。奇怪的是它没有警告 UB。实际操作中大malloc使用 glibc 的实现将为您提供来自操作系统的新页面,并且它们都将保留零,即0.0。但是 clang 会给你 XMM0 中剩下的一切!如果您添加一个((double*)arr)[0] = 1;, clang 将加载循环之前的第一个元素。

不幸的是,编译器不知道如何比前缀和计算做得更好。看与 SSE 的并行前缀(累积)和 https://stackoverflow.com/questions/19494114/parallel-prefix-cumulative-sum-with-sse and Intel cpu 上的 SIMD 前缀和 https://stackoverflow.com/questions/10587598/simd-prefix-sum-on-intel-cpu了解如何将其速度加快 2 倍,和/或使其并行化。

我更喜欢 Intel 语法,但是Godbolt 编译器浏览器 https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,lang:c%2B%2B,source:'%23include+%3Ccstdio%3E%0A%23include+%3Ccstdlib%3E%0A%0A%23define+INLINE+__attribute__((always_inline))+inline%0A%0Astruct+alignas(8)+WrappedDouble+%7B%0A++++double+value%3B%0A%0A++++INLINE+friend+const+WrappedDouble+operator%2B(const+WrappedDouble%26+left,+const+WrappedDouble%26+right)+%7B%0A++++++++return+%7Bleft.value+%2B+right.value%7D%3B%0A++++%7D%3B%0A%7D%3B%0A%0A%23define+doubleType+WrappedDouble+//+either+%22double%22+or+%22WrappedDouble%22%0A%0Aint+main()+%7B%0A++++int+N+%3D+100000000%3B%0A++++doubleType*+arr+%3D+(doubleType*)malloc(sizeof(doubleType)*N)%3B%0A++++//+((double*)arr)%5B0%5D+%3D+1%3B%0A++++//+with+no+array+init,+clang+doesn!'t+bother+to+init+its+XMM0%0A++++for+(int+i+%3D+1%3B+i+%3C+N%3B+i%2B%2B)+%7B%0A++++++++arr%5Bi%5D+%3D+arr%5Bi+-+1%5D+%2B+arr%5Bi%5D%3B%0A++++%7D%0A%0A++++free(arr)%3B%0A++++printf(%22done%5Cn%22)%3B%0A%0A++++return+0%3B%0A%7D'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:30.50309915596882,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang700,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c%2B%2B,libs:!(),options:'-O3',source:1),l:'5',n:'0',o:'x86-64+clang+7.0.0+(Editor+%231,+Compiler+%231)+C%2B%2B',t:'0')),k:36.16356751069786,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c%2B%2B,libs:!(),options:'-O3+-march%3Dhaswell+-Wall+-Wextra',source:1),l:'5',n:'0',o:'x86-64+gcc+8.2+(Editor+%231,+Compiler+%232)+C%2B%2B',t:'0')),k:33.33333333333333,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4如果您愿意,可以为您提供像问题中那样的 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)

苹果XCode的gcc在现代 OS X 系统上,它实际上是变相的 clang/LLVM。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么这个 C++ 包装类没有被内联掉? 的相关文章

  • Qt-Qlist 检查包含自定义类

    有没有办法覆盖加载自定义类的 Qt QList 的比较机制 即在 java 中你只需要重写一个比较方法 我有一个带有我的自定义类模型的 QList QList
  • Newtonsoft JSON PreserveReferences处理自定义等于用法

    我目前在使用 Newtonsoft Json 时遇到一些问题 我想要的很简单 将要序列化的对象与所有属性和子属性进行比较以确保相等 我现在尝试创建自己的 EqualityComparer 但它仅与父对象的属性进行比较 另外 我尝试编写自己的
  • 将布尔参数传递给 SQL Server 存储过程

    我早些时候问过这个问题 我以为我找到了问题所在 但我没有 我在将布尔参数传递给存储过程时遇到问题 这是我的 C 代码 public bool upload false protected void showDate object sende
  • 指针问题(仅在发布版本中)

    不确定如何描述这一点 但我在这里 由于某种原因 当尝试创建我的游戏的发布版本进行测试时 它的敌人创建方面不起作用 Enemies e level1 3 e level1 0 Enemies sdlLib 500 2 3 128 250 32
  • C - 找到极限之间的所有友好数字

    首先是定义 一对友好的数字由两个不同的整数组成 其中 第一个整数的除数之和等于第二个整数 并且 第二个整数的除数之和等于第一个整数 完美数是等于其自身约数之和的数 我想做的是制作一个程序 询问用户一个下限和一个上限 然后向他 她提供这两个限
  • 指针减法混乱

    当我们从另一个指针中减去一个指针时 差值不等于它们相距多少字节 而是等于它们相距多少个整数 如果指向整数 为什么这样 这个想法是你指向内存块 06 07 08 09 10 11 mem 18 24 17 53 7 14 data 如果你有i
  • 在 ASP.NET Core 3.1 中使用包含“System.Web.HttpContext”的旧项目

    我们有一些用 Net Framework编写的遗留项目 应该由由ASP NET Core3 1编写的API项目使用 问题是这些遗留项目正在使用 System Web HttpContext 您知道它不再存在于 net core 中 现在我们
  • 如何将图像路径保存到Live Tile的WP8本地文件夹

    我正在更新我的 Windows Phone 应用程序以使用新的 WP8 文件存储 API 本地文件夹 而不是 WP7 API 隔离存储文件 旧的工作方法 这是我如何成功地将图像保存到 共享 ShellContent文件夹使用隔离存储文件方法
  • 如何衡量两个字符串之间的相似度? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 给定两个字符串text1 and text2 public SOMEUSABLERETURNTYPE Compare string t
  • 从库中捕获主线程 SynchronizationContext 或 Dispatcher

    我有一个 C 库 希望能够将工作发送 发布到 主 ui 线程 如果存在 该库可供以下人员使用 一个winforms应用程序 本机应用程序 带 UI 控制台应用程序 没有 UI 在库中 我想在初始化期间捕获一些东西 Synchronizati
  • 为什么在展开的 ADD 循环内重新初始化寄存器会使其运行速度更快,即使循环内有更多指令?

    我有以下代码 include
  • Discord.net 无法在 Linux 上运行

    我正在尝试让在 Linux VPS 上运行的 Discord net 中编码的不和谐机器人 我通过单声道运行 但我不断收到此错误 Unhandled Exception System Exception Connection lost at
  • 如何在 VBA 中声明接受 XlfOper (LPXLOPER) 类型参数的函数?

    我在之前的回答里发现了问题 https stackoverflow com q 19325258 159684一种无需注册即可调用 C xll 中定义的函数的方法 我之前使用 XLW 提供的注册基础结构 并且使用 XlfOper 类型在 V
  • NHibernate - CreateCriteria 与 CreateAlias

    假设以下场景 class Project public Job Job class Job public Name 假设我想使用 Criteria API 搜索其 Job 名称为 sumthing 的所有项目 我可以使用 CreateAli
  • 在 Dynamics CRM 插件中访问电子邮件发件人地址

    我正在编写一个 Dynamics CRM 2011 插件 该插件挂钩到电子邮件实体的更新后事件 阶段 40 pipeline http msdn microsoft com en us library gg327941 aspx 并且在此阶
  • WCF:将随机数添加到 UsernameToken

    我正在尝试连接到用 Java 编写的 Web 服务 但有些东西我无法弄清楚 使用 WCF 和 customBinding 几乎一切似乎都很好 除了 SOAP 消息的一部分 因为它缺少 Nonce 和 Created 部分节点 显然我错过了一
  • C - 直接从键盘缓冲区读取

    这是C语言中的一个问题 如何直接读取键盘缓冲区中的数据 我想直接访问数据并将其存储在变量中 变量应该是什么数据类型 我需要它用于我们研究所目前正在开发的操作系统 它被称为 ICS OS 我不太清楚具体细节 它在 x86 32 位机器上运行
  • 32 位到 64 位内联汇编移植

    我有一段 C 代码 在 GNU Linux 环境下用 g 编译 它加载一个函数指针 它如何执行并不重要 使用一些内联汇编将一些参数推送到堆栈上 然后调用该函数 代码如下 unsigned long stack 1 23 33 43 save
  • 为什么 C# Math.Ceiling 向下舍入?

    我今天过得很艰难 但有些事情不太对劲 在我的 C 代码中 我有这样的内容 Math Ceiling decimal this TotalRecordCount this PageSize Where int TotalRecordCount
  • 防止索引超出范围错误

    我想编写对某些条件的检查 而不必使用 try catch 并且我想避免出现 Index Out of Range 错误的可能性 if array Element 0 Object Length gt 0 array Element 1 Ob

随机推荐