内联函数是如何编译成汇编的?
How are inline functions compiled to assembly?
我有一些循环更新值的 C++ 代码,出于好奇,我想看看构成正文循环的程序集。这让我尝试了一些内联在编译后的样子(编译器是带有 O2 的 MSVC)。
但是,当我将指令集与实际内联时我认为的样子进行比较时,我对我的发现感到有点困惑。这是一些上下文:
template<typename T>
struct ClassWithInline
{
Values *v;
ClassWithInline(Values *v) : v{ v } {}
T inlineMe(T * const c) const
{
// some function of *c, using v->some_constants
}
};
Values
对象只是包含常量的东西。 ClassWithInline
是另一个对象 Owner
的成员,所有者具有函数 callTheInline
:
struct Owner
{
ClassWithInline<double> a;
Values *v;
Owner(Values *v) : a{ ClassWithInline<double>(v) }, v{ v } {}
void callTheInline()
{
double *ptr = new double[100];
double *dptr = new double[100];
size_t the_end = std::floor(1000 + log(100000));
for (size_t n = 0; n < the_end; ++n)
{
dptr[n] = a.inlineMe(ptr + n);
}
ClassWithInline<double> b(v);
for (size_t n = 0; n < the_end; ++n)
{
dptr[n] = b.inlineMe(ptr + n);
}
}
};
(不稳定的结束迭代次数是为了让编译器在编译时不知道循环的大小并引入一些其他优化。)
现在,当我查看为那些 for 循环生成的程序集时,它们截然不同;事实上,从 a
调用 inlineMe
的汇编指令是原来的两倍。我该如何弥合这种差异?
a.inlineMe(ptr + n);
000000013F642094 mov rbp,rbx
000000013F642097 mov qword ptr [rsp+20h],r15
000000013F64209C sub rbp,rsi
000000013F64209F lea r15,[r9-3]
000000013F6420A3 mov r14,rsi
000000013F6420A6 lea r10,[rbx+8]
000000013F6420AA sub r14,rbx
000000013F6420AD nop dword ptr [rax]
000000013F6420B0 mov rcx,qword ptr [rdi]
000000013F6420B3 lea rdx,[r14+r10]
000000013F6420B7 movsd xmm0,mmword ptr [r10-8]
000000013F6420BD movsd xmm1,mmword ptr [rdx+rbp-10h]
000000013F6420C3 addsd xmm1,mmword ptr [r10]
000000013F6420C8 movsd xmm2,mmword ptr [rdi+8]
000000013F6420CD lea rax,[rcx+r8]
000000013F6420D1 mulsd xmm0,xmm3
000000013F6420D5 mulsd xmm2,xmm2
000000013F6420D9 addsd xmm1,mmword ptr [rbx+rax*8]
000000013F6420DE mov rax,r8
000000013F6420E1 sub rax,rcx
000000013F6420E4 addsd xmm1,mmword ptr [rbx+rax*8]
000000013F6420E9 subsd xmm1,xmm0
000000013F6420ED divsd xmm1,xmm2
000000013F6420F1 movsd mmword ptr [r14+r10-8],xmm1
000000013F6420F8 movsd xmm1,mmword ptr [r10+8]
000000013F6420FE addsd xmm1,mmword ptr [r10-8]
000000013F642104 mov rcx,qword ptr [rdi]
000000013F642107 movsd xmm0,mmword ptr [r10]
000000013F64210C movsd xmm2,mmword ptr [rdi+8]
000000013F642111 mulsd xmm0,xmm3
000000013F642115 lea rax,[rcx+r8]
000000013F642119 mulsd xmm2,xmm2
000000013F64211D addsd xmm1,mmword ptr [rbx+rax*8+8]
000000013F642123 mov rax,r8
000000013F642126 sub rax,rcx
000000013F642129 addsd xmm1,mmword ptr [rbx+rax*8+8]
000000013F64212F subsd xmm1,xmm0
000000013F642133 divsd xmm1,xmm2
000000013F642137 movsd mmword ptr [rdx],xmm1
000000013F64213B movsd xmm1,mmword ptr [r10+10h]
000000013F642141 addsd xmm1,mmword ptr [r10]
000000013F642146 mov rcx,qword ptr [rdi]
000000013F642149 movsd xmm0,mmword ptr [r10+8]
000000013F64214F movsd xmm2,mmword ptr [rdi+8]
000000013F642154 mulsd xmm0,xmm3
000000013F642158 lea rax,[rcx+r8]
000000013F64215C mulsd xmm2,xmm2
000000013F642160 addsd xmm1,mmword ptr [rbx+rax*8+10h]
000000013F642166 mov rax,r8
000000013F642169 sub rax,rcx
000000013F64216C addsd xmm1,mmword ptr [rbx+rax*8+10h]
000000013F642172 subsd xmm1,xmm0
000000013F642176 divsd xmm1,xmm2
000000013F64217A movsd mmword ptr [r14+r10+8],xmm1
000000013F642181 movsd xmm1,mmword ptr [r10+18h]
000000013F642187 addsd xmm1,mmword ptr [r10+8]
000000013F64218D mov rcx,qword ptr [rdi]
000000013F642190 movsd xmm0,mmword ptr [r10+10h]
000000013F642196 movsd xmm2,mmword ptr [rdi+8]
000000013F64219B mulsd xmm0,xmm3
000000013F64219F lea rax,[rcx+r8]
000000013F6421A3 mulsd xmm2,xmm2
000000013F6421A7 addsd xmm1,mmword ptr [rbx+rax*8+18h]
000000013F6421AD mov rax,r8
000000013F6421B0 add r8,4
000000013F6421B4 sub rax,rcx
000000013F6421B7 addsd xmm1,mmword ptr [rbx+rax*8+18h]
000000013F6421BD subsd xmm1,xmm0
000000013F6421C1 divsd xmm1,xmm2
000000013F6421C5 movsd mmword ptr [r14+r10+10h],xmm1
000000013F6421CC add r10,20h
000000013F6421D0 cmp r8,r15
000000013F6421D3 jb Owner::callTheInline+0B0h (013F6420B0h)
b.inlineMe(ptr + n);
000000013F6422A4 movsd xmm1,mmword ptr [rcx+r10*8-10h]
000000013F6422AB addsd xmm1,mmword ptr [rdx+rcx]
000000013F6422B0 movsd xmm0,mmword ptr [rdx+rcx-8]
000000013F6422B6 mulsd xmm0,xmm3
000000013F6422BA addsd xmm1,mmword ptr [rcx+r8*8-8]
000000013F6422C1 addsd xmm1,mmword ptr [rcx-8]
000000013F6422C6 subsd xmm1,xmm0
000000013F6422CA divsd xmm1,xmm5
000000013F6422CE movsd mmword ptr [rdi+rcx-8],xmm1
000000013F6422D4 movsd xmm2,mmword ptr [rdx+rcx-8]
000000013F6422DA addsd xmm2,mmword ptr [rdx+rcx+8]
000000013F6422E0 movsd xmm0,mmword ptr [rdx+rcx]
000000013F6422E5 mulsd xmm0,xmm3
000000013F6422E9 addsd xmm2,mmword ptr [rcx+r8*8]
000000013F6422EF addsd xmm2,mmword ptr [rcx]
000000013F6422F3 subsd xmm2,xmm0
000000013F6422F7 divsd xmm2,xmm5
000000013F6422FB movsd mmword ptr [rdi+rcx],xmm2
000000013F642300 movsd xmm0,mmword ptr [rdx+rcx+8]
000000013F642306 movsd xmm1,mmword ptr [rdx+rcx]
000000013F64230B addsd xmm1,mmword ptr [rcx+rbp]
000000013F642310 mulsd xmm0,xmm3
000000013F642314 addsd xmm1,mmword ptr [rcx+r8*8+8]
000000013F64231B addsd xmm1,mmword ptr [rcx+8]
000000013F642320 subsd xmm1,xmm0
000000013F642324 divsd xmm1,xmm5
000000013F642328 movsd mmword ptr [rdi+rcx+8],xmm1
000000013F64232E movsd xmm2,mmword ptr [rcx+r10*8+18h]
000000013F642335 addsd xmm2,mmword ptr [rdx+rcx+8]
000000013F64233B movsd xmm0,mmword ptr [rcx+rbp]
000000013F642340 mulsd xmm0,xmm3
000000013F642344 addsd xmm2,mmword ptr [rcx+r8*8+10h]
000000013F64234B addsd xmm2,mmword ptr [rcx+10h]
000000013F642350 subsd xmm2,xmm0
000000013F642354 divsd xmm2,xmm5
000000013F642358 movsd mmword ptr [r14+rcx],xmm2
000000013F64235E add rcx,20h
000000013F642362 sub rax,1
000000013F642366 jne Owner::callTheInline+2A4h (013F6422A4h)
函数内联主要有以下三个作用:
- 它消除了函数调用开销。
- 它允许编译器跨越函数的边界进行优化。
- 它允许编译器对传递给函数的硬编码参数做出硬性假设。这包括指向成员函数的 this 指针。
内联总是发生在 C++ 代码被翻译成汇编之前。编译器本质上将内联函数视为将被调用函数的源代码插入到调用位置。几乎。 (实际上,编译器通常 也 将内联函数编译成普通函数并为其分配弱链接,但这不会在进一步的内联过程中使用。这不是我们感兴趣的在这里。)
在您的示例中,a
是 Owner
的成员,而 b
是堆栈上的局部变量。 a
和 b
都保持状态 v
。
要寻址a
,编译器需要通过Owner
的this指针来寻址它。要寻址 b
编译器不需要使用 Owner
的 this 指针,它只是在堆栈上。仅此一项就已经在指令数量上产生了很大的差异。实际上,这还取决于是否允许编译器内联 callTheInline()
以及编译器对 Owner
实例的存储了解多少。
a.v
的值在函数 callTheInline()
结束后仍然存在,而 b
不会在函数结束后继续存在。这可能允许编译器省略某些计算。但是 b.v
不会持续到函数末尾,这允许编译器省略计算 inlineMe()
.
它们不是。(尤其是当它们只是模板时。)
它们在变成 asm 之前被内联(通常是编译器对数据流的内部表示,通常是某种 SSA)。在 之后发生了更多的优化,因此实际的 asm 取决于内联点的周围代码,当然还有 args 和对 return 值所做的事情。
例如,具有未在一个调用站点中使用的输出 arg 的函数可以优化计算它的函数部分。或者,如果其中一个 args 是一个编译时常量,那可以 极大地 简化生成的 asm。 (例如,if(x<8)
在内联和持续传播后可能会变成 if(false)
或 if(true)
。)
在您的例子中,您的一个循环使用了一个 class 成员对象,其指针可能指向任何地方。您根本没有显示 using ClassWithInline::v
函数,所以它根本是一个非静态成员函数,而不仅仅是一个模板化的自由函数,这很奇怪。
但如果ClassWithInline::v
确实进入其中,a.inlineMe(ptr + n);
将涉及this.v
和this.a.v
,这可能也可能不指向重叠内存。编译器不知道,所以必须做出保守的假设,或者发出循环的 2 个版本并检查 运行 快速或安全版本之前的重叠。这会破坏自动矢量化,并且需要更多 store/reload 才能使 asm 即使在出现别名的情况下也是正确的。
(这是一个 struct
,而不是 class
,所以那些成员是 public 并且这个函数的调用者可能在调用我们之前修改了那些成员。)
但是 b.inlineMe(ptr + n)
对两个指针都使用 this.v
,在内联后编译器可以看到。
涉及的其他内存来自 new
,已知不会与其他内存重叠。也就是说,任何预先存在的指针都不能指向 return 由 new[]
编辑的缓冲区。我认为 MSVC 做了足够的别名分析来解决这个问题。但考虑到缺乏自动矢量化,也许不会。
顺便说一句,调用两个指针 v
确实让 think/talk 感到困惑。
我有一些循环更新值的 C++ 代码,出于好奇,我想看看构成正文循环的程序集。这让我尝试了一些内联在编译后的样子(编译器是带有 O2 的 MSVC)。
但是,当我将指令集与实际内联时我认为的样子进行比较时,我对我的发现感到有点困惑。这是一些上下文:
template<typename T>
struct ClassWithInline
{
Values *v;
ClassWithInline(Values *v) : v{ v } {}
T inlineMe(T * const c) const
{
// some function of *c, using v->some_constants
}
};
Values
对象只是包含常量的东西。 ClassWithInline
是另一个对象 Owner
的成员,所有者具有函数 callTheInline
:
struct Owner
{
ClassWithInline<double> a;
Values *v;
Owner(Values *v) : a{ ClassWithInline<double>(v) }, v{ v } {}
void callTheInline()
{
double *ptr = new double[100];
double *dptr = new double[100];
size_t the_end = std::floor(1000 + log(100000));
for (size_t n = 0; n < the_end; ++n)
{
dptr[n] = a.inlineMe(ptr + n);
}
ClassWithInline<double> b(v);
for (size_t n = 0; n < the_end; ++n)
{
dptr[n] = b.inlineMe(ptr + n);
}
}
};
(不稳定的结束迭代次数是为了让编译器在编译时不知道循环的大小并引入一些其他优化。)
现在,当我查看为那些 for 循环生成的程序集时,它们截然不同;事实上,从 a
调用 inlineMe
的汇编指令是原来的两倍。我该如何弥合这种差异?
a.inlineMe(ptr + n);
000000013F642094 mov rbp,rbx
000000013F642097 mov qword ptr [rsp+20h],r15
000000013F64209C sub rbp,rsi
000000013F64209F lea r15,[r9-3]
000000013F6420A3 mov r14,rsi
000000013F6420A6 lea r10,[rbx+8]
000000013F6420AA sub r14,rbx
000000013F6420AD nop dword ptr [rax]
000000013F6420B0 mov rcx,qword ptr [rdi]
000000013F6420B3 lea rdx,[r14+r10]
000000013F6420B7 movsd xmm0,mmword ptr [r10-8]
000000013F6420BD movsd xmm1,mmword ptr [rdx+rbp-10h]
000000013F6420C3 addsd xmm1,mmword ptr [r10]
000000013F6420C8 movsd xmm2,mmword ptr [rdi+8]
000000013F6420CD lea rax,[rcx+r8]
000000013F6420D1 mulsd xmm0,xmm3
000000013F6420D5 mulsd xmm2,xmm2
000000013F6420D9 addsd xmm1,mmword ptr [rbx+rax*8]
000000013F6420DE mov rax,r8
000000013F6420E1 sub rax,rcx
000000013F6420E4 addsd xmm1,mmword ptr [rbx+rax*8]
000000013F6420E9 subsd xmm1,xmm0
000000013F6420ED divsd xmm1,xmm2
000000013F6420F1 movsd mmword ptr [r14+r10-8],xmm1
000000013F6420F8 movsd xmm1,mmword ptr [r10+8]
000000013F6420FE addsd xmm1,mmword ptr [r10-8]
000000013F642104 mov rcx,qword ptr [rdi]
000000013F642107 movsd xmm0,mmword ptr [r10]
000000013F64210C movsd xmm2,mmword ptr [rdi+8]
000000013F642111 mulsd xmm0,xmm3
000000013F642115 lea rax,[rcx+r8]
000000013F642119 mulsd xmm2,xmm2
000000013F64211D addsd xmm1,mmword ptr [rbx+rax*8+8]
000000013F642123 mov rax,r8
000000013F642126 sub rax,rcx
000000013F642129 addsd xmm1,mmword ptr [rbx+rax*8+8]
000000013F64212F subsd xmm1,xmm0
000000013F642133 divsd xmm1,xmm2
000000013F642137 movsd mmword ptr [rdx],xmm1
000000013F64213B movsd xmm1,mmword ptr [r10+10h]
000000013F642141 addsd xmm1,mmword ptr [r10]
000000013F642146 mov rcx,qword ptr [rdi]
000000013F642149 movsd xmm0,mmword ptr [r10+8]
000000013F64214F movsd xmm2,mmword ptr [rdi+8]
000000013F642154 mulsd xmm0,xmm3
000000013F642158 lea rax,[rcx+r8]
000000013F64215C mulsd xmm2,xmm2
000000013F642160 addsd xmm1,mmword ptr [rbx+rax*8+10h]
000000013F642166 mov rax,r8
000000013F642169 sub rax,rcx
000000013F64216C addsd xmm1,mmword ptr [rbx+rax*8+10h]
000000013F642172 subsd xmm1,xmm0
000000013F642176 divsd xmm1,xmm2
000000013F64217A movsd mmword ptr [r14+r10+8],xmm1
000000013F642181 movsd xmm1,mmword ptr [r10+18h]
000000013F642187 addsd xmm1,mmword ptr [r10+8]
000000013F64218D mov rcx,qword ptr [rdi]
000000013F642190 movsd xmm0,mmword ptr [r10+10h]
000000013F642196 movsd xmm2,mmword ptr [rdi+8]
000000013F64219B mulsd xmm0,xmm3
000000013F64219F lea rax,[rcx+r8]
000000013F6421A3 mulsd xmm2,xmm2
000000013F6421A7 addsd xmm1,mmword ptr [rbx+rax*8+18h]
000000013F6421AD mov rax,r8
000000013F6421B0 add r8,4
000000013F6421B4 sub rax,rcx
000000013F6421B7 addsd xmm1,mmword ptr [rbx+rax*8+18h]
000000013F6421BD subsd xmm1,xmm0
000000013F6421C1 divsd xmm1,xmm2
000000013F6421C5 movsd mmword ptr [r14+r10+10h],xmm1
000000013F6421CC add r10,20h
000000013F6421D0 cmp r8,r15
000000013F6421D3 jb Owner::callTheInline+0B0h (013F6420B0h)
b.inlineMe(ptr + n);
000000013F6422A4 movsd xmm1,mmword ptr [rcx+r10*8-10h]
000000013F6422AB addsd xmm1,mmword ptr [rdx+rcx]
000000013F6422B0 movsd xmm0,mmword ptr [rdx+rcx-8]
000000013F6422B6 mulsd xmm0,xmm3
000000013F6422BA addsd xmm1,mmword ptr [rcx+r8*8-8]
000000013F6422C1 addsd xmm1,mmword ptr [rcx-8]
000000013F6422C6 subsd xmm1,xmm0
000000013F6422CA divsd xmm1,xmm5
000000013F6422CE movsd mmword ptr [rdi+rcx-8],xmm1
000000013F6422D4 movsd xmm2,mmword ptr [rdx+rcx-8]
000000013F6422DA addsd xmm2,mmword ptr [rdx+rcx+8]
000000013F6422E0 movsd xmm0,mmword ptr [rdx+rcx]
000000013F6422E5 mulsd xmm0,xmm3
000000013F6422E9 addsd xmm2,mmword ptr [rcx+r8*8]
000000013F6422EF addsd xmm2,mmword ptr [rcx]
000000013F6422F3 subsd xmm2,xmm0
000000013F6422F7 divsd xmm2,xmm5
000000013F6422FB movsd mmword ptr [rdi+rcx],xmm2
000000013F642300 movsd xmm0,mmword ptr [rdx+rcx+8]
000000013F642306 movsd xmm1,mmword ptr [rdx+rcx]
000000013F64230B addsd xmm1,mmword ptr [rcx+rbp]
000000013F642310 mulsd xmm0,xmm3
000000013F642314 addsd xmm1,mmword ptr [rcx+r8*8+8]
000000013F64231B addsd xmm1,mmword ptr [rcx+8]
000000013F642320 subsd xmm1,xmm0
000000013F642324 divsd xmm1,xmm5
000000013F642328 movsd mmword ptr [rdi+rcx+8],xmm1
000000013F64232E movsd xmm2,mmword ptr [rcx+r10*8+18h]
000000013F642335 addsd xmm2,mmword ptr [rdx+rcx+8]
000000013F64233B movsd xmm0,mmword ptr [rcx+rbp]
000000013F642340 mulsd xmm0,xmm3
000000013F642344 addsd xmm2,mmword ptr [rcx+r8*8+10h]
000000013F64234B addsd xmm2,mmword ptr [rcx+10h]
000000013F642350 subsd xmm2,xmm0
000000013F642354 divsd xmm2,xmm5
000000013F642358 movsd mmword ptr [r14+rcx],xmm2
000000013F64235E add rcx,20h
000000013F642362 sub rax,1
000000013F642366 jne Owner::callTheInline+2A4h (013F6422A4h)
函数内联主要有以下三个作用:
- 它消除了函数调用开销。
- 它允许编译器跨越函数的边界进行优化。
- 它允许编译器对传递给函数的硬编码参数做出硬性假设。这包括指向成员函数的 this 指针。
内联总是发生在 C++ 代码被翻译成汇编之前。编译器本质上将内联函数视为将被调用函数的源代码插入到调用位置。几乎。 (实际上,编译器通常 也 将内联函数编译成普通函数并为其分配弱链接,但这不会在进一步的内联过程中使用。这不是我们感兴趣的在这里。)
在您的示例中,a
是 Owner
的成员,而 b
是堆栈上的局部变量。 a
和 b
都保持状态 v
。
要寻址a
,编译器需要通过Owner
的this指针来寻址它。要寻址 b
编译器不需要使用 Owner
的 this 指针,它只是在堆栈上。仅此一项就已经在指令数量上产生了很大的差异。实际上,这还取决于是否允许编译器内联 callTheInline()
以及编译器对 Owner
实例的存储了解多少。
a.v
的值在函数 callTheInline()
结束后仍然存在,而 b
不会在函数结束后继续存在。这可能允许编译器省略某些计算。但是 b.v
不会持续到函数末尾,这允许编译器省略计算 inlineMe()
.
它们不是。(尤其是当它们只是模板时。)
它们在变成 asm 之前被内联(通常是编译器对数据流的内部表示,通常是某种 SSA)。在 之后发生了更多的优化,因此实际的 asm 取决于内联点的周围代码,当然还有 args 和对 return 值所做的事情。
例如,具有未在一个调用站点中使用的输出 arg 的函数可以优化计算它的函数部分。或者,如果其中一个 args 是一个编译时常量,那可以 极大地 简化生成的 asm。 (例如,if(x<8)
在内联和持续传播后可能会变成 if(false)
或 if(true)
。)
在您的例子中,您的一个循环使用了一个 class 成员对象,其指针可能指向任何地方。您根本没有显示 using ClassWithInline::v
函数,所以它根本是一个非静态成员函数,而不仅仅是一个模板化的自由函数,这很奇怪。
但如果ClassWithInline::v
确实进入其中,a.inlineMe(ptr + n);
将涉及this.v
和this.a.v
,这可能也可能不指向重叠内存。编译器不知道,所以必须做出保守的假设,或者发出循环的 2 个版本并检查 运行 快速或安全版本之前的重叠。这会破坏自动矢量化,并且需要更多 store/reload 才能使 asm 即使在出现别名的情况下也是正确的。
(这是一个 struct
,而不是 class
,所以那些成员是 public 并且这个函数的调用者可能在调用我们之前修改了那些成员。)
但是 b.inlineMe(ptr + n)
对两个指针都使用 this.v
,在内联后编译器可以看到。
涉及的其他内存来自 new
,已知不会与其他内存重叠。也就是说,任何预先存在的指针都不能指向 return 由 new[]
编辑的缓冲区。我认为 MSVC 做了足够的别名分析来解决这个问题。但考虑到缺乏自动矢量化,也许不会。
顺便说一句,调用两个指针 v
确实让 think/talk 感到困惑。