【问题标题】:How to compel a compiler to generate a code equivalent for a manual switch?如何强制编译器为手动切换生成等效代码?
【发布时间】:2018-02-03 03:34:47
【问题描述】:

任务很简单:我们有一个类型列表(即using list = std::tuple<A, B, C, ...>;),我们希望根据运行时已知的索引来调度它的内容。 “调度”是指调用模板化处理程序,由该列表中的类型参数化。 手动写switch很容易:

template <class... Args>
auto dispatch(size_t i, Args&& ...args)
{
  switch (i) {
    case 0: return handler<typename std::tuple_element<0, list>::type>(std::forward<Args>(args)...);
    ...
  }
}

但这很乏味、容易出错并且很无聊。 我正在寻找一种方法来强制编译器通过一些更紧凑和简洁的定义来完成它。

我想达到与手动开关相同或非常接近的结果。因此,生成一个函数指针表(如here)并不是一个理想的解决方案(我不想在处理程序完全可内联的情况下引入函数调用)。

到目前为止,我已经找到了两种方法:

  1. 内联递归(感谢Horstling

    template <size_t N>
    struct dispatch_helper
    {
        template <class... Args>
        static R dispatch(size_t i, Args&& ...args)
        {
            if (N == i)
                return handler<typename std::tuple_element<N, list>::type>(std::forward<Args>(args)...);
            return dispatch_helper<N+1>::dispatch(i, std::forward<Args>(args)...);
        }
    };
    
    template <>
    struct dispatch_helper<200>
    {
        template <class... Args>
        static R dispatch(size_t type, Args&& ...args)
        {
            return {};
        }
    };
    
    template <class... Args>
    auto dispatch_recursion(size_t i, Args&& ...args)
    {
        return dispatch_helper<0>::dispatch(i, std::forward<Args>(args)...);
    }
    
  2. std::initializer_list的用法

    template <class L>
    struct Iterator;
    
    template <template <class...> class L, class... T>
    struct Iterator<L<T...>>
    {
        template <class... Args>
        static R dispatch(size_t i, Args&& ...args)
        {
            R res;
            std::initializer_list<int> l {
                (
                    i == T::value ? (res = handler<typename T::type>(std::forward<Args>(args)...), 0) : 0
                )...
            };
            (void)l;
            return res;
        }
    };
    
    template <class... Args>
    auto dispatch_type_list_iter(size_t i, Args&& ...args)
    {
        return Iterator<index_list<list>>::dispatch(i, std::forward<Args>(args)...);
    }
    

    我在这里省略了一些细节,比如将类型列表转换为索引类型列表或如何处理返回类型,但我希望这个想法很清楚。

我在godbolt 上尝试了这些,Clang 4.0 为第一种方法生成一个“对数”开关(小开关树)和一个代码,与第二种方法的手动开关相同。 我玩了处理程序的大小,可内联或不可内联处理程序,结果看起来很稳定。

但是 GCC 和 ICC 都生成了一个简单的条件序列(对于这两种方法),这非常可悲。

那么,还有其他解决方案吗?尤其是那些至少在 Clang GCC 中工作的人。

【问题讨论】:

  • 我在这里尝试过与您的问题类似的方法:Compiler Explorer。基于 switch 语句或表查找的 Clang 内联调度。 GCC 似乎没有内联它们!我记得还使用表查找实现了一些功能,并看到编译器生成开关,如汇编。我想只要表是 constexpr,编译器通常足够聪明,可以将表查找转换为开关,反之亦然,这取决于他们的内部分析得出什么更合适的结论。所以我相信你问的是不可能的。
  • 我从来没有找到一种生成组件的技术和开关盒一样好。考虑这个例子:godbolt.org/g/aC2zLu。即使在这里,开关也会产生更好的代码!底线:学会爱提升PP。它使使用预处理器变得非常容易,这是一个非常好的库。
  • stackoverflow.com/a/45380228/841108 是对类似问题(类似动机)的相关回答
  • @NirFriedman:Clang 4.0 可以生成与手动切换代码相同的代码。是 GCC (ICC) 让我失望了……
  • @BasileStarynkevitch 这个解决方案怎么比手动切换好??它更加冗长。

标签: c++ c++11 gcc clang c++14


【解决方案1】:

你可以自己做二分查找:

#include <tuple>
#include <iostream>

template <class T>
void function(T arg) {
    std::cout << arg << std::endl;
}

如果你能够使用 c++14 并且可以使用 auto 进行返回类型推导和使用 auto 进行泛型 lambda 表达式,那么你可以走得更远:

#include <tuple>
#include <iostream>
#include <string>

template <class Function, size_t begin, size_t end, class Tuple>
struct  call_on_element;

template <class Function, size_t begin, class Tuple>
struct call_on_element<Function, begin, begin, Tuple> {
    static auto call(Function f, size_t, const Tuple& t) {
        return f(std::get<begin>(t));
    }
};

template <class Function, size_t begin, size_t end, class Tuple>
struct call_on_element {
    static auto call(Function f, size_t i, const Tuple& t) {
        constexpr size_t half = begin + (end - begin) / 2;
        if(i <= half) {
            return call_on_element<Function, begin, half, Tuple>::call(f, i, t);
        } else {
            return call_on_element<Function, half + 1, end, Tuple>::call(f, i, t);
        }
    }
};

template <class Function, class... Args>
auto dispatch(Function f, size_t i, Args&&... args) {
    const auto tuple = std::make_tuple(std::forward<decltype(args)>(args)...);
    return call_on_element<Function, 0, sizeof...(Args) - 1, decltype(tuple)>::call(f, i , tuple);
}


struct Functor {
    template <class T>
    std::string operator()(T arg) {
        std::cout << arg << std::endl;
        return "executed functor";
    }
};

int main() {

    int i = 42;
    char c = 'y';
    float f = 1337;

    std::cout << "using Functor" << std::endl;
    dispatch(Functor(), 0, i, c, f);
    dispatch(Functor(), 1, i, c, f);
    auto s = dispatch(Functor(), 2, i, c, f);

    std::cout << "return value of dispatch = " << s << std::endl;

    std::cout << "using lambda" << std::endl;
    dispatch([](auto arg) { std::cout << arg << std::endl;}, 0, i, c, f);
    dispatch([](auto arg) { std::cout << arg << std::endl;}, 1, i, c, f);
    dispatch([](auto arg) { std::cout << arg << std::endl;}, 2, i, c, f);

};

我想这对于编译器来说更难内联,但使用起来更舒适,也不是不可能。


为了检查内联,我将 c++14 版本修改为:

int main() {
    dispatch(1, 42, 'c', 3.14);
};

// compiled with 'g++ -g -O3 -std=c++14 -S main.cpp' (using gcc7.1.1)
// generated objectdump with objdump -d a.out | c++filt

Objdump 提供以下输出:

00000000000009a0 <main>:
 9a0:   55                      push   %rbp
 9a1:   53                      push   %rbx
 9a2:   48 8d 3d d7 16 20 00    lea    0x2016d7(%rip),%rdi        # 202080 <std::cout@@GLIBCXX_3.4>
 9a9:   ba 01 00 00 00          mov    $0x1,%edx
 9ae:   48 83 ec 18             sub    $0x18,%rsp
 9b2:   48 8d 74 24 07          lea    0x7(%rsp),%rsi
 9b7:   c6 44 24 07 63          movb   $0x63,0x7(%rsp)
 9bc:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
 9c3:   00 00 
 9c5:   48 89 44 24 08          mov    %rax,0x8(%rsp)
 9ca:   31 c0                   xor    %eax,%eax
 9cc:   e8 7f ff ff ff          callq  950 <std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)@plt>
 9d1:   48 89 c5                mov    %rax,%rbp
 9d4:   48 8b 00                mov    (%rax),%rax
 9d7:   48 8b 40 e8             mov    -0x18(%rax),%rax
 9db:   48 8b 9c 05 f0 00 00    mov    0xf0(%rbp,%rax,1),%rbx
 9e2:   00 
 9e3:   48 85 db                test   %rbx,%rbx
 9e6:   74 67                   je     a4f <main+0xaf>
 9e8:   80 7b 38 00             cmpb   $0x0,0x38(%rbx)
 9ec:   74 2d                   je     a1b <main+0x7b>
 9ee:   0f be 73 43             movsbl 0x43(%rbx),%esi
 9f2:   48 89 ef                mov    %rbp,%rdi
 9f5:   e8 86 ff ff ff          callq  980 <std::basic_ostream<char, std::char_traits<char> >::put(char)@plt>
 9fa:   48 89 c7                mov    %rax,%rdi
 9fd:   e8 5e ff ff ff          callq  960 <std::basic_ostream<char, std::char_traits<char> >::flush()@plt>
 a02:   31 c0                   xor    %eax,%eax
 a04:   48 8b 4c 24 08          mov    0x8(%rsp),%rcx
 a09:   64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
 a10:   00 00 
 a12:   75 36                   jne    a4a <main+0xaa>
 a14:   48 83 c4 18             add    $0x18,%rsp
 a18:   5b                      pop    %rbx
 a19:   5d                      pop    %rbp
 a1a:   c3                      retq   
 a1b:   48 89 df                mov    %rbx,%rdi
 a1e:   e8 fd fe ff ff          callq  920 <std::ctype<char>::_M_widen_init() const@plt>
 a23:   48 8b 03                mov    (%rbx),%rax
 a26:   48 8d 0d 73 01 00 00    lea    0x173(%rip),%rcx        # ba0 <std::ctype<char>::do_widen(char) const>
 a2d:   be 0a 00 00 00          mov    $0xa,%esi
 a32:   48 8b 50 30             mov    0x30(%rax),%rdx
 a36:   48 39 ca                cmp    %rcx,%rdx
 a39:   74 b7                   je     9f2 <main+0x52>
 a3b:   be 0a 00 00 00          mov    $0xa,%esi
 a40:   48 89 df                mov    %rbx,%rdi
 a43:   ff d2                   callq  *%rdx
 a45:   0f be f0                movsbl %al,%esi
 a48:   eb a8                   jmp    9f2 <main+0x52>
 a4a:   e8 21 ff ff ff          callq  970 <__stack_chk_fail@plt>
 a4f:   e8 bc fe ff ff          callq  910 <std::__throw_bad_cast()@plt>
 a54:   66 90                   xchg   %ax,%ax
 a56:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 a5d:   00 00 00 

