运行时一次高效的函数选择器

Efficient function selector once at runtime

我有一个操作需要并行执行很多次。例如,在点云处插入图像。对于这个操作,我有几个变体。例如不同的插值函数(考虑线性、二次、三次等)。问题是,如何在运行时有效地 select 操作一次。我想避免对操作的每次调用进行分支。

通常情况下,我会为此使用模板实例化。但是,我通过 Matlab Mex API 调用该函数。这意味着,我不知道编译时的“选择”操作。

现在,我正在考虑使用函数指针,但我没有使用它们的经验。什么是 selecting 特定操作的一个变体的有效方法,以便后续调用将直接转移到正确的版本。

最小示例:

class Image
{
public:
    size_t siz[3] = { 0, 0, 0 }; // image size (always 3D)
    double *f; // input image
    
    Image(double *f, size_t i, size_t j, size_t k) : f(f), siz{i, j, k} {
    }

    double interp_v1(size_t offset) {
    return // insert code to do interpolation method 1
    }

    double interp_v2(size_t offset) {
    return // insert code to do interpolation method 2
    }

    double interp_v3(size_t offset) {
    return // insert code to do interpolation method 3
    }

    double (*interp)(size_t offset) {
    return interp_v1 // use interp_v1 when interp is called (can be changed at runtime)
    }

}

如果我对你的问题理解正确,你可以在输入你反复调用的代码部分之前提前选择操作。

double interp_v1(std::size_t offset)
{
    //implementation here
}

double interp_v2(std::size_t offset)
{
    //implementation here
}

int main()
{
    double (*interp_func)(std::size_t);

    if ( /* some condition */ ) {
        interp_func = interp_v1;
    }
    else if ( /* some other condition */ ) {
        interp_func = interp_v2;
    }

    //Loop that does the heavy lifting
    for (int counter = 0; counter != 1000000; ++counter) {
        auto some_variable = interp_func(offset);
    }
}

可能这两个选项应该大致相同。如果性能真的很重要,只需测量您的代码即可。我做了一个基准测试,ifs 和指针一样快。

请记住,如果你使用函数指针,你有一个间接,并且使用“if”语句分支应该不是一个大问题,因为分支预测每次都会开始猜测(理论上)一些电话后。所以你应该只选择感觉更清晰、更容易理解的那个。在我的例子中,if 语句使这一点更清楚,并且可能使内联更可行。

我尝试过并且看起来同样快(在某些情况下甚至更快)的另一个选项是动态多态性。这个也更容易维护,以防有人想要添加新方法。这是一个动态多态的例子。:

struct Base {
    void whatever() = 0;
};

struct Method1: Base {
    void whatever() override {
//Code for first method
    }
};

struct Method2: Base {
    void whatever() override {
//COde for second method..
    }
};

这里是 link 我的基准测试人员:https://quick-bench.com/q/1mR0EyYrqvzunpEGbrHBw_U7eEY

我相信手动设置函数指针与虚函数与一小部分switch/case之间的差异与真正的底层操作相比不会有太大差异。

如果谈论图片和大量像素,您应该考虑最佳算法,而不是使用函数或 vtable 指针的一两个间接寻址。

此外,我希望代码大小无关紧要,您可以使用要调用的函数对循环进行模板化,并在循环本身内部获得真正为零的 运行 时间开销,因为成员指针是一个编译时间常数。

class Image
{
    public:
        double *f; // input image
        size_t siz[3] = { 0, 0, 0 }; // image size (always 3D)

        Image(double *f, size_t i, size_t j, size_t k) : f(f), siz{i, j, k} {
        }

        double interp_v1(size_t offset) {
            return 0; // insert code to do interpolation method 1
        }

        double interp_v2(size_t offset) {
            return 0;// insert code to do interpolation method 2
        }

        double interp_v3(size_t offset) {
            return 0;// insert code to do interpolation method 3
        }

        template < auto which_func >
            double loop( size_t offset )
            {
                //Loop that does the heavy lifting
                for (int counter = 0; counter != 1000000; ++counter) {
                    auto some_variable = (this->*which_func)(offset);
                }

                return 0;
            }

};


int main()
{
    double f[3];
    Image img{ f, 1,1,1 };

    int what = 1;

    switch ( what )
    {
        case 0:
            img.loop<&Image::interp_v1>(0);
            break;

        case 1:
            img.loop<&Image::interp_v2>(0);
            break;

        case 0:
            img.loop<&Image::interp_v3>(0);
            break;
    }

}

我不知道你有完整的数据结构。但是如果你有静态函数或者可以修改为静态函数,你也许也可以删除“this->*”。如果确实有问题,这可以避免取消引用 this 指针。

所有性能问题的提示:衡量、衡量、衡量。通常情况下,编译器通过优化循环之外的东西非常好。因此,您可能已经在没有任何“编译器提示”的情况下获得了指针取消引用的内容。我相信您的算法以及内存布局和调用顺序更为重要。缓存行未命中和这种昂贵的问题比单指针使用更重要!

如果您只是想要一个使用函数指针的示例,这里有一个使用函数指针数组的方法。设置代码,以便您可以在运行时更改正在使用的函数指针。

double interp_v1(size_t offset) {
return 1;// insert code to do interpolation method 1
}

double interp_v2(size_t offset) {
return 2;// insert code to do interpolation method 2
}

double interp_v3(size_t offset) {
return 3;// insert code to do interpolation method 3
}

// Array of function pointers with the specified signature
double (*interp_array[])(size_t offset) =
{ interp_v1, interp_v2, interp_v3 };

// Function pointer with the specified signature
double (*interp_ptr)(size_t offset) = interp_array[0];

// Set the function pointer
void interp_set(int i) {
    if( i > 0 && i <= sizeof(interp_array)/sizeof(interp_array[0]) ) {
        interp_ptr = interp_array[i-1];
    }
}

// Function interp uses whatever funtion interp_ptr is pointing to
double interp(size_t offset) {
return interp_ptr(offset);
}