编译时生成的函数分派器,开销最小

时间:2018-11-14 10:19:29

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

我正在尝试使用编译时生成的数组实现快速函数分派器,以便能够在运行时在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)。 / p>

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

请询问是否有不清楚的地方。

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

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

5 个答案:

答案 0 :(得分:7)

好吧,我不知道您是否能够做自己想做的事。在我看来,编写一个为任何输入创建完美哈希函数的代码对我来说似乎很……无法实现。

无论如何,这是编写代码的简单解决方案。它是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

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

答案 1 :(得分:4)

基于@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兼容,并且在std::map不是常数时使用了笨拙的i(最坏情况下的复杂度日志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

答案 2 :(得分:2)

OP要求按照Bolov的解决方案示例使用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<>()的编译时间,如下所示

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<>()也必须为constexpr,在C ++ 11中,不能返回void;我已经使用了以下

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

答案 3 :(得分:2)

bolov的解决方案有一点改进(IMHO)

在执行execute_if时写true返回f<I>(),否则返回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<Is>(i)永远被调用Is,当第一个Is等于i时也是如此;使用||会造成短路,即execute_is<Is>(i)仅在得到等于Is的{​​{1}}之前被调用。

答案 4 :(得分:0)

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

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

但是我认为这样做很难。