这些"abundant"汇编代码的用途是什么,为什么要编译它们?



我不熟悉C++编译器的实现,我正在编写这样的C++片段(用于学习):

#include <vector>
void vector8_inc(std::vector<unsigned char>& v) {
for (std::size_t i = 0; i < v.size(); i++) {
v[i]++;
}
}
void vector32_inc(std::vector<unsigned int>& v) {
for (std::size_t i = 0; i < v.size(); i++) {
v[i]++;
}
}
int main() {
std::vector<unsigned char> my{ 10,11,2,3 };
vector8_inc(my);
std::vector<unsigned int> my2{ 10,11,2,3 };
vector32_inc(my2);
}

通过生成它的汇编代码并反汇编它的二进制代码,我找到了vector8_incvector32_inc:

g++ -S test.cpp -o test.a -O2
_Z11vector8_incRSt6vectorIhSaIhEE:
.LFB853:
.cfi_startproc
endbr64
movq    (%rdi), %rdx
cmpq    8(%rdi), %rdx
je  .L1
xorl    %eax, %eax
.p2align 4,,10
.p2align 3
.L3:
addb    $1, (%rdx,%rax)
movq    (%rdi), %rdx
addq    $1, %rax
movq    8(%rdi), %rcx
subq    %rdx, %rcx // line C: calculate size of the vector
cmpq    %rcx, %rax
jb  .L3
.L1:
ret
.cfi_endproc
......
_Z12vector32_incRSt6vectorIjSaIjEE:
.LFB854:
.cfi_startproc
endbr64
movq    (%rdi), %rax   // line A
movq    8(%rdi), %rdx
subq    %rax, %rdx // line C': calculate size of the vector
movq    %rdx, %rcx
shrq    $2, %rcx   // Line B: ← What is this used for?
je  .L6
addq    %rax, %rdx
.p2align 4,,10
.p2align 3
.L8:
addl    $1, (%rax)
addq    $4, %rax
cmpq    %rax, %rdx
jne .L8
.L6:
ret
.cfi_endproc
......

但实际上,这两个功能是";内联的";在main()中,上面显示的指令永远不会执行,而是编译并加载到内存中(我通过在a行添加断点并检查内存来验证这一点):main()包含上述段的重复逻辑

以下是我的问题:

  1. 这两个段用于什么?为什么要编译它们?_Z12vector32_incRSt6vectorIjSaIjEE_Z11vector8_incRSt6vectorIhSaIhEE
  2. _Z12vector32_incRSt6vectorIjSaIjEE(即shrq $2, %rcx)中的指令(B行)是否意味着"将向量的大小整数右移2位"?如果是,这个有什么用
  3. 为什么在vector8_inc中循环的每一圈都计算size(),而在vector32_inc中,它是在执行循环之前计算的?(参见g++-O2优化级别下的C行和C’行)

首先,如果使用c++filt:之类的工具对函数名进行解包,函数名的可读性会高得多

_Z12vector32_incRSt6vectorIjSaIjEE 
-> vector32_inc(std::vector<unsigned int, std::allocator<unsigned int> >&)
_Z11vector8_incRSt6vectorIhSaIhEE 
-> vector8_inc(std::vector<unsigned char, std::allocator<unsigned char> >&)

Q1

您将这些函数定义为全局(外部链接)。编译器一次编译一个模块。它不知道这个文件是否是你的整个程序,也不知道你以后是否会将它与其他对象文件链接。在后一种情况下,来自另一个模块的某些函数可能会调用vector8_incvector32_inc,因此编译器需要创建一个可用于此类调用的脱机版本。

传统的链接器在模块级别链接代码,并且不能选择性地从单个对象文件中排除函数。它必须在链接中包含test.o模块,这意味着二进制文件包含该模块的所有代码,包括行外vector_inc函数。你知道它们永远不能被调用,但编译器不知道,链接器也不检查。

这里有三种方法可以避免它们包含在二进制文件中:

  • 在源代码中将这些函数声明为static。这确保了它们不能从其他模块调用,因此,如果编译器可以从该模块内内联所有调用,则不需要发出离线版本。

  • 使用-fwhole-program编译,这告诉GCC假设函数不能从它看不到的任何地方调用。

  • 使用-flto链接以启用链接时间优化。这为链接器提供了更多的数据,这样它就可以变得智能,并可以省略从未调用过的单个函数(以及更多)。当我使用-flto时,我再次不再在二进制文件中看到行外代码。

Q2

右移2等于除以4。在GCC的实现中,std::vector类似乎包含一个开始指针(加载到%rax中)和一个结束指针(加载在%rdx中)。减去这些指针得到向量中的字节数。对于unsigned int的向量,每个元素的大小为4个字节,因此除以4得出元素的数量。

该值仅用于通过将向量与零进行比较来测试向量是否为空。在这种情况下,循环被跳过,函数什么也不做。(如果结果为零,shr将设置零标志,如果设置了零标志,je将跳转。这不应该发生,因为指向unsigned int的指针应该是4字节对齐的(除非库正在做一些聪明的事情,比如将元数据存储在低位)。因此,我认为这种转变实际上是不必要的,这可能是一个小的遗漏优化。

Q3

这最有可能处理混叠。

C++中有一条一般规则,粗略地说,通过一种类型的指针进行写入可能不会修改另一种类型。因此,在vector32_inc中,编译器知道通过指向unsigned int的指针写入向量的元素,不能修改向量的元数据(显然是其他类型的对象,很可能是指针)。因此,将size()计算从循环中取出是安全的,因为它在循环执行过程中不会发生变化。同样,用于迭代对象的指针可以始终保存在寄存器中。

然而,字符类型对此规则有一个特殊的豁免,部分原因是像memcpy这样的东西可以工作。因此,当您执行vec[i]++,通过指向unsigned char的指针进行写入时,编译器必须考虑指针可能实际指向某些vector元数据的可能性。(如果vector使用得当,但编译器不知道,则实际上不应该发生这种情况。)如果使用得当,则vec[i]++可能会修改内存中的元数据。下一个循环迭代应该使用新修改的元数据,所以它必须从内存中重新加载,以防万一。请注意,向量的起始指针也会重新加载,并使用新值,而不是始终将其保存在寄存器中。

最新更新