template <size_t begin, size_t end, class Tuple>
struct  call_on_element;

template <size_t begin, class Tuple>
struct call_on_element<begin, begin, Tuple> {
    static void call(size_t, const Tuple& t) {
        function(std::get<begin>(t));
    }
};

template <size_t begin, size_t end, class Tuple>
struct call_on_element {
    static void call(size_t i, const Tuple& t) {
        constexpr size_t half = begin + (end - begin) / 2;
        if(i <= half) {
            call_on_element<begin, half, Tuple>::call(i, t);
        } else {
            call_on_element<half+1, end, Tuple>::call(i, t);
        }
    }
};

template <class... Args>
void dispatch(size_t i, Args&&... args) {
    const auto tuple = std::make_tuple(std::forward<decltype(args)>(args)...);
    call_on_element<0, sizeof...(Args)-1, decltype(tuple)>::call(i , tuple);
}

int main() {

    int i = 42;
    char c = 'y';
    float f = 1337;

    dispatch(0, i, c, f);
    dispatch(1, i, c, f);
    dispatch(2, i, c, f);

};

callq std::basic_ostream&lt;...&gt;je ... 的调用让我相信,它实际上是内联函数并生成对数条件树。

【讨论】:

  • 我用你的变种更新了 Godbolt 的测试。不幸的是,godbolt 无法生成短 url。但要点是 - Clang 4.0 为列表中的每 50 个元素发出一个单独的代码块,而 GCC 7.2 内联所有元素。不幸的是,这两个编译器都会产生很多条件,而不是像 switch 那样的表跳转。
  • 我以为你不想跳桌。表跳转更容易实现:D
  • 我不想要一个指针表——一个包含指向处理函数的指针的表。手动切换转换为跳转表,无需任何函数调用。请参阅我原帖中godbolt的链接,有几种实现方式,包括手动切换。
