2013-12-11 35 views
8

編譯器的大小與編譯器的每個新類型實例化編譯器的大小如何編譯器?avoid linear growthC++編譯器如何優化模板代碼?

我不明白我們如何避免在使用新實例時製作所有模板化代碼的副本。

我覺得編譯時間和二進制大小對於所有人來說都會變得極其笨重,除非最簡單的模板在相當大的代碼庫中。但是他們的流行表明編譯器能夠做出一些魔術來使它們變得切實可行。

+7

他們沒有。 (儘管它們可能會爲相同類型進行一些合併)這就是爲什麼可能會用幾行精心編寫的模板代碼來崩潰C++編譯器。 – Mysticial

+0

是什麼讓你覺得他們做? –

+1

無論您創建了多少次實例,類模板實例化都會出現在您的exe文件中。與功能模板相同。由於您聲明瞭1000個變量類型爲矢量,因此您不會獲得大量可執行文件。 – polkadotcadaver

回答

6

許多模板函數足夠小以便有效內聯,因此做的在二進制文件中獲得了線性增長 - 但它不會超過使用等效非模板函數獲得的效果。

一個定義規則在這裏非常重要,因爲它允許編譯器假定具有相同模板參數的任何模板實例都會生成相同的代碼。如果它檢測到模板函數早已在源文件中實例化,則它可以使用該副本而不是生成新副本。名稱改變使得鏈接器可以從不同的編譯源識別相同的功能。所有這些都不能保證,因爲你的程序不應該能夠區分函數的相同副本,但編譯器每天都會做出比這更難的優化。

需要過濾掉重複的一次是函數包含一個靜態變量 - 只能有一個副本。但是,這可以通過過濾掉重複函數或者自己濾除靜態變量來實現。

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,其中「類型」更改爲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次,它將只有一個實例。現在,如果我更新我的代碼以處理像float這樣的新類型,那麼我將在我的可執行文件中獲得另一個函數,但無論使用該函數多少次都只有一個函數。

+0

他說「每個**新**類型實例的模板?」。 mymax (3,4);會增加可執行文件的大小。 –

+0

他知道這一點。他所聯繫的答案也是如此。我只是詳細闡述了這個答案,表明模板函數在需要的基礎上添加到可執行文件中,並且使用鏈接問題的命名法添加了多個「實例化」,並不會增加可執行文件的大小,因爲多個實例化將使用相同的函數。 – voodoogiant

5

有多種事情,導致沒有太有害exacutable尺寸多個實例:

  1. 許多模板只是過客事情經過另一層。雖然可能有相當多的代碼,但它在代碼實例化和內聯時大部分消失。注意內聯[和做一些優化]可以很容易地導致更大的代碼,但。請注意,內聯功能往往導致更小(和更快)的代碼(主要是因爲否則必要的調用序列通常需要比內聯的指令更多的指令,優化程序有更好的機會進一步減少整體代碼看看發生了什麼)。
  2. 如果未內聯模板代碼,則需要將不同翻譯單元中的重複實例合併爲一個實例。我不是鏈接專家,但我的理解是,例如,ELF使用不同的部分,鏈接器可以選擇僅包含實際使用的部分。
  3. 在更大的可執行文件中,您需要一些詞彙類型和實例,這些類型和實例在許多地方都有使用並且可以有效共享。使用自定義類型做所有事情都不是好主意,類型刪除無疑是避免太多類型的重要工具。

這就是說,在可能的情況下,它確實還原了實例化模板,特別是如果只有少量通常使用的臨時實例。一個很好的例子是IOStreams庫,它不太可能用於超過4種類型(通常只用於一種):將模板定義及其實例化移入單獨的轉換單元中可能不會減小可執行文件的大小,但肯定會減少編譯時間!從C++ 11開始,可以將模板實例聲明爲extern,這允許定義可見而不隱式地實例化已知在別處實例化的專業化。

+0

我也認爲OP是從模板=函數的(錯誤的)想法開始的,但這顯然是錯誤的,C++中的模板是元編程的一個例子,它是一種在軟件內部生成軟件的技術,函數更多必要和不太抽象的概念。也涉及不同的機制,例如對於被稱爲「重載」的模板部分專用化的次數過多,並且這可能導致某人認爲它在運行時就像函數重載一樣起作用。 – user2485710