C++ 函数指针:从性能的角度来看,简单的规范用法是否不好?如果是,那么c++;11个替代方案?

C++ 函数指针:从性能的角度来看,简单的规范用法是否不好?如果是,那么c++;11个替代方案?,c++,c++11,C++,C++11,我在我的c++代码中经常使用函数指针,总是以符合这个简单规范示例的方式使用(例如,函数具有相同的I/O,但所需的操作仅在运行时已知): #包括 使用名称空间std; 整数相加(整数第一,整数第二){ 返回第一个+第二个; } 整数减法(整数第一,整数第二){ 返回第一秒; } int操作(int-first,int-second,int(*functocall)(int,int)){ 返回(*函数调用)(第一,第二); } int main(){ INTA,b; 整数(*加号)(整数,整数)=相

我在我的
c++
代码中经常使用函数指针,总是以符合这个简单规范示例的方式使用(例如,函数具有相同的I/O,但所需的操作仅在运行时已知):

#包括
使用名称空间std;
整数相加(整数第一,整数第二){
返回第一个+第二个;
}
整数减法(整数第一,整数第二){
返回第一秒;
}
int操作(int-first,int-second,int(*functocall)(int,int)){
返回(*函数调用)(第一,第二);
}
int main(){
INTA,b;
整数(*加号)(整数,整数)=相加;
int(*减)(int,int)=减法;
a=操作(7,5,加上);
b=操作(20,a,负);

cout当然,您必须对其进行分析,但一般来说,通过函数指针调用的性能困难在于编译器无法为您提供内联的好处。类似这样的功能可能会更快:

enum FuncTodo{
    PLUS,
    MINUS,
};

int operation(int first, int second, FuncTodo todo) {
    switch(todo) {
        case PLUS: return first + second;
        case MINUS: return first - second;
    }
}

当然,在您的使用中,这可能是不可能的(如果您想在代码<>操作< /代码>之后编写新的操作来扩展代码),即使可能,您也可以认为它不太可维护。


最后,在进行概要分析时要小心。在您的示例中,编译器原则上可以计算出每个调用的实际目的地(您使用
plus
调用
操作
,但编译器可以“知道”
plus的值实际上是
添加
)。因此,您的指导性示例可能比实际代码优化得更好,在实际代码中,传递给
操作的函数指针直到运行时才确定。

通常在使用函数指针或跳转表时,这是因为我们需要在运行时选择要调用的函数,但我们不能使用模板,也不想支付性能pe(运行时)多态性的数量。在这些狭隘的情况下,就性能而言,这与您将获得的一样好。至少在x86硬件上

然而,它的维护和扩展不如更惯用的技术那么容易*,因此,除非您确实需要通过使用函数指针和跳转表来提高性能,否则最好使用多态性、模板、C++11
函数和lambdas,甚至可能是一个开关

总之,在编写跳转表之前,首先分析代码



*“更多惯用技巧”:在某些领域(如我的领域)跳转表是习惯性的,很容易维护。YMMV.

< P>如果你真的担心性能,你应该考虑做编译时调度-模板化操作;这肯定适用于你的简单例子;不确定你的实际使用是否需要真正的运行时操作绑定(在这种情况下,这个解决方案不起作用)。

模板
int运算(int-first,int-second,函子f)
{
返回f(第一,第二);
}
...
a=操作(7,5,std::plus/*自C++14以来,滚动您自己的,否则*/);
b=操作(20,a,标准::负);

通过函数指针调用的性能开销当然是不可忽略的,特别是对于像add()和subtract()这样短的函数这就是为什么C++ STD::排序击败C的QSOL例程,对于简单的基本类型。

< P>如果不能使用编译时机制来删除函数的运行时评估(即:需要一个函数指针),那么你不妨看看推测性虚拟化。 有关Visual Studio编译器开发人员提供的名为“编译器机密”的简要介绍,请访问

更面向对象的代码版本如下所示:

struct Operation {
    virtual int op(int first, int secont) = 0;
    virtual ~Operation() = default;
};


struct Add : public Operation {
    virtual int op(int first,  int second) {
        return first + second;
    }
    virtual ~Add() {}
};

struct Sub : public Operation {
    virtual int op(int first,  int second) {
        return first - second;
    }
    virtual ~Sub() {}
};

struct Mul: public Operation {
    virtual int op(int first,  int second) {
        return first * second;
    }
    virtual ~Mul() {}
};

int main () {
    std::unique_ptr<Operation> o = new Add;
    auto a = o(10,20);
}
int main() {
    // using profile guided optimization he might figure that the two most
    // frequent calls are to Add or Sub

    Operator *op; // is something unknown

    int a;
    if (typeid(op) == typeid(Add)) {
        a = 10 + 20;
    }
    else if (typeid(op) == typeid(Sub)) {
        a = 10 - 20;
    }
    else {
        a = op(10,20);
    }
}

如果您关注的是性能(而不是可读性、可维护性等),那么有各种线程化代码技术你应该阅读关于这方面的文章并尝试一些。

非常松散地说,已经存在了一段时间的技术通常被优化为性能。这适用于函数指针。调用函数时,速度不会慢。性能将取决于C++编译器所做的优化。G最适合基准测试(针对使用C++11闭包的东西,如lambdas)@BasileStarynkevitch我从来没有学过其他的构造,所以我很难进行基准测试。这就是我问的主要原因。那么
std::function
呢?@Caramiriel:从使用函数指针的基础首先是提高性能的假设出发,为什么要使用
std::function?另一个与性能相关的问题是,在某些处理器上,函数指针所需的间接调用可能很慢。在最近的英特尔或AMD台式机或笔记本电脑处理器上,情况就不那么如此了。@Steve:谢谢,我真的很喜欢这种结构,因为它简单(加上Bathsheba的评论,我现在不再担心性能损失了)。我以后会更多地使用它。还要感谢你的最后一段(我的典型功能比“添加”更复杂,但最好记住你的警告,特别是因为我的问题非常笼统)即使开关案例调用函数,这也会更快,因为编译器会内联调用。我相信它对所有小函数都会这样做(尽管编译器可能会有所不同)。因此,您不必像答案所描述的那样手动内联代码(尽管在这种情况下,似乎不太麻烦)现代编译器在优化许多
switch
块方面并不是非常出色,但我喜欢这种技术与模板实例化相结合,用于那些不太简单的操作,比如添加和减去内容。
struct Operation {
    virtual int op(int first, int secont) = 0;
    virtual ~Operation() = default;
};


struct Add : public Operation {
    virtual int op(int first,  int second) {
        return first + second;
    }
    virtual ~Add() {}
};

struct Sub : public Operation {
    virtual int op(int first,  int second) {
        return first - second;
    }
    virtual ~Sub() {}
};

struct Mul: public Operation {
    virtual int op(int first,  int second) {
        return first * second;
    }
    virtual ~Mul() {}
};

int main () {
    std::unique_ptr<Operation> o = new Add;
    auto a = o(10,20);
}
int main () {
    std::unique_ptr<Operation> o = new Add;
    auto a = o(10,20);
}


int main() {
    // compiler is able to prove that it will always be the Add function.
    std::unique_ptr<Add> add = new Add;
    auto a = add(10,20);
}

int main() {
    // compiler is able to prove that the scope  of add will never leave
    // main.
    Add add;
    auto a = add(10,20);
}

int main() {
    // compiler can show that inlining is faster.
    Add add;
    auto a = 10 + 20
}
int main() {
    // using profile guided optimization he might figure that the two most
    // frequent calls are to Add or Sub

    Operator *op; // is something unknown

    int a;
    if (typeid(op) == typeid(Add)) {
        a = 10 + 20;
    }
    else if (typeid(op) == typeid(Sub)) {
        a = 10 - 20;
    }
    else {
        a = op(10,20);
    }
}