C -基于命令行输入有效地改变函数指针

vyswwuz2  于 2023-01-20  发布在  其他
关注(0)|答案(3)|浏览(126)

我有几个相似的函数,比如A,B,C,我想用命令行选项选择其中一个,而且,我调用了这个函数数十亿次,因为我定义了一个函数指针Phi,并只设置一次,到所需的函数,而不是检查函数中的变量数十亿次,但是当我设置,Phi = A,(所以不考虑用户输入)我的代码运行约24秒,当我添加一个if-else并设置Phi为所需的函数,我的代码运行约30秒与完全相同的参数.(当然命令行选项设置Phi为A)什么是有效的方式来处理这种情况?
我的职责:

double funcA(double r)
{
    return 0;
}

double funcB(double r)
{
    return 1;
}

double funcC(double r)
{
    return r;
}
void computationFunctionFast(Context *userInputs) {
    double (*Phi)(double) = funcA;
    
    /* computation codes */
}
void computationFunctionSlow(Context *userInputs) {
    double (*Phi)(double);
    switch (userInputs->funcEnum) {
        case A:
        Phi = funcA;
        break;

        case B:
        Phi = funcB;
        break;

        case C:
        Phi = funcC;
    }
    
    /* computation codes */
}

我试过gcc,clang,icx的-O2和-O3优化。(gcc在上述情况下没有性能差异,但性能最差)虽然我使用的是C,我也试过std::function。我试过在不同的作用域中定义Phi函数等。

qnyhuwrf

qnyhuwrf1#

一般来说,这里有几件事对性能稍有不利:

  • 分支/比较导致分支预测/指令高速缓存的低效使用,并且还可能影响流水线操作。
  • 函数指针的效率是出了名的低,因为它们通常会阻塞内联,而且编译器通常对此无能为力。

下面是一个基于您的代码的示例:

double computationFunctionSlow (int input, double val) {
    double (*Phi)(double);
    switch (input) {
        case 0:  Phi = funcA; break;
        case 1:  Phi = funcB; break;
        case 2:  Phi = funcC; break;
    }
    double res = Phi(val);
    return res;
}

叮当声15.0.0 x86_64-O3给出:

computationFunctionSlow:                # @computationFunctionSlow
        cmp     edi, 2
        ja      .LBB3_1
        movsxd  rax, edi
        lea     rcx, [rip + .Lswitch.table.computationFunctionSlow]
        jmp     qword ptr [rcx + 8*rax]         # TAILCALL
.LBB3_1:
        xorps   xmm0, xmm0
        ret
.Lswitch.table.computationFunctionSlow:
        .quad   funcA
        .quad   funcB
        .quad   funcC

即使我选择的数字是相邻的,通常的编译器也无法优化出比较cmp,即使我包含了一个default: return 0;,它仍然存在,你可以很容易地手动优化任何具有连续索引的switch到函数指针跳转表中,如下所示:

double computationFunctionSlow (int input, double val) {
    double (*Phi[3])(double) = {funcA, funcB, funcC};
    double res = Phi[input](val);
    return res;
}

叮当声15.0.0 x86_64-O3给出:

computationFunctionSlow:                # @computationFunctionSlow
        movsxd  rax, edi
        lea     rcx, [rip + .L__const.computationFunctionSlow.Phi]
        jmp     qword ptr [rcx + 8*rax]         # TAILCALL
.L__const.computationFunctionSlow.Phi:
        .quad   funcA
        .quad   funcB
        .quad   funcC

这使得代码稍微好一点,因为比较指令/分支现在被删除了。然而,这实际上是一个微观优化,不应该对性能有太大影响。你必须确定它是否有任何改进。
(Also gcc 12.2没有很好地优化这段代码,所以我在这个例子中使用了clang。)
Godbolt链接:https://godbolt.org/z/ja4zerj7o

dgjrabp2

dgjrabp22#

没有更“有效”的方法来处理这种情况,你已经在做你应该做的了。
您观察到的时间差异是因为:
1.在第一种情况下(Phi = funcA),编译器知道函数总是相同的,因此能够优化它的调用。根据你的“计算代码”,这可能意味着内联函数并为你简化大量的计算。
1.在第二种情况下(Phi = <choice from user>),编译器不知道哪个函数会被选中,因此无法优化代码的其他部分对它的调用,也无法像第一种情况那样将优化传播到“计算代码”的其他部分。
一般来说,你能做的并不多,动态函数指针本身就增加了一些运行时开销,使优化变得更困难(或者不可能)。
你可以尝试在不同的函数或不同的分支中复制“计算代码”,这些代码是在AssertPhi等于一个常数之后才输入的,如下所示:

void computationFunctionSlow(Context *userInputs) {
    if (userInputs->funcEnum == A) {
        const double (*Phi)(double) = funcA;
        // computation code
    } else if (...) {
        // ...
    } 
}

在上面的代码中,编译器知道在任何if块中,Phi的值只能有一个值,因此 * 可以 * 执行上面第1点中讨论的相同优化。

c3frrgcw

c3frrgcw3#

当你使用enum来选择一个函数指针时,你不需要在你的userInputs中放置一个enum,直接在结构中添加函数指针就可以了,并且消除了每次调用时的分支。
代替

struct Context
{
    .
    .
    .
    enum funcType funcEnum;
};

使用

struct Context
{
    .
    .
    .
    double (*phi)(double);
};

你会得到这样的结果:

void computationFunctionSlow(Context *userInputs) {

    /* computation codes */
    double result = userInputs->phi( data );
}

相关问题