我不熟悉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_inc
和vector32_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()包含上述段的重复逻辑
以下是我的问题:
- 这两个段用于什么?为什么要编译它们?
_Z12vector32_incRSt6vectorIjSaIjEE
、_Z11vector8_incRSt6vectorIhSaIhEE
_Z12vector32_incRSt6vectorIjSaIjEE
(即shrq $2, %rcx
)中的指令(B行)是否意味着"将向量的大小整数右移2位"?如果是,这个有什么用- 为什么在
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_inc
或vector32_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]++
可能会修改内存中的元数据。下一个循环迭代应该使用新修改的元数据,所以它必须从内存中重新加载,以防万一。请注意,向量的起始指针也会重新加载,并使用新值,而不是始终将其保存在寄存器中。