编译二进制文件的大小如何编译器avoid linear growth与模板的每个新类型实例化?
在使用新的实例化时,我看不出如何避免复制所有模板化代码。
我觉得编译时和二进制大小对于除了相当大的代码库中最简单的模板之外的所有人来说都是非常难以处理的。但是他们的普遍性表明编译器能够做一些魔术来使它们变得实用。
答案 0 :(得分:6)
许多模板函数足够小,可以有效地内联,因此做在二进制文件中获得线性增长 - 但它只不过是你使用等效的非模板函数获得的。
一个定义规则在这里很重要,因为它允许编译器假设具有相同模板参数的任何模板实例化生成相同的代码。如果它检测到模板函数早先已在源文件中实例化,则它可以使用该副本而不是生成新副本。名称修改使链接器可以从不同的编译源识别相同的函数。这些都不能保证,因为你的程序不能区分函数的相同副本之间的区别,但是编译器每天都比这更难实现优化。
需要过滤重复的一次是当函数包含静态变量时 - 只能有一个副本。但这可以通过过滤掉重复的函数或过滤掉静态变量本身来实现。
答案 1 :(得分:5)
有多种因素导致多个实例化对exacutable大小没有太大的危害:
也就是说,在可能的情况下,预先实例化模板会有所回报,特别是如果通常只使用少量的即时消息。一个很好的例子是IOStreams库,它不太可能与超过4种类型一起使用(通常它只与一个一起使用):将模板定义及其实例化移动到单独的转换单元中可能不会减少可执行文件的大小,但肯定会减少编译时间!从C ++ 11开始,可以将模板实例化声明为extern
,这允许定义可见,而不会在已知在其他地方实例化的特化项上进行隐式实例化。
答案 2 :(得分:3)
我认为你误解了模板的实现方式。模板在需要使用的基础上编译成相应的类/函数。
考虑以下代码......
template <typename Type>
Type mymax(Type a, Type b) {
return a > b ? a : b;
}
int main(int argc, char** argv)
{
}
编译这个,我得到以下程序集。
.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
你会发现它只包含主要功能。现在我更新我的代码以使用模板功能。
int main(int argc, char** argv)
{
mymax<double>(3,4);
}
编译我得到一个更长的程序集输出,包括处理双精度的模板函数。编译器看到模板函数被“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
现在让我说我改变代码以使用该功能两次。
int main(int argc, char** argv)
{
mymax<double>(3,4);
mymax<double>(4,5);
}
再次,让我们看看它创建的程序集。它与之前的输出相当,因为大多数代码只是编译器创建函数mymax,其中“Type”更改为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_
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
因此,基本上模板不会影响exec大小,只需手动编写函数即可。这只是一个方便。编译器将为给定类型的一个或多个用途创建一个函数,因此如果我使用它1或1000次,则只有一个实例。现在,如果我更新我的代码以处理像浮点数这样的新类型,我将在我的可执行文件中获得另一个函数,但无论我使用该函数多少次都只有一个函数。