【问题标题】:Compile time generated function dispatcher with minimal overhead编译时生成的函数调度器,开销最小
【发布时间】:2018-11-14 10:19:28
【问题描述】:

我正在尝试使用编译时生成的数组来实现一个快速的函数调度器,以便能够在运行时在 O(1) 中使用它。

一些代码行只是为了澄清:

template<int i>
void f()
  {
  // do stuff 
  }

// specialized for every managed integer 
template<>
void f<1>
{
// do stuff
}

Dispatcher<1,5,100,300> dispatcher;  
dispatcher.execute(5); // this should call f<5>()

我们将 N 称为调度程序的输入数(在这种情况下为 4),将 M 称为调度程序输入的最大值(在这种情况下为 300)。

我已经能够创建一个大小等于 M 的数组。这利用了这样一个事实,即在运行时您可以执行以下操作:

dispatcher.execute(5) -> internalArray[5]();

这当然行得通,但对于大维度的数组是不可行的。

最好的办法是只生成一个包含 N 个元素的数组,然后做一些数学技巧将输入索引转换为第二个数组的索引。

在示例中,将 1,5,100,300 分别转换为 0,1,2,3。我已经能够做一种预处理方法来转换它们,但我正在寻找一种方法来避免这一步。

换句话说,我认为我正在寻找某种最小的完美散列,可以在编译时以非常有效的方式用于我的特定情况(理想情况下没有任何开销,例如:goto: MyInstruction)。

我不是在寻找使用虚函数、std::map 或复杂操作的替代方案。

有不清楚的地方请教。

PS 我正在使用 C++11,但欢迎任何想法

[编辑] 我知道标签是 GCC 的值语言扩展。有了这些,我也许可以实现我的目标,但需要一个可移植的解决方案。

【问题讨论】:

  • 如果你打电话给dispatcher.execute(4)会发生什么?
  • 如果调度器应该是编译时的,那么这意味着.execute()参数也应该是编译时的,为什么不把它作为模板参数呢?然后execute&lt;N&gt;() 会调用f&lt;N&gt;,尽管我仍然不明白为什么在这种特殊情况下你想要一个调度程序而不是直接调用。如果您希望 execute 接受运行时参数,那么通过调度不清楚您想要什么。
  • execute 方法是否需要运行时参数?还是总是编译时间?
  • 你仍然可以创建Perfect_hash_function
  • 你实测过std::unordered_map的性能吗?它专为有效解决此类问题而设计。

标签: c++ c++11 template-meta-programming constexpr perfect-hash


【解决方案1】:

嗯,我不知道你是否能够做你想做的事。编写一个为任何输入创建完美散列函数的代码在我看来很漂亮......不可行。

无论如何,这是编写代码的简单解决方案。它是 C++17,但可以通过一些技巧与 C++11 一起使用。

template<int i> void f();

template <int... Is>
struct Dispatcher
{
    template <int I> constexpr auto execute_if(int i)
    {
        if  (I == i)
            f<I>();
    }

    constexpr auto execute(int i)
    {
        (execute_if<Is>(i), ...);
    }
};

auto test()
{
    Dispatcher<1,5,100,300> dispatcher;  
    dispatcher.execute(5);
}

上面的代码只是简单的跳转,因为5是一个编译时间常数:

test():                               # @test()
        jmp     void f<5>()            # TAILCALL

如果参数是运行时变量,那么它会进行一系列比较:

auto test(int i)
{
    Dispatcher<1,5,100,300> dispatcher;  
    dispatcher.execute(i);
}
test(int):                               # @test(int)
        cmp     edi, 99
        jg      .LBB0_4
        cmp     edi, 1
        je      .LBB0_7
        cmp     edi, 5
        jne     .LBB0_9
        jmp     void f<5>()            # TAILCALL
.LBB0_4:
        cmp     edi, 100
        je      .LBB0_8
        cmp     edi, 300
        jne     .LBB0_9
        jmp     void f<300>()          # TAILCALL
.LBB0_9:
        ret
.LBB0_7:
        jmp     void f<1>()            # TAILCALL
.LBB0_8:
        jmp     void f<100>()          # TAILCALL

该解决方案可以改进为执行二分搜索,但这并不简单。

