在计算图中避免虚函数调用

时间:2017-02-27 13:58:41

标签: c++ performance virtual-functions numerical-computing

我使用DAG(有向无环图)来表示和评估表达式;每个节点代表一个操作(+-/*,累积等...)并通过顺序评估每个节点来实现对整个表达式的评估按拓扑排序顺序。每个节点都继承基类RefNode,并根据它所代表的运算符实现虚拟函数 evaluate 。 Node类在代表运算符的仿函数上进行模板化。节点评估顺序保留在vector<RefNode*>中,并对每个元素进行->evaluate()次调用。

一些快速分析显示,虚拟evaluate会使加法节点减慢2倍[1]因子,无论是从开销还是破坏分支预测。

作为第一步,将类型信息编码为整数,并相应地使用static_cast。这确实有所帮助,但它的笨重,我宁愿不在我的代码的热门部分跳来跳去。

struct RefNode {
    double output;
    inline virtual void evaluate(){}
};

template<class T>
struct Node : RefNode {
    double* inputs[NODE_INPUT_BUFFER_LENGTH];
    T evaluator;
    inline void evaluate(){ evaluator(inputs, output); }
};

struct Add {
    inline void operator()(double** inputs, double &output)
    {
        output=*inputs[0]+*inputs[1];
    }
};

评估可能如下:

Node<Add>* node_1 = ...
Node<Add>* node_2 = ...
std::vector<RefNode*> eval_vector;

eval_vector.push_back(node_1);
eval_vector.push_back(node_2);

for (auto&& n : eval_vector) {
    n->evaluate();
}

我有以下问题,请注意性能至关重要:

  1. 在这种情况下如何避免虚函数?
  2. 如果没有,我如何改变表示表达式图形的方式以支持多个操作,其中一些操作必须保持状态,并避免虚函数调用。
  3. Tensorflow / Theano等其他框架如何代表计算图?
  4. [1]我的系统上的单个加法操作需要大约2.3ns的虚函数和1.1ns没有。虽然这很小,但整个计算图主要是加法节点,因此有很长一段时间可以保存。

1 个答案:

答案 0 :(得分:0)

如评论中所述,您需要在编译时知道图表以删除虚拟调度。为此,您只需使用std::tuple

auto eval_vector = std::make_tuple(
    Node<Add>{ ... },
    Node<Add>{ ... },
    ...
);

然后,您只需删除virtualoverride关键字,并删除基类中的空函数。

你会发现基于for循环的范围还不支持元组。要迭代它,您将需要该功能:

template<typename T, typename F, std::size_t... S>
void for_tuple(std::index_sequence<S...>, T&& tuple, F&& function) {
    int unpack[] = {(static_cast<void>(
        function(std::get<S>(std::forward<T>(tuple))
    ), 0)..., 0};
    static_cast<void>(unpack);
}

template<typename T, typename F>
void for_tuple(T&& tuple, F&& function) {
    constexpr std::size_t N = std::tuple_size<std::remove_reference_t<T>>::value;
    for_tuple(std::make_index_sequence<N>{}, std::forward<T>(tuple), std::forward<F>(function));
}

然后你可以像这样迭代你的元组:

for_tuple(eval_vector, [](auto&& node){
    node.evaluate();
});