我知道内联函数不使用堆栈来复制参数,但它只是在调用它的地方替换函数的主体。
考虑这两个函数:
inline void add(int a) {
a++;
} // does nothing, a won't be changed
inline void add(int &a) {
a++;
} // changes the value of a
如果堆栈不用于发送参数,编译器如何知道变量是否会被修改?替换这两个函数的调用后的代码是什么样子的?
如果您强制它内联方法,我已经检查了至少 GCC 对它的作用:
inline static void add1(int a) __attribute__((always_inline));
void add1(int a) {
a++;
} // does nothing, a won't be changed
inline static void add2(int &a) __attribute__((always_inline));
void add2(int &a) {
a++;
} // changes the value of a
int main() {
label1:
int b = 0;
add1(b);
label2:
int a = 0;
add2(a);
return 0;
}
此程序集输出如下所示:
.file "test.cpp"
.text
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
.L2:
movl $0, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, -8(%ebp)
addl $1, -8(%ebp)
.L3:
movl $0, -12(%ebp)
movl -12(%ebp), %eax
addl $1, %eax
movl %eax, -12(%ebp)
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
有趣的是,即使是第一次add1()
有效地调用它,因为在函数调用之外没有做任何事情,也没有被优化掉。
如果堆栈不用于发送参数,编译器如何知道变量是否会被修改?
正如 Matthieu M. 已经指出的那样,语言结构本身对堆栈一无所知。您为函数指定 inline 关键字只是为了给编译器一个提示并表达您希望该例程内联的愿望。如果发生这种情况完全取决于编译器。
编译器试图预测在特定情况下此过程的优势可能是什么。如果编译器决定内联函数会使代码变慢或变大到无法接受的程度,它就不会内联它。或者,如果它只是因为语法依赖而不能,例如其他代码使用函数指针进行回调,或者在动态/静态代码库中将函数导出到外部。
替换这两个函数的调用后的代码是什么样子的?
目前,在编译时没有内联此函数
g++ -finline-functions -S main.cpp
你可以看到它,因为在 main 的反汇编中
void add1(int a) {
a++;
}
void add2(int &a) {
a++;
}
inline void add3(int a) {
a++;
} // does nothing, a won't be changed
inline void add4(int &a) {
a++;
} // changes the value of a
inline int f() { return 43; }
int main(int argc, char** argv) {
int a = 31;
add1(a);
add2(a);
add3(a);
add4(a);
return 0;
}
我们看到对每个例程的调用:
main:
.LFB8:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $31, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %edi
call _Z4add1i // function call
leaq -4(%rbp), %rax
movq %rax, %rdi
call _Z4add2Ri // function call
movl -4(%rbp), %eax
movl %eax, %edi
call _Z4add3i // function call
leaq -4(%rbp), %rax
movq %rax, %rdi
call _Z4add4Ri // function call
movl $0, %eax
leave
ret
.cfi_endproc
使用 -O1 编译将完全从程序中删除所有函数,因为它们什么都不做。然而除了
__attribute__((always_inline))
允许我们查看内联代码时会发生什么:
void add1(int a) {
a++;
}
void add2(int &a) {
a++;
}
inline static void add3(int a) __attribute__((always_inline));
inline void add3(int a) {
a++;
} // does nothing, a won't be changed
inline static void add4(int& a) __attribute__((always_inline));
inline void add4(int &a) {
a++;
} // changes the value of a
int main(int argc, char** argv) {
int a = 31;
add1(a);
add2(a);
add3(a);
add4(a);
return 0;
}
现在:g++ -finline-functions -S main.cpp
结果:
main:
.LFB9:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $31, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %edi
call _Z4add1i // function call
leaq -4(%rbp), %rax
movq %rax, %rdi
call _Z4add2Ri // function call
movl -4(%rbp), %eax
movl %eax, -8(%rbp)
addl $1, -8(%rbp) // addition is here, there is no call
movl -4(%rbp), %eax
addl $1, %eax // addition is here, no call again
movl %eax, -4(%rbp)
movl $0, %eax
leave
ret
.cfi_endproc
inline
关键字有两个关键效果。一个效果是它暗示了实现“在调用点的函数体的内联替换比通常的函数调用机制更可取”。这种用法是一个提示,而不是强制,因为“在调用点执行此内联替换不需要实现”。
另一个主要影响是它如何修改一个定义规则。根据 ODR,程序必须包含在程序中使用 odr 的任何给定非内联函数的一个定义。这对内联函数不太适用,因为“内联函数应在使用它的每个翻译单元中定义......”。在一百个不同的翻译单元中使用相同的内联函数,链接器将面临一百个函数定义。这不是问题,因为同一函数的多个实现“......在每种情况下都应具有完全相同的定义”。看待这个问题的一种方式:仍然只有一个定义;看起来链接器有一大堆。
注意:所有引用的材料均来自 C++11 标准的第 7.1.2 节。
是什么让你认为有一个堆栈?即使有,是什么让您认为它将用于传递参数?
你必须明白有两个层次的推理:
在语言级别,如果您通过非常量引用传递参数,它可能会被函数修改。语言层面不知道这个神秘的“堆栈”是什么。注意:inline
关键字对函数调用是否内联几乎没有影响,它只是表示定义是内联的。
在机器级别......有很多方法可以实现这一点。进行函数调用时,您必须遵守调用约定。这个约定定义了函数参数(和返回类型)如何在调用者和被调用者之间交换,以及其中谁负责保存/恢复 CPU 寄存器。一般来说,因为它是如此低级,所以这个约定会根据每个 CPU 系列而改变。
例如,在 x86 上,几个参数将直接在 CPU 寄存器中传递(如果它们适合),而其余参数(如果有)将在堆栈上传递。