gpt4 book ai didi

c - 为什么gcc对函数中的局部变量重新排序?

转载 作者:太空狗 更新时间:2023-10-29 12:28:01 37 4
gpt4 key购买 nike

我写了一个C程序,只读取/写入一个大数组。出于好奇,我使用命令gcc -O0 program.c -o program编译了该程序,然后使用objdump -S命令反编译了C程序。

该问题末尾附带了read_arraywrite_array函数的代码和汇编。

我试图解释gcc如何编译函数。我使用//添加了我的评论和问题

取一首write_array()函数的汇编代码的开头

  4008c1:   48 89 7d e8             mov    %rdi,-0x18(%rbp) // this is the first parameter of the fuction
4008c5: 48 89 75 e0 mov %rsi,-0x20(%rbp) // this is the second parameter of the fuction
4008c9: c6 45 ff 01 movb $0x1,-0x1(%rbp) // comparing with the source code, I think this is the `char tmp` variable
4008cd: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) // this should be the `int i` variable.

我不了解的是:

1) char tmp显然是在 int i函数中的 write_array之后定义的。为什么gcc对这两个局部变量的存储位置重新排序?

2)从偏移量开始, int i-0x8(%rbp)处, char tmp-0x1(%rbp)处,这表示变量 int i占用7个字节?这很奇怪,因为 int i在x86-64机器上应该是4个字节。是不是我的猜测是gcc试图做一些调整?

3)我发现gcc优化选择非常有趣。是否有一些好的文件/书解释了gcc的工作原理? (第三个问题可能是题外话,如果您这样想,请忽略。我只是尝试看看是否有一些捷径来学习gcc用于编译的基 native 制。:-))

下面是一段功能代码:
#define CACHE_LINE_SIZE 64
static inline void
read_array(char* array, long size)
{
int i;
char tmp;
for ( i = 0; i < size; i+= CACHE_LINE_SIZE )
{
tmp = array[i];
}
return;
}

static inline void
write_array(char* array, long size)
{
int i;
char tmp = 1;
for ( i = 0; i < size; i+= CACHE_LINE_SIZE )
{
array[i] = tmp;
}
return;
}

以下是gcc -O0中write_array的反汇编代码:
00000000004008bd <write_array>:
4008bd: 55 push %rbp
4008be: 48 89 e5 mov %rsp,%rbp
4008c1: 48 89 7d e8 mov %rdi,-0x18(%rbp)
4008c5: 48 89 75 e0 mov %rsi,-0x20(%rbp)
4008c9: c6 45 ff 01 movb $0x1,-0x1(%rbp)
4008cd: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
4008d4: eb 13 jmp 4008e9 <write_array+0x2c>
4008d6: 8b 45 f8 mov -0x8(%rbp),%eax
4008d9: 48 98 cltq
4008db: 48 03 45 e8 add -0x18(%rbp),%rax
4008df: 0f b6 55 ff movzbl -0x1(%rbp),%edx
4008e3: 88 10 mov %dl,(%rax)
4008e5: 83 45 f8 40 addl $0x40,-0x8(%rbp)
4008e9: 8b 45 f8 mov -0x8(%rbp),%eax
4008ec: 48 98 cltq
4008ee: 48 3b 45 e0 cmp -0x20(%rbp),%rax
4008f2: 7c e2 jl 4008d6 <write_array+0x19>
4008f4: 5d pop %rbp
4008f5: c3 retq

最佳答案

即使在-O0,除非有调用方,否则gcc不会为static inline函数提供定义。在这种情况下,它实际上不是内联的:而是发出独立的定义。因此,我想您的拆卸就是出于此。

您使用的是旧版gcc吗? gcc 4.6.4将vars按该顺序放在堆栈上,但是4.7.3及更高版本使用其他顺序:

    movb    $1, -5(%rbp)    #, tmp
