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

10

7
它们不会这样做。(尽管它们可能对相同类型进行一些聚合)这就是为什么用几行精心编写的模板代码就可以让C++编译器崩溃的原因。 - Mysticial
1
一个类模板实例化只会在你的exe中出现一次,无论你创建了多少个它的实例。函数模板也是如此。你不会因为声明了1000个类型为vector<int>的变量而得到一个庞大的可执行文件。 - user2428400
不要忘记,模板通常会被内联。 - R. Martinho Fernandes
3
以下是一种几乎让我们尝试的所有编译器都崩溃的示例:http://chat.stackoverflow.com/transcript/message/3657411#3657411 - Mysticial
1
还有一个导致ICC崩溃的:http://chat.stackoverflow.com/transcript/message/3657932#3657932 - Mysticial
显示剩余3条评论
3个回答

6
许多模板函数足够小,可以有效地内联,所以您确实会得到二进制文件的线性增长-但与等效的非模板函数一样,并没有更多的增长。
在这里,一个定义规则很重要,因为它允许编译器假定任何具有相同模板参数的模板实例化生成相同的代码。如果检测到模板函数已在源文件中早期实例化过,它可以使用该副本而不是生成新副本。名称改编使链接器能够从不同的编译源中识别相同的函数。虽然程序无法区分函数的相同副本,但编译器每天都会进行更难的优化,但没有任何保证。
唯一需要过滤重复项的时间是当函数包含静态变量时-只能有一个副本。但这可以通过过滤重复函数或过滤静态变量本身来实现。

5
有多种因素导致多个实例化对可执行文件大小并不太有害:
  1. 许多模板只是将事物传递到另一层。尽管可能有相当多的代码,但在代码被实例化和内联时大部分代码都会消失。请注意,内联[并进行一些优化]很容易导致更大的代码。请注意,内联函数通常会导致更小(和更快)的代码(基本上是因为否则必要的调用序列通常需要比内联的指令还要多,而且优化器有更好的机会通过更全面的视图来进一步减少代码)。
  2. 如果模板代码没有被内联,则需要在不同的翻译单元中合并重复的实例化。我不是链接器专家,但我的理解是,例如ELF使用不同的部分,链接器可以选择仅包括实际使用的那些部分。
  3. 在更大的可执行文件中,您将需要一些词汇类型和实例化,这些类型和实例化在许多地方都被使用并有效共享。使用自定义类型做所有事情是不好的想法,类型擦除无疑是避免过多类型的重要工具。
话虽如此,在可能的情况下,预实例化模板确实会有所回报,特别是如果通常仅使用少量实例。一个很好的例子是IOStreams库,它不太可能与多个类型一起使用(通常只与一种类型一起使用):将模板定义及其实例化移到单独的翻译单元中可能不会减小可执行文件大小,但肯定会减少编译时间!从C++11开始,可以将模板实例化声明为extern,这允许定义在专门已知被实例化的特化上显式可见而不会被隐式实例化。

我认为 OP 的起点是错误的,他认为模板=函数,但这显然是错误的。在 C++ 中,模板是元编程的一个例子,它是一种在软件内部生成软件的技术,而函数则是一个更加命令式和不太抽象的概念。还有不同的机制涉及其中,例如,对于模板的部分特化很多时候被称为“重载”,这可能会让人们认为这就像运行时的函数重载一样工作。 - user2485710

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”被更改为双精度浮点数。无论我使用该函数多少次,它都只会被声明一次。

    .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

基本上,模板不会增加代码的执行大小,就像手写函数一样。它只是一种方便的方法。编译器将为给定类型的一个或多个用途创建一个函数,因此,如果我使用它1次或1000次,只会有一个实例。现在,如果我更新我的代码以处理新类型(例如浮点数),我将在可执行文件中得到另一个函数,但无论我使用该函数多少次,只会有一个。


他说,“每个模板的新类型实例化?” mymax<int>(3,4); 会增加可执行文件的大小。 - Mustafa Ozturk
他知道这一点。他链接的答案也是这样说的。我只是在阐述那个答案,展示模板函数是在需要时添加到可执行文件中的,并且使用链接问题的命名法进行多个“实例化”,不会显著增加可执行文件的大小,因为多个实例将使用相同的函数。 - voodoogiant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接