【解决方案2】:

为了保持简单的声明,我是Boost.Preprocessor 的忠实粉丝。我同意这些是宏,您可能会鄙视它们,但是当它们实际上提高代码的可读性和维护性时,为什么不使用它们呢?

#include <boost/preprocessor.hpp>

#include <iostream>
//the following two #include are only for handler() code
#include <typeinfo>
#include <tuple>

template <class T>
void handler(int x) {
  std::cout << typeid(T).name() << ' ' << x << '\n';
}

#define TYPE_LIST (int)(char)(bool) //just declare your types here

using list = std::tuple<BOOST_PP_SEQ_ENUM(TYPE_LIST)>;

template <class... Args>
auto dispatch(size_t i, Args&& ...args)
{
  switch (i) {
#define TYPE_case(r, data, i, type) case i: return handler<type>(std::forward<Args>(args)...);
    BOOST_PP_SEQ_FOR_EACH_I(TYPE_case,, TYPE_LIST);
#undef TYPE_case
  }
}

int main(int, char**) {
  for (int i = 0; i < std::tuple_size<list>::value; ++i) {
    dispatch(i, i);
  }
}

【讨论】:

  • 好吧,宏不适合grepack(或事件ctags)等外部工具,而且更容易出错。而且它们更难调试和理解。与宏相比,我更喜欢外部代码生成器,但 C++ 模板机制更好(无需外部工具、类型安全、更易于调试)。
  • 我同意这对于外部工具来说并不理想。我也更喜欢模板并且只使用预处理器作为最后的手段,但在我看来,必须保持多个声明同步(甚至更多跨多个文件)是绝对最糟糕的。到目前为止,我的工具链中还没有代码生成器,因此使用 Boost.Preprocessor 对构建过程没有影响。