movl $0, -4(%rbp) #, i
在您的asm中,它们是按初始化顺序而不是声明顺序存储的,但是我认为这只是偶然,因为顺序在gcc 4.7中已更改。另外,像 int i=1;这样的初始化程序不会改变分配顺序,因此完全破坏了该理论。
记住 gcc is designed around a series of transformations from source to asm, so -O0 doesn't mean "no optimization"。您应该认为 -O0忽略了 -O3通常会做的一些事情。没有选择尝试从源代码到asm进行尽可能原义的转换。
一旦gcc决定了为它们分配空间的顺序:
  • charrbp-1:这是第一个可以保存char的位置。如果还有另一个char需要存储,则可以放在rbp-2处。
  • :位于intrbp-8:由于从rbp-1rbp-4的4个字节不是免费的,因此下一个可用的自然对齐位置是rbp-8

  • 或在gcc 4.7及更高版本中,-4是int的第一个可用位置,而-5是该位置下的下一个字节。

    RE:节省空间:
    的确,将char设置为-5会使触摸的地址最低 %rsp-5,而不是 %rsp-8,但这不会节省任何内容。
    堆栈指针在AMD64 SysV ABI中为16B对齐。 (从技术上讲, %rsp+8(堆栈args的开始)在输入之前对齐函数入口。) %rbp-8触摸 %rbp-5不会进入的新页面或缓存行的唯一方法是使堆栈小于4B对齐。即使在32位代码中,这也是极不可能的。
    该函数“分配”或“拥有”了多少堆栈:在AMD64 SysV ABI中,该函数“拥有” %rsp (That size was chosen because a one-byte displacement can go up to -128 )下方的128B红色区域。信号处理程序和用户空间堆栈的任何其他异步用户都将避免破坏红色区域,这就是为什么函数可以在不降低 %rsp的情况下写入 %rsp以下的内存的原因。因此,从这个 Angular 来看,我们使用多少红色区域无关紧要;信号处理程序耗尽堆栈的机会不受影响。
    在没有Redzone的32位代码中,对于任一顺序,gcc都使用 sub $16, %esp在堆栈上保留空间。 (尝试在Godbolt上使用 -m32)。同样,使用5个字节还是8个字节都没有关系,因为我们以16为单位进行保留。
    charint变量很多时,即使将声明混合在一起,gcc也会将 char打包到4B组中,而不是浪费空间来分散碎片:
    void many_vars(void) {
    char tmp = 1; int i=1;
    char t2 = 2; int i2 = 2;
    char t3 = 3; int i3 = 3;
    char t4 = 4;
    }
    with gcc 4.6.4 -O0 -fverbose-asm ,有助于标记哪个存储是哪个变量,这就是为什么编译器asm输出比反汇编更可取的原因:
        pushq   %rbp  #
    movq %rsp, %rbp #,
    movb $1, -4(%rbp) #, tmp
    movl $1, -16(%rbp) #, i
    movb $2, -3(%rbp) #, t2
    movl $2, -12(%rbp) #, i2
    movb $3, -2(%rbp) #, t3
    movl $3, -8(%rbp) #, i3
    movb $4, -1(%rbp) #, t4
    popq %rbp #
    ret
    我认为变量根据cct版本在 -O0处以声明的正向或反向顺序进行。

    我制作了 read_array函数的一个版本,该版本可在以下方面进行优化:
    // assumes that size is non-zero.  Use a while() instead of do{}while() if you want extra code to check for that case.
    void read_array_good(const char* array, size_t size) {
    const volatile char *vp = array;
    do {
    (void) *vp; // this counts as accessing the volatile memory, with gcc/clang at least
    vp += CACHE_LINE_SIZE/sizeof(vp[0]);
    } while (vp < array+size);
    }
    Compiles to the following, with gcc 5.3 -O3 -march=haswell:
            addq    %rdi, %rsi      # array, D.2434
    .L11:
    movzbl (%rdi), %eax # MEM[(const char *)array_1], D.2433
    addq $64, %rdi #, array
    cmpq %rsi, %rdi # D.2434, array
    jb .L11 #,
    ret
    将表达式强制转换为void是告诉编译器已使用值的规范方法。例如要禁止使用未使用的变量警告,可以编写 (void)my_unused_var;
    对于gcc和clang,使用 volatile指针取消引用确实可以生成内存访问,而无需tmp变量。 C标准对于构成对 volatile的访问的构成是非常非特定的,因此这可能不是完全可移植的。另一种方法是将读取的值 xor到累加器中,然后将其存储到全局值中。只要您不使用整个程序优化,编译器就不会知道没有东西读取全局,因此它无法优化计算。
    有关此第二种技术的示例,请参见 the vmtouch source code。 (它实际上为累加器使用了一个全局变量,这使代码变得笨拙。当然,这几乎无关紧要,因为它接触的是页面,而不仅仅是高速缓存行,因此即使在内存读取的情况下,TLB丢失和页面错误的瓶颈也会很快出现,在循环执行的依赖链中进行修改-写入。)

    我尝试编写gcc或clang可以编译为没有序言的函数(但假定 size最初非零)时失败了。 GCC始终希望针对 add rsi,rdi循环条件使用 cmp/jcc,即使使用 -march=haswellsub rsi,64/ jae可以像 cmp/jcc一样进行宏融合。但一般而言,在AMD上,GCC的内部循环较少。
    read_array_handtuned_haswell:
    .L0
    movzx eax, byte [rdi] ; overwrite the full RAX to avoid any partial-register false deps from writing AL
    add rdi, 64
    sub rsi, 64
    jae .L0 ; or ja, depending on what semantics you want
    ret
    Godbolt Compiler Explorer link with all my attempts and trial versions
    如果循环终止条件是 je,我可以得到类似 do { ... } while( size -= CL_SIZE );这样的循环,但是我似乎无法说服gcc在减去时捕获无符号借位。它要先减去然后再用 cmp -64/jb来检测下溢。这是 not that hard to get compilers to check the carry flag after an add to detect carry:/
    让编译器进行4-insn循环也很容易,但并非没有序言。例如计算结束指针(数组+大小)并递增一个指针,直到它大于或等于。
    幸运的是,这并不重要。我们得到的循环是好的。

    关于c - 为什么gcc对函数中的局部变量重新排序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36298567/

    37 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com