功能调用成本

时间:2011-08-05 05:10:43

标签: c++ gcc x86 x86-64

我正在处理的应用程序现在使用了一些蛮力数值算法,它可以调用许多微小的函数数十亿次。我正在徘徊通过使用倾斜和静态多态来消除函数调用可以提高性能。

在以下情况下调用函数相对于调用非内联函数和非内函函数的成本是多少:

1)通过函数指针调用函数

2)虚拟函数调用

我知道很难衡量,但是粗略估计会做到这一点。

谢谢!

4 个答案:

答案 0 :(得分:3)

要使成员函数调用编译器需要:

Fetch address of function -> Call function

调用虚函数编译器需要:

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

注意:该虚拟机制是编译器实现细节,因此对于不同的编译器,实现可能会有所不同,甚至可能没有vptrvtable。说了通常,编译器使用vptrvtable实现它,然后上面成立。

所以肯定会有一些开销(一个额外的Fetch),要确切知道它会产生多大的影响,你必须对源代码进行分析,没有更简单的方法。

答案 1 :(得分: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,该程序集将针对其他架构进行更改。

查看程序集,看起来它正在为虚拟调用使用两个额外的movq,它可能是vtable中的一些偏移量。请注意,在实际代码中,它需要保存一些寄存器(无论是函数指针还是虚拟调用),但虚拟调用仍需要两个额外的movq over函数指针。

答案 2 :(得分:0)

只需使用像AMD的代码分析师(使用IBS和TBS)这样的分析器,否则你可以选择更加“硬核”的路线并给Agner Fog的优化手册一个读取(它们将有助于精确指令时序和优化代码): http://www.agner.org/optimize/

答案 3 :(得分: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语法。

相关问题