2011-08-05 51 views
0

我處理的,現在使用了一些蠻力數值算法調用許多微小的功能數十億次的應用。我在使用傾斜和靜態多態消除函數調用,可以改善性能。功能通話費用

什麼是調用相對於調用在以下情況下的非內聯和非本徵函數的函數的成本:

1)通過函數指針

2)虛擬函數調用

函數調用

我知道這是很難衡量,而是一個非常粗略的估計會做到這一點。

謝謝!

+8

現代編譯器現在爲你在線小功能,即使你不指定'inline'。您是否分析了應用程序或查看了生成的程序集(使用優化開關構建)?沒有代碼/編譯器設置,我們所說的任何東西都是猜測。 –

+1

如何分析和尋找瓶頸? – littleadv

+0

@硅片。猜測是可以的,只要它來自理解問題的人。我不知道。 – pic11

回答

3

爲了使一個成員函數調用編譯器需要:

Fetch address of function -> Call function 

要調用虛函數編譯器需要:

Fetch address of vptr -> Fetch address of the function -> Call function 

注:虛擬機制是編譯器的實現細節,所以實施可能有些許差異,不同的編譯器,有可能甚至不爲此事vptrvtable。話雖這麼說通常情況下,編譯器與vptrvtable實現它,然後上面也是如此。

所以肯定會有一些開銷(一個額外的Fetch),要準確地知道它會產生多大的影響,您將不得不以簡單的方式來描述源代碼。

+0

vtable的地址最常被加載到一個寄存器中(如'this')。這降低了虛擬呼叫的成本。 –

+0

@Bo佩爾森:我承認我不知道如何編譯器可能會在這種情況下優化,因此,也許你說的是正確的。感謝您爲透視添加了新的維度。 –

+0

這有點讓人誤解。現代CPU的流水線很多。一次和兩次提取之間的差異不是因素二,因爲第二次提取取決於第一次。這意味着你無法儘早開始提取。 – MSalters

0

只需使用一個分析器像AMD的codeanalyst(使用IBS和TBS),否則你可以去更多的「鐵桿」路線,並昂納霧的優化手冊讀取(它們將幫助精密指令時序和優化您的代碼) :http://www.agner.org/optimize/

2

這取決於你的目標架構和編譯器,但有一兩件事你可以做的是寫一個小的測試和檢查產生的裝配。

我做了一個做了測試:

// test.h 
#ifndef FOO_H 
#define FOO_H 

void bar(); 

class A { 
public: 
    virtual ~A(); 
    virtual void foo(); 
}; 

#endif 

// main.cpp 
#include "test.h" 

void doFunctionPointerCall(void (*func)()) { 
    func(); 
} 

void doVirtualCall(A *a) { 
    a->foo(); 
} 

int main() { 
    doFunctionPointerCall(bar); 

    A a; 
    doVirtualCall(&a); 

    return 0; 
} 

注意,你甚至不需要寫TEST.CPP,因爲你只需要檢查大會的main.cpp。

要查看編譯器裝配輸出,用gcc使用標誌-S:

gcc main.cpp -S -O3 

這將創建一個文件main.s,與組件的輸出。 現在我們可以看到gcc爲這些調用生成了什麼。

doFunctionPointerCall:

.globl _Z21doFunctionPointerCallPFvvE 
    .type _Z21doFunctionPointerCallPFvvE, @function 
_Z21doFunctionPointerCallPFvvE: 
.LFB0: 
    .cfi_startproc 
    jmp *%rdi 
    .cfi_endproc 
.LFE0: 
    .size _Z21doFunctionPointerCallPFvvE, .-_Z21doFunctionPointerCallPFvvE 

doVirtualCall:這裏

.globl _Z13doVirtualCallP1A 
    .type _Z13doVirtualCallP1A, @function 
_Z13doVirtualCallP1A: 
.LFB1: 
    .cfi_startproc 
    movq (%rdi), %rax 
    movq 16(%rax), %rax 
    jmp *%rax 
    .cfi_endproc 
.LFE1: 
    .size _Z13doVirtualCallP1A, .-_Z13doVirtualCallP1A 

注意我使用的是x86_64的,大會將其他achitectures改變。

展望大會,它看起來像它使用兩個額外的MOVQ虛擬呼叫,它可能是一些虛函數表偏移。請注意,在一個真正的代碼,那就需要節省一些寄存器(無論是函數指針或虛擬呼叫),但虛擬呼叫仍然需要在函數指針兩個額外MOVQ。

+0

與函數指針不同的是,您已經選擇了要調用的函數。您應該爲此添加代碼。 –

0

函數調用是一個顯著的開銷,如果功能都很小。當進行多次呼叫時,在現代CPU上優化的CALL和RETURN仍然很明顯。此外,小功能可以跨內存傳播,因此CALL/RETURN也可能導致緩存未命中和過度分頁。

//code 
int Add(int a, int b) { return a + b; } 
int main() { 
    Add(1, Add(2, 3)); 
... 
} 

// NON-inline x86 ASM 
Add: 
    MOV eax, [esp+4] // 1st argument a 
    ADD eax, [esp+8] // 2nd argument b 
    RET 8 // return and fix stack 2 args * 4 bytes each 
    // eax is the returned value 

Main: 
    PUSH 3 
    PUSH 2 
    CALL [Add] 
    PUSH eax 
    PUSH 1 
    CALL [Add] 
... 

// INLINE x86 ASM 
Main: 
    MOV eax, 3 
    ADD eax, 2 
    ADD eax, 1 
... 

如果優化你的目標和你調用很多小的功能,它總是最好的內聯它們。對不起,我不關心c/C++編譯器使用的醜陋的ASM語法。