为什么gcc重新sorting函数中的局部variables?

我写了一个只读/写大型数组的C程序。 我用gcc -O0 program.c -o program编译了程序出于好奇,我用objdump -S命令来objdump -S C程序。

read_arraywrite_array函数的代码和程序集附在这个问题的末尾。

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

write_array()函数的汇编代码的开始部分为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重新sorting这两个局部variables的内存位置?

2)从偏移量, int i-0x8(%rbp)char tmp-0x1(%rbp) ,这表明variablesint i需要7个字节? 这很奇怪,因为int i应该在x86-64机器上是4个字节。 不是吗? 我的猜测是gcc试图做一些alignment?

3)我发现gcc优化的select是相当有趣的 。 有一些很好的文档/书籍可以解释gcc是如何工作的吗? (第三个问题可能是题外话,如果你这么想的话,请忽略它,我只是想看看是否有一些捷径去学习gcc用来编译的底层机制:-))

下面是一段function代码:

 #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 

Solutions Collecting From Web of "为什么gcc重新sorting函数中的局部variables?"

即使在-O0 ,gcc也不会发出static inline函数的定义,除非有一个调用者。 在这种情况下,它并不实际内联:相反,它会发出一个独立的定义。 所以我想你的反汇编是从那个。


你使用的是一个非常古老的gcc版本吗? gcc 4.6.4将这些变量按顺序放在堆栈中,但4.7.3和更高版本使用另一个顺序:

  movb $1, -5(%rbp) #, tmp movl $0, -4(%rbp) #, i 

在你的asm中,它们按照初始化而不是声明的顺序存储,但是我认为这只是偶然的,因为使用gcc 4.7改变了顺序。 另外,在int i=1; 不改变分配的顺序,这样的理论就完全是鱼雷。

请记住, gcc是围绕着一系列从源代码到asm的转换而设计的,所以-O0并不意味着“不优化” 。 你应该把-O0看作是-O0-O3通常所做的一些事情。 没有任何选项会尝试从源代码到asm进行字面上的尽可能的翻译。

一旦gcc确定了为他们分配空间的顺序:

  • rbp-1char :这是第一个可以存放char 。 如果还有另一个需要存储的char ,它可以在rbp-2

  • rbp-8int :因为从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 (堆栈参数的开始)在函数入口处对齐,然后再按任何东西。) %rbp-8触摸新页面或缓存行的唯一方法是%rbp-5 wouldn' t是堆栈小于4B对齐。 这是极不可能的,即使在32位的代码。

至于有多少堆栈被这个函数“分配”或“拥有”:在AMD64 SysV ABI中,函数“拥有”128B以下的红色区域为%rsp (选择该大小是因为一个字节的位移可以去高达-128 ) 。 信号处理程序和用户空间堆栈的任何其他异步用户将避免破坏红色区域,这就是为什么函数可以写入%rsp下面的内存而不减少%rsp 。 所以从这个角度来看,我们使用的红色区域有多少并不重要, 信号处理程序用完堆栈的机会不受影响。

在没有redzone的32位代码中,对于任何一个命令,gcc都会在sub $16, %espsub $16, %esp上保留空间sub $16, %esp 。 (用Godbolt上的-m32尝试)。 所以我们再次使用5或8个字节并不重要,因为我们以16个为单位保留。

当有很多charint变量时,即使声明混合在一起,gcc也会将字符串打包到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; } 

使用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 

我认为变量以正向或反向顺序声明,取决于gcc版本,在-O0


我做了一个你的read_array函数的版本,在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); } 

用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东西的访问是非常不具体的,所以这可能不是完全可移植的。 另一种方法是将你读入的值存入累加器,然后将其存储到全局中。 只要你不使用全程序优化,编译器不知道没有东西读全局,所以不能优化计算。

有关此第二种技术的示例,请参阅vmtouch源代码 。 (它实际上使用了累加器的全局变量,这使得代码变得笨重,当然,这并不重要,因为它触及页面,而不仅仅是缓存行,所以它很快就会在TLB未命中和页面错误上造成瓶颈,在循环携带的依赖链中修改 – 写入)。


我尝试过,但没有写出gcc或clang会编译成一个没有序言的函数(它假定size最初是非零的):

 read_array_handtuned: .L0 mov al, dword [rdi] ; maybe not ideal on AMD: partial-reg writes have a false dep on old value. Hopefully the load can still start, and just the merging is serialized? add rdi, 64 sub rsi, 64 jae .L0 ; or ja, depending on what semantics you want 

Godbolt编译器资源管理器链接与我所有的尝试和试用版本

如果循环终止条件是je ,我可以得到类似的结果do { ... } while( size -= CL_SIZE ); 但是我似乎无法从编译器中获得最优代码来减去无符号借位。 他们想要减去然后cmp -64/jb来检测下溢。 在添加检测进位之后,编译器检查进位标志并不困难。

让编译器做一个4-insn循环也很容易,但不是没有序幕。 例如计算一个结束指针(数组+大小)并增加一个指针,直到它大于或等于。

对于保存在堆栈中的本地变量,地址顺序取决于堆栈的增长方向。 你可以参考堆栈是向上还是向下? 了解更多信息。

这很奇怪,因为int我应该在x86-64机器上是4个字节。 不是吗?

如果我的内存正确地为我服务,则x86-64机器上的int大小为8.您可以通过编写测试应用程序来打印sizeof(int)来确认它。