【讨论】:

  • 非常有趣的解决方案。我对标签调度做了一些类似的事情。这种方法在 O(N) 中提供了一个解决方案,其中 N 定义如上。这本身还不错,但我一直在寻找 O(1) 的解决方案。如果不存在解决方案,我正在考虑根据 N,M 值自动使用不同的解决方案。理想情况下,最好的解决方案是一种散列函数生成器,它可以在 N 个输入的情况下创建。例如对于输入:10, 100, 32, 45, 58, 126, 3, 29, 200, 400, 0 函数应该是: (X ^ 28) % 13
  • @svoltron 这就是我在第一段中的意思:我认为不可能创建一个代码,因为任何输入都会创建一个完美的哈希函数。
  • 是的,它是O(N)。可以通过二分搜索得到O(log N),但元编程就没有那么容易了。
  • 有趣的解决方案;对于 C++14,您可以简单地使用初始化 C 样式数组的技巧 (constexpr auto execute(int i) { using unused = int[]; (void)unused { 0, (execute_if&lt;Is&gt;(i), 0)... }; });也适用于 C++11,但不能作为 constexpr 方法
  • @svoltron - 也许我错了,但是......我不这么认为;或者更好:你可以用递归的方式来做(如果你愿意,我可以尝试写一个例子)但是编译器对模板递归施加了限制,如果N很棒,这可能是个问题。
【解决方案2】:

基于@bolov 的回答,当i 不是常量时,可以通过更改使用任意调度算法:

constexpr auto execute(int i)
{
    (execute_if<Is>(i), ...);
}

收件人:

constexpr auto execute(unsigned i)
{
    (execute_if<Is>(i), ...);
}

然后添加:

constexpr auto execute (int& i)
{
    // Add arbitrary dispatch mechanism here
}

完整的示例,C++11 兼容并在 i 不是常量时使用相当笨重的 std::map(最坏情况复杂度 log n)(我放弃了 constexpr 的东西以使 C++ 中的生活更轻松11):

#include <map>
#include <iostream>

std::map <int, void (*) ()> map;

template <int i> void f ();
template <> void f <1> () { std::cout << "f1\n"; }
template <> void f <2> () { std::cout << "f2\n"; }
template <> void f <3> () { std::cout << "f3\n"; }
template <> void f <4> () { std::cout << "f4\n"; }
template <> void f <5> () { std::cout << "f5\n"; }

template <int ... Is>
struct Dispatcher
{
    template <int first> void execute_if (int i)
    {
        if (first == i)
        {            
            std::cout << "Execute f" << i << " via template\n";
            f <first> ();
        }
    }

    template <int first, int second, int... rest> void execute_if (int i)
    {
        if (first == i)
        {            
            std::cout << "Execute f" << i << " via template\n";
            f <first> ();
        }
        else
            execute_if <second, rest...> (i);
    }

    void execute (unsigned i)
    {
        execute_if <Is...> (i);
    }

    void execute (int& i)
    {
        std::cout << "Execute f" << i << " via map\n";
        map.at (i) ();
    }
};

int main()
{
    map [1] = f <1>;
    map [2] = f <2>;
    map [3] = f <3>;
    map [4] = f <4>;
    map [5] = f <5>;

    Dispatcher <1, 2, 4> dispatcher;  
    dispatcher.execute (2);
    int i = 4;
    dispatcher.execute (i);
}

输出:

Execute f2 via template
f2
Execute f4 via map
f4

Live Demo


编辑:根据 OP 的要求,这是一个使用二分搜索而不是 std::map 的版本。关键是在 Dispatcher 构造函数中构建要搜索的数组。

#include <vector>
#include <iostream>

template <int i> void f ();
template <> void f <1> () { std::cout << "f1\n"; }
template <> void f <2> () { std::cout << "f2\n"; }
template <> void f <3> () { std::cout << "f3\n"; }
template <> void f <4> () { std::cout << "f4\n"; }
template <> void f <5> () { std::cout << "f5\n"; }

using ve = std::pair <int, void (*) ()>;

template <int ... Is>
struct Dispatcher
{
    template <int first> void execute_if (int i)
    {
        if (first == i)
        {            
            std::cout << "Execute f" << i << " via template\n";
            f <first> ();
        }
    }

    template <int first, int second, int... rest> void execute_if (int i)
    {
        if (first == i)
        {            
            std::cout << "Execute f" << i << " via template\n";
            f <first> ();
        }
        else
            execute_if <second, rest...> (i);
    }

    void execute (unsigned i)
    {
        execute_if <Is...> (i);
    }

    void execute (int& i)
    {
        std::cout << "Execute f" << i << " via binary search\n";
        auto lb = lower_bound (indexes.begin (), indexes.end (), ve (i, nullptr), 
            [] (ve p1, ve p2) { return p1.first < p2.first; });    
        if (lb != indexes.end () && lb->first == i)
            lb->second ();
    }

    template <int first> void append_index ()
    {
        indexes.emplace_back (ve (first, f <first>));
    }

    template <int first, int second, int... rest> void append_index ()
    {
        append_index <first> ();
        append_index <second, rest...> ();
    }

    Dispatcher ()
    {
        append_index <Is...> ();
    }

private:
    std::vector <ve> indexes;
};

int main()
{
    Dispatcher <1, 2, 4> dispatcher;  
    dispatcher.execute (2);
    int i = 4;
    dispatcher.execute (i);
}

Live demo

