C ++编译器如何优化模板代码?

时间:2013-12-11 20:28:42

标签: c++ templates

编译二进制文件的大小如何编译器avoid linear growth与模板的每个新类型实例化?

在使用新的实例化时,我看不出如何避免复制所有模板化代码。

我觉得编译时和二进制大小对于除了相当大的代码库中最简单的模板之外的所有人来说都是非常难以处理的。但是他们的普遍性表明编译器能够做一些魔术来使它们变得实用。

3 个答案:

答案 0 :(得分:6)

许多模板函数足够小,可以有效地内联,因此在二进制文件中获得线性增长 - 但它只不过是你使用等效的非模板函数获得的。

一个定义规则在这里很重要,因为它允许编译器假设具有相同模板参数的任何模板实例化生成相同的代码。如果它检测到模板函数早先已在源文件中实例化,则它可以使用该副本而不是生成新副本。名称修改使链接器可以从不同的编译源识别相同的函数。这些都不能保证,因为你的程序不能区分函数的相同副本之间的区别,但是编译器每天都比这更难实现优化。

需要过滤重复的一次是当函数包含静态变量时 - 只能有一个副本。但这可以通过过滤掉重复的函数或过滤掉静态变量本身来实现。

答案 1 :(得分:5)

有多种因素导致多个实例化对exacutable大小没有太大的危害:

  1. 许多模板只是将事物传递给另一层。尽管可能存在相当多的代码,但在代码实例化和内联时,它们大多会消失。注意内联[并进行一些优化]很容易导致更大的代码。请注意,内联函数通常会导致更小的(和更快)代码(基本上因为其他必要的调用序列通常需要比内联更多的指令,优化器会更好通过更全面地了解正在发生的事情来进一步减少代码的机会。
  2. 如果没有内联模板代码,则需要将不同翻译单元中的重复实例化合并到一个实例中。我不是链接器专家,但我的理解是,例如,ELF使用不同的部分,链接器可以选择仅包括实际使用的那些部分。
  3. 在较大的可执行文件中,您需要一些在很多地方使用并有效共享的词汇类型和实例。使用自定义类型执行所有操作都是不错的主意,类型擦除肯定是避免类型太多的重要工具。
  4. 也就是说,在可能的情况下,预先实例化模板会有所回报,特别是如果通常只使用少量的即时消息。一个很好的例子是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次,则只有一个实例。现在,如果我更新我的代码以处理像浮点数这样的新类型,我将在我的可执行文件中获得另一个函数,但无论我使用该函数多少次都只有一个函数。