[英]How do C++ compilers optimize template code?
How do compilers avoid linear growth in the size of the compiled binary with each new type instantiation of a template? 编译器如何通过模板的每个新类型实例化来避免编译二进制文件大小的线性增长 ?
I don't see how we can avoid making a copy of all the templated code when a new instantiation is used. 我没有看到当使用新的实例化时我们如何避免复制所有模板化代码。
I feel that compile times and binary sizes would be made extremely unwieldy for all but the simplest templates in a reasonably large code base. 我觉得编译时间和二进制大小对于除了相当大的代码库中最简单的模板之外的所有人来说都是非常笨重的。 But their prevalence suggests that compilers are able to do some magic to make them practical. 但是它们的流行表明编译器能够做一些魔术来使它们变得实用。
Many template functions are small enough to inline effectively, so you do get linear growth in the binary - but it is no more than you would get with equivalent non-template functions. 许多模板功能是足够小,有效地串联,所以你得到的二元线性增长-但它是没有比你更会用同等的非模板函数得到。
The One Definition Rule is important here, as it allows the compiler to assume that any template instantiation with identical template parameters generates identical code. 一个定义规则在这里很重要,因为它允许编译器假设具有相同模板参数的任何模板实例化生成相同的代码。 If it detects that the template function has already been instantiated earlier in a source file, it can use that copy instead of generating a new one. 如果它检测到模板函数早先已在源文件中实例化,则它可以使用该副本而不是生成新副本。 Name mangling makes it possible for a linker to recognize the same function from different compiled sources. 名称修改使链接器可以从不同的编译源识别相同的函数。 None of this is guaranteed since your program shouldn't be able to tell the difference between identical copies of a function, but compilers do harder optimizations than this every day. 这些都不能保证,因为您的程序不能区分函数的相同副本之间的区别,但编译器每天都会做比这更难的优化。
The one time that duplicates are required to be filtered out is when a function contains a static variable - there can only be one copy. 需要过滤重复的一次是当函数包含静态变量时 - 只能有一个副本。 But that can be achieved either by filtering out the duplicate functions, or filtering out the static variables themselves. 但这可以通过过滤掉重复的函数或过滤掉静态变量本身来实现。
There are multiple things which result in multiple instantiations not being too harmful to the exacutable size: 有多种因素会导致多个实例化对exacutable大小没有太大的危害:
That said, where possible it does pay off to preinstantiate templates, especially if there are only a small number of instantations which are generally used. 也就是说,在可能的情况下,预先实例化模板会有所回报,特别是如果通常只使用少量的即时消息。 A great example is the IOStreams library which is unlikely to be used with more than 4 types (typically it is used with just one): moving the template definitions and their instantiations into separate translation units may not reduce the executable size but will certainly reduce the compile time! 一个很好的例子是IOStreams库,它不太可能与超过4种类型一起使用(通常它只与一个一起使用):将模板定义及其实例化移动到单独的转换单元中可能不会减少可执行文件的大小,但肯定会减少编译时间! Starting with C++11 it is possible to declare template instantiations as extern
which allows the definitions to be visible without getting implicitly instantiated on specializations which are known to be instantiated elsewhere. 从C ++ 11开始,可以将模板实例化声明为extern
,这允许定义可见而不会在已知在其他地方实例化的特化上进行隐式实例化。
I think you're misunderstanding how templates are implemented. 我认为你误解了模板的实现方式。 Templates are compiled on a need-to-use basis into a corresponding class/function. 模板在需要使用的基础上编译成相应的类/函数。
Consider the following code... 考虑以下代码......
template <typename Type>
Type mymax(Type a, Type b) {
return a > b ? a : b;
}
int main(int argc, char** argv)
{
}
Compiling this, I get the following assembly. 编译这个,我得到以下程序集。
.file "example.cpp"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1"
.section .note.GNU-stack,"",@progbits
You'll notice it only contains the main function. 你会发现它只包含主要功能。 Now I update my code to use the template function. 现在我更新我的代码以使用模板功能。
int main(int argc, char** argv)
{
mymax<double>(3,4);
}
Compiling that I get a much longer assembly output including the template function to handle doubles. 编译我得到一个更长的程序集输出,包括处理双精度的模板函数。 The compiler saw the template function was being used by the type "double" so made a function to handle that case. 编译器看到模板函数被“double”类型使用,因此创建了一个处理该情况的函数。
.file "example.cpp"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movabsq $4616189618054758400, %rdx
movabsq $4613937818241073152, %rax
movq %rdx, -24(%rbp)
movsd -24(%rbp), %xmm1
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
call _Z5mymaxIdET_S0_S0_
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.section .text._Z5mymaxIdET_S0_S0_,"axG",@progbits,_Z5mymaxIdET_S0_S0_,comdat
.weak _Z5mymaxIdET_S0_S0_
.type _Z5mymaxIdET_S0_S0_, @function
_Z5mymaxIdET_S0_S0_:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movsd %xmm0, -8(%rbp)
movsd %xmm1, -16(%rbp)
movsd -8(%rbp), %xmm0
ucomisd -16(%rbp), %xmm0
jbe .L9
movq -8(%rbp), %rax
jmp .L6
.L9:
movq -16(%rbp), %rax
.L6:
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size _Z5mymaxIdET_S0_S0_, .-_Z5mymaxIdET_S0_S0_
.ident "GCC: (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1"
.section .note.GNU-stack,"",@progbits
Now let's say I change the code to use that function twice. 现在让我们说我改变代码以使用该函数两次。
int main(int argc, char** argv)
{
mymax<double>(3,4);
mymax<double>(4,5);
}
Again, let's look at the assembly it creates. 再次,让我们看看它创建的程序集。 It's comparable to the previous output because most of that code was just the compiler creating the function mymax where "Type" is changed to a double. 它与之前的输出相当,因为大多数代码只是编译器创建函数mymax,其中“Type”更改为double。 No matter how many times I use that function, it will only be declared once. 无论我使用该函数多少次,它都只会被声明一次。
.file "example.cpp"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movabsq $4616189618054758400, %rdx
movabsq $4613937818241073152, %rax
movq %rdx, -24(%rbp)
movsd -24(%rbp), %xmm1
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
call _Z5mymaxIdET_S0_S0_
movabsq $4617315517961601024, %rdx
movabsq $4616189618054758400, %rax
movq %rdx, -24(%rbp)
movsd -24(%rbp), %xmm1
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
call _Z5mymaxIdET_S0_S0_
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.section .text._Z5mymaxIdET_S0_S0_,"axG",@progbits,_Z5mymaxIdET_S0_S0_,comdat
.weak _Z5mymaxIdET_S0_S0_
.type _Z5mymaxIdET_S0_S0_, @function
_Z5mymaxIdET_S0_S0_:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movsd %xmm0, -8(%rbp)
movsd %xmm1, -16(%rbp)
movsd -8(%rbp), %xmm0
ucomisd -16(%rbp), %xmm0
jbe .L9
movq -8(%rbp), %rax
jmp .L6
.L9:
movq -16(%rbp), %rax
.L6:
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size _Z5mymaxIdET_S0_S0_, .-_Z5mymaxIdET_S0_S0_
.ident "GCC: (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1"
.section .note.GNU-stack,"",@progbits
So basically templates don't affect the exec size any more than writing the functions by hand. 因此,基本上模板不会影响exec大小,只需手动编写函数即可。 It's just a convenience. 这只是一个方便。 The compiler will create a function for one or more uses of a given type so if I use it 1 or 1000 times, there will only be one instance of it. 编译器将为给定类型的一个或多个用途创建一个函数,因此如果我使用它1或1000次,则只有一个实例。 Now if I update my code to also handle a new type like floats, I'll get another function in my executable, but only one no matter how many times I use that function. 现在,如果我更新我的代码以处理像浮点数这样的新类型,我将在我的可执行文件中获得另一个函数,但无论我使用该函数多少次都只有一个函数。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.