【讨论】:

  • 有趣的选择。也感谢分享。这是一种执行 bolov 提出的“可以改进解决方案以执行二进制搜索,但这并非微不足道”的方法,但不幸的是,正如我在问题中所说,我正在寻找一个解决方案不涉及使用 std::map 或其他“动态”方法(我知道分配器,但仍有一些开销)
  • 在我的答案中添加了二进制搜索版本。
  • 保罗,再次感谢您的反馈。我没有注意到这和 std::map 版本之间的区别。性能应该仍然像前一个那样是 O(log(N)),但它仍然使用动态方法 (std::vector) 而不是 constexpr。然而,对于一些宽松的约束,它是一个非常好的选择
  • 如果在编译时知道i,则解决方案可以是O(1)。如果没有,一些需要查找表,选择你的毒药。很简单。
  • 另一个“毒药”是按照建议使用 log(N) 搜索。但我正在寻找的是查找表和这个 log(N) 解决方案中间的东西
【解决方案3】:

按照 bolov 的解决方案示例,OP 要求提供 C++11 解决方案,保持 constexpres-ness。

好吧...我不认为这是个好主意,因为 C++11 中的 constexpr 函数/成员需要递归并返回一个值。并且编译器对模板递归有严格的限制,如果Nsizeof...(Is))很高,这可能是个问题。

无论如何......我能想象的最好的是以下

template <int... Is>
struct Dispatcher
{
    template <typename = void>
    constexpr int execute_h (int) const
     { /* wrong case; exception? */ return -1; }

    template <int J0, int ... Js>
    constexpr int execute_h (int i) const
     { return J0 == i ? (f<J0>(), 0) : execute_h<Js...>(i); }

    constexpr int execute (int i) const
     { return execute_h<Is...>(i); }
};

可以使用,计算f&lt;&gt;()的编译时间,如下

void test()
{
    constexpr Dispatcher<1,5,100,300> dispatcher;  
    constexpr auto val1 = dispatcher.execute(5);
    constexpr auto val2 = dispatcher.execute(6);

    std::cout << val1 << std::endl; // print 0 (5 is in the list)
    std::cout << val2 << std::endl; // print -1 (6 isn't in the list)
}

f&lt;&gt;() 也必须是 constexpr,并且在 C++11 中,不能返回 void;我用过以下

template <int i>
constexpr int f ()
 { return i; }

【讨论】:

  • 感谢分享,确实在 C++11 中,这对于 O(N) 情况来说确实是一个非常好的解决方案
  • @svoltron - 是的:bolov 的解决方案很有趣,但不幸的是,O(N)。恕我直言,最好使用std::mapstd::unordered_map (可能是常数;可能使用一些模板元编程初始化编译时);但您已明确排除此类解决方案。
  • @svoltron - 我是个白痴:不需要execute_if() 是递归案例:你可以在execute_h() 中做所有事情。简化答案。
  • 不用担心,谢谢您的反馈。在这种情况下,我正在寻找最坏情况的算法,因此 std::unordered_map 被排除在外。一种编译时二叉树确实很有趣
【解决方案4】:

bolov 解决方案的一点改进(恕我直言)

execute_if返回true,当f&lt;I&gt;()被执行时,或者false,否则

template <int I>
constexpr auto execute_if (int i) const
{ return I == i ? f<I>(), true : false; }

而不是使用逗号操作符进行模板折叠

template <int ... Is>
constexpr auto execute(int i) const
 { (execute_if<Is>(i), ...); }

我们可以使用 or (||) 运算符

template <int ... Is>
constexpr auto execute(int i) const
 { (execute_if<Is>(i) || ...); }

使用逗号运算符,execute_is&lt;Is&gt;(i) 将永远被调用 Is,当第一个 Is 等于 i 时也是如此;使用|| 我们有短路,即execute_is&lt;Is&gt;(i) 仅在我们得到一个等于iIs 之前被调用。

【讨论】:

  • 您的代码具有完全相同的生成程序集。这是因为“好像规则”。我的和你的可观察到的行为是相同的,编译器可以正确地分析它并进行相应的优化。我称之为过早优化。对不起,我没有看到这个版本的好处。需要明确的是,我并不是说这很糟糕或类似的东西。只是没有区别。
  • @bolov - 我明白了......我倾向于低估编译器优化的力量:(
【解决方案5】:

理论上(!)您可以使用 C++ 模板创建完美的哈希函数。

  • This question 有关于如何创建完美哈希函数的代码(使用蛮力,所以它只适用于相对较小的集合)。
  • This question 表明 C++ 模板是图灵完备的,因此应该可以将上述代码转换为 C++ 模板。
  • C preprocessor 甚至可以实现,因为它不需要无限循环。

但我认为这样做相当困难。

【讨论】:

    猜你喜欢
    • 2014-09-12
    • 2015-05-22
    • 1970-01-01
    • 2017-02-23
    • 1970-01-01
    • 2014-03-12
    • 2015-09-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多