【解决方案3】:

你有多鄙视宏?

template <class... Args>
auto dispatch(size_t i, Args&& ...args)
{
    #define CASE(N) case N: return handler<typename std::tuple_element<N, list>::type>(std::forward<Args>(args)...)
    switch (i) {
        CASE(0);
        CASE(1);
        CASE(2);
    }
    #undef CASE
}

【讨论】:

  • 嗯,这只是一个手动开关,打字少了一点。主要问题是当你改变你的类型列表时,你必须改变一个开关。我的目标是一个单一的规范点(只是一个类型列表应该包含其处理的所有信息)。
【解决方案4】:

这是获取通用跳转表的方法。如果它被多次使用,最好将JumpTable 存储在一个类中,然后将它分派到一个仿函数中。

#include <tuple>
#include <vector>
#include <iostream>
#include <utility>
#include <experimental/array>

template <size_t id, class Tuple, class Function>
auto functionWrapper() {
    return [](const Tuple& tuple, Function f) {
        f(std::get<id>(tuple));
    };
}

template <class Tuple, class Function, size_t... id>
auto arrayWrapper(std::index_sequence<id...>) {

    return std::experimental::make_array(
        functionWrapper<id, Tuple, Function>()...
    );

}

template <class Function, class... Args>
void dispatch(Function f, size_t i, Args&&... args) {
    using TupleType = std::tuple<Args...>;

    const auto JumpTable = arrayWrapper<TupleType, Function>(std::make_index_sequence<sizeof...(Args)>());


    JumpTable[i](std::make_tuple(args...), f);
}

int main() {
    for(int i = 0; i < 3; i++) {
        dispatch(
            [](auto arg){ std::cout << arg << std::endl; },
            i, // index
            42, 'c', 3.14 // args...
        );
    }
};

【讨论】:

  • 您查看了程序集并确认所有 std:: 函数 crud 都被消除了?
  • 我查看了 asm,但没有完全理解它以确保确定。我在函数部分没有找到任何std::function 构造函数和operator(),但是主函数太长了,我不明白发生了什么。但我不认为,这很重要。要么使用第一个方法和 O(n*log n) 进行内联,要么使用函数表添加间接并获得 O(1)。使用这两种方法对代码进行基准测试将得到更可靠的结果,而不是试图解释 asm 代码。我的猜测是,n &lt; 10 内联会更快,但谁知道呢。
  • 我实际上认为这段代码甚至都不正确,您将元组参数列表与控制调用哪个调度程序的类型列表混为一谈。您根本没有提到 list 或任何类型列表,只需将参数转换为元组即可。分配器的参数与索引不同分配器的类型列表不同。您所说的 perf 可能是正确的,但到目前为止介绍的任何方法都不太可能像 switch case 一样快(如果处理程序很小)。
  • 顺序无关紧要,只是你所做的和原来的问题没有关系。最初的问题是打电话给handler&lt;get&lt;i, list&gt;&gt;(args...)。这不是这里发生的事情。你基本上是在打电话给handler(get&lt;i, tie(args...)&gt;)
  • 其实我的目标是打电话给handler&lt;typename std::tuple_element&lt;I, type_list&gt;::type&gt;(args...)。无需具体化类型列表,它只存在于编译时。但是可以针对该问题量身定制带有指针表的解决方案。只是我希望避免任何函数调用,我不相信这个解决方案允许。
猜你喜欢
  • 2014-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多