当我在禁用优化的情况下进行编译时,为什么clang不使用内存目标x86指令?它们有效吗?

Sam*_*Sam 19 c assembly gcc clang compiler-optimization

我编写了这个简单的汇编代码,运行它并使用GDB查看内存位置:

    .text

.global _main

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
    addl    $6, -4(%rbp)
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

它直接在内存中添加5到6个,根据GDB它可以工作.所以这是直接在内存中执行数学运算而不是CPU寄存器.

现在在C中编写相同的东西并将其编译为汇编,结果如下:

...  # clang output
    xorl    %eax, %eax
    movl    $0, -4(%rbp)
    movl    $5, -8(%rbp)
    movl    -8(%rbp), %ecx   # load a
    addl    $6, %ecx         # a += 6
    movl    %ecx, -8(%rbp)   # store a
....
Run Code Online (Sandbox Code Playgroud)

在将它们添加到一起之前,它会将它们移动到寄存器中.

那么为什么我们不直接在内存中添加?

它慢了吗?如果是这样,为什么直接在内存中添加甚至允许,为什么汇编程序在开始时没有抱怨我的汇编代码?

编辑:这是第二个程序集块的C代码,我在编译时禁用了优化.

#include <iostream>

int main(){
 int a = 5;
 a+=6; 
 return 0;
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 33

你禁用了优化,你很惊讶asm看起来效率低下?好吧不要. 您已经要求编译器快速编译:缩短编译时间而不是生成的二进制文件的短运行时间.并具有调试模式一致性.

是的,GCC和clang将在调整现代x86 CPU时使用内存目标添加.如果您没有使用添加结果在寄存器中,则效率很高.显然,你的手写asm有一个重大的错过优化. movl $5+6, -4(%rbp)效率要高得多,因为这两个值都是汇编时常量,因此在运行时很难实现.就像你的反优化编译器输出一样.

(更新:刚刚注意到你的编译器输出包含了xor %eax,%eax,所以这看起来像clang/LLVM,而不是我最初猜到的gcc.这个答案中的几乎所有内容同样适用于clang,但gcc -O0不会寻找xor-zeroing peephole优化-O0,使用mov $0, %eax.)

有趣的事实:gcc -O0实际上会用addl $6, -4(%rbp)在你的main.


您已经从手写的asm中了解到,向内存添加立即数可编码为x86 add指令,因此唯一的问题是gcc/LLVM的优化器是否决定使用它.但是您禁用了优化.

内存目标添加不执行"内存中"计算,CPU必须加载/添加/存储.这样做时不会干扰任何架构寄存器,但它不仅仅是将6DRAM 发送到那里.另请参见'num num'的num num是原子的吗?对于C和x86 asm内存目标ADD的详细信息,使用/不使用lock前缀使其显示为原子.

有将ALU放入DRAM的计算机架构研究,因此计算可以并行发生,而不是要求所有数据通过内存总线传递到CPU以进行任何计算.随着内存大小增长快于内存带宽,这成为一个越来越大的瓶颈,CPU吞吐量(使用宽SIMD指令)也比内存带宽增长得更快.(需要更多的计算强度(每个加载/存储的ALU工作量)CPU不会停止.快速缓存有帮助,但是一些问题具有大的工作集并且难以应用缓存阻塞.快速缓存确实可以缓解最大的问题的时间.)

但就目前而言,add $6, -4(%rbp)解码为加载,添加和存储CPU内部的uops.负载使用内部临时目标,而不是架构寄存器.

现代x86 CPU有一些隐藏的内部逻辑寄存器,多uop指令可以用于临时.这些隐藏的寄存器在发布/重命名阶段被重命名为物理寄存器,因为它们被分配到无序后端,但在前端(解码器输出,uop缓存,IDQ)uops只能引用表示机器逻辑状态的"虚拟"寄存器.因此,内存目标ALU指令解码的多个uop可能使用隐藏的tmp寄存器.

我们知道存在这些用于微代码/多uop指令:http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/称它们为"内部使用的额外架构寄存器".它们不是x86机器状态的一部分,只是作为逻辑寄存器的意义,寄存器分配表(RAT)必须跟踪寄存器重命名到物理寄存器文件.x86指令之间不需要它们的值,仅适用于一条x86指令中的微指令,特别是微编码的指令rep movsb(如检查大小和重叠,如果可能则使用16或32字节的加载/存储),也适用于多条指令-uop memory + ALU指令.

原始8086不是无序的,甚至是流水线的.它可以直接加载到ALU输入,然后在ALU完成时,存储结果. 它在寄存器文件中不需要临时的"架构"寄存器,只需要在组件之间进行正常的缓冲.这大概就是486之前的一切.也许甚至奔腾.


它慢了吗?如果是这样,为什么直接添加内存甚至允许,为什么汇编程序在开始时没有抱怨我的汇编代码?

在这种情况下,如果我们假装该值已经在内存中,则立即向内存添加是最佳选择.(而不是仅仅从另一个立即常量存储.)

现代x86从8086发展而来.在现代x86 asm中有许多缓慢的方法可以做,但是如果不破坏向后兼容性,它们都不会被禁止.例如,enter指令在186中被添加回来以支持嵌套的Pascal过程,但现在非常慢.该loop指令自8086以来就已经存在,但对于编译器的使用速度来说太慢了,因为大约486我想,也许是386.(为什么循环指令很慢?英特尔难道没有有效地实现它吗?)

x86绝对是最后一个架构,你应该认为在允许和高效之间存在任何联系. 它的发展距离ISA设计的硬件非常远.但总的来说,任何大多数ISA都不是这样.例如,PowerPC的一些实现(特别是PlayStation 3中的Cell处理器)具有缓慢的微编码变量计数移位,但该指令是PowerPC ISA的一部分,因此根本不支持该指令将非常痛苦,并且不值得使用多个在热循环之外,指令而不是让微代码执行它.

你可以编写一个拒绝使用或警告已知慢速指令的汇编程序,enter或者loop,但有时你会优化大小,而不是速度,然后缓慢但小的指令loop是有用的.(https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code,看看x86机器码答案,就像我的8位字节的32位GCD循环一样x86代码使用许多小而慢的指令,如3-uop 1-byte xchg eax, r32,偶数inc/ loop作为3字节替代4字节test ecx,ecx/ jnz).优化代码大小在引导扇区的现实生活中很有用,或者用于512字节或4k"演示"等有趣的东西,它们只在极少量的可执行文件中绘制出酷炫的图形和播放声音.或者对于在启动期间仅执行一次的代码,较小的文件大小更好.或者在程序的生命周期中很少执行,较小的I-cache占用空间优于吹走大量缓存(以及等待代码获取的前端停顿).一旦指令字节实际到达CPU并被解码,这可能超过最大效率.特别是如果与代码大小节省相比存在较小的差异.

普通汇编程序只会抱怨不可编码的指令; 绩效分析不是他们的工作.他们的工作是将文本转换为输出文件中的字节(可选择使用对象文件元数据),允许您为任何您认为可能有用的目的创建所需的任何字节序列.


避免减速需要一次查看超过1条指令

大多数使代码变慢的方法都涉及明显不好的指令,只是整体组合很慢. 检查性能错误通常需要一次查看多于1条指令.

例如,此代码将导致Intel P6系列CPU上的部分寄存器停顿:

mov  ah, 1
add  eax, 123
Run Code Online (Sandbox Code Playgroud)

这些指令中的任何一个都可能是高效代码的一部分,因此汇编程序(只需要单独查看每个指令)不会警告您.虽然写AH完全是值得怀疑的; 通常是一个坏主意.也许一个更好的例子是在一个循环中的部分标志停顿,dec/jnzadcSnB-family之前的CPU上便宜. 在某些CPU的紧密循环中出现ADC/SBB和INC/DEC问题

如果您正在寻找一种工具来警告您有关昂贵的说明,那么GAS就不是了. 像IACA或LLVM-MCA这样的静态分析工具可能会帮助您在代码块中显示昂贵的指令. (什么是IACA以及如何使用它?(如何)我可以使用LLVM机器码分析器预测代码片段的运行时间?)它们的目的是分析循环,但是为它们提供一个代码块,无论它是否为循环身体与否将让他们向你展示每个教学在前端花费多少uops,也许是关于延迟的东西.

但实际上你必须更多地了解你正在优化的管道,以便理解每条指令的成本取决于周围的代码(它是否是长依赖链的一部分,以及整体瓶颈是什么).有关:


GCC/clang -O0的最大影响是语句之间根本不进行优化,将所有内容溢出到内存和重新加载,因此每个C语句都由一个单独的asm指令块完全实现.(用于一致的调试,包括在任何断点处停止时修改C变量).

但即使在一个语句的asm块内,clang -O0显然也会跳过优化传递,决定使用CISC内存 - 目标指令指令是否会获胜(给定当前调整).所以clang最简单的代码往往使用CPU作为加载存储机器,使用单独的加载指令将事物放入寄存器中.

GCC -O0正好像你期望的那样编译你的主要.(启用优化后,它当然会编译为xor %eax,%eax/ ret,因为a未使用.)

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $5, -4(%rbp)
    addl    $6, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

如何使用内存目标查看clang/LLVM add

我用clang8.2 -O3将这些函数放在Godbolt编译器资源管理器上. 每个函数都编译为一个asm指令,默认-mtune=generic为x86-64. (因为现代x86 CPU解码内存目的地有效地添加,最多只有内部uops作为单独的加载/添加/存储指令,有时更少与负载+添加部分的微融合.)

void add_reg_to_mem(int *p, int b) {
    *p += b;
}

 # I used AT&T syntax because that's what you were using.  Intel-syntax is nicer IMO
    addl    %esi, (%rdi)
    ret

void add_imm_to_mem(int *p) {
    *p += 3;
}

  # gcc and clang -O3 both emit the same asm here, where there's only one good choice
    addl    $3, (%rdi)
    ret
Run Code Online (Sandbox Code Playgroud)

gcc -O0输出仅仅是完全新空房禁地,如重装p两次,因为它则会覆盖指针而计算+3.我也可以使用全局变量而不是指针来为编译器提供一些无法优化的东西. -O0因为这可能会少得多.

    # gcc8.2 -O0 output
    ... after making a stack frame and spilling `p` from RDI to -8(%rbp)
    movq    -8(%rbp), %rax        # load p
    movl    (%rax), %eax          # load *p, clobbering p
    leal    3(%rax), %edx         # edx = *p + 3
    movq    -8(%rbp), %rax        # reload p
    movl    %edx, (%rax)          # store *p + 3
Run Code Online (Sandbox Code Playgroud)

GCC实际上甚至没有试图不吮吸,只是为了快速编译,并且尊重在语句之间保留内存中的所有内容的约束.

clang -O0输出对此不太可怕:

 # clang -O0
   ... after making a stack frame and spilling `p` from RDI to -8(%rbp)
    movq    -8(%rbp), %rdi    # reload p
    movl    (%rdi), %eax      # eax = *p
    addl    $3, %eax          # eax += 3
    movl    %eax, (%rdi)      # *p = eax
Run Code Online (Sandbox Code Playgroud)

另请参见如何从GCC /铿锵声组件输出中删除"噪音"?有关编写函数的更多信息,编译为有趣的asm而不进行优化.


如果我编译-m32 -mtune=pentium,gcc -O3会避免memory-dst添加:

P5奔腾微架构(1993)解码为类RISC内部微指令.复杂的指令需要更长的时间才能运行,并使其有序的双重问题 - 超标量管道上升.所以GCC避免使用它们,使用更加RISCy的x86指令子集,P5可以更好地管道化.

# gcc8.2 -O3 -m32 -mtune=pentium
add_imm_to_mem(int*):
    movl    4(%esp), %eax    # load p from the stack, because of the 32-bit calling convention

    movl    (%eax), %edx     # *p += 3 implemented as 3 separate instructions
    addl    $3, %edx
    movl    %edx, (%eax)
    ret
Run Code Online (Sandbox Code Playgroud)

您可以在上面的Godbolt链接上自己尝试一下; 这是来自哪里.只需在下拉列表中将编译器更改为gcc并更改选项.

不确定它实际上是一场胜利,因为它们是背靠背的.因为它是一个真正的胜利,gcc将不得不交错一些独立的指令.根据Agner Fog的指令表,add $imm, (mem)有序P5需要3个时钟周期,但在U或V管道中是可配对的.我读了他的微指南的P5 Pentium部分已经有一段时间了,但是有序管道肯定要按程序顺序启动每条指令.(缓慢的指令,包括商店,可以在其他指令开始之后完成.但是这里的添加和存储取决于之前的指令,所以他们肯定要等待).

如果您感到困惑,英特尔仍然将Pentium和Celeron品牌用于Skylake等低端现代CPU.这不是我们所说的.我们谈论的是最初的Pentium 微体系结构,现代Pentium品牌的CPU甚至都没有.

GCC拒绝-mtune=pentium没有-m32,因为没有64位奔腾CPU.第一代Xeon Phi使用Knight's Corner uarch,基于有序P5 Pentium,增加了类似于AVX512的矢量扩展.但是gcc似乎并不支持-mtune=knc.Clang确实如此,但选择使用内存目的地添加到此处-m32 -mtune=pentium.

LLVM项目直到P5过时(KNC除外)才开始,而gcc正在积极开发和调整,而P5广泛用于x86台式机.因此,gcc仍然知道一些P5调整内容并不奇怪,而LLVM并没有真正区别于现代x86,它将内存目标指令解码为多个uop,并且可以无序执行.

  • Downvoter:这是漫长而漫无边际的,需要很长时间才能达到目的,但我很确定其中没有一个是错误的.请解释一下您认为错误的原因. (9认同)

归档时间:

查看次数:

1214 次

最近记录:

6 年,10 月 前