使用带有可变参数函数模板的参数的std :: algorithms的大多数cpu效率方法?

时间:2014-02-18 02:23:59

标签: c++ c++11 variadic-templates

假设您有一个可变参数函数模板,该模板采用仿函数和一系列同类型,并且您希望使用std::accumulate来折叠序列,如下所示:

template<typename BinaryFuncType, typename... ArgTypes>
do_something(const BinaryFuncType& f, const ArgTypes&... objects)
{
    // ...
    // use std::accumulate to fold 'objects' using 'f'
    // ...
}

是否可以将可变参数(objects)直接传递给范围算法(std::accumulate ,即而不会产生复制成本的成本对可迭代容器的对象(或引用)?

3 个答案:

答案 0 :(得分:5)

您可以创建std::reference_wrapper个对象的容器并在其上累积。这是一个例子:

#include <iostream>
#include <functional>
#include <algorithm>
#include <array>
using namespace std;
template<typename... ArgTypes>
int sum(const ArgTypes&... numbers) {
    array<reference_wrapper<const int>, sizeof...(numbers)> A = {ref(numbers)...};
    return accumulate(A.begin(), A.end(), 0);
}
int main() {
    int x = 1, y = 2, z = 3;
    cout << sum(x, y, z) << endl; // prints 6
}

我会留给你弄清楚如何根据你的问题调整这个更一般的设置。

答案 1 :(得分:4)

显然是,但是以扭曲的方式。请考虑以下代码:

#include <algorithm>
#include <array>
#include <cstdio>
#include <iterator>

template<typename... Ts>
int sum(Ts... numbers) {
    std::array<int,sizeof...(numbers)> list{{numbers...}};
    return std::accumulate(std::begin(list), std::end(list), 0);
}

__attribute__((noinline))
void f(int x, int y, int z) {
  std::printf("sum = %d\n", sum(x, y, z));
}

int main(int argc, char* argv[]) {
  int x = std::atoi(argv[1]);
  int y = std::atoi(argv[2]);
  int z = std::atoi(argv[3]);    
  f(x, y, z);
}

我查看了生成的汇编代码。以下是sum()被clang优化的内容,为了清楚起见,我将汇编代码重写到C:

int sum(int x, int y, int z) {
  int tmp = x;
  tmp += y;
  tmp += z;
  return tmp;
}

我可以说生成的汇编代码是最优的!它摆脱了临时std::array并在std::accumulate()中展开循环。

所以你的问题的答案:即使你创建一个临时的可迭代容器,它也可以优化如果编译器足够智能你的数字类型很简单足够的(内置类型或POD)。您不会为创建临时容器或将元素复制到临时容器中付出代价如果可以将其优化掉。


可悲的是,gcc 4.7.2并不那么灵巧:

int sum(int x, int y, int z) {
  int a[3];
  a[0] = x;
  a[1] = y;
  a[2] = z;
  int tmp = x;
  tmp += y;
  tmp += z;
  return tmp;
}

不幸的是,它没有意识到它可以摆脱临时数组。我将使用来自trunk的最新gcc进行检查,如果问题仍然存在,请提交错误报告; 它似乎是优化器中的一个错误。

答案 2 :(得分:2)

一旦您的模板被实例化,编译器就会看到许多不同的参数。每个参数甚至可以是不同的类型。

就像你想迭代fun (a, b, c, d)的参数一样,并期望代码优化器能够处理层混淆的层。

你可以选择一个递归模板,但那会像低效一样神秘。

你可以设计一个无模板的可变参数函数,但是你必须使用<cstdarg>接口并且可以吻std::accumulate再见。

可能你可以使用可变参数作为普通旧数组的初始值并在其上使用std::accumulate,前提是你将闪亮的新玩具限制为可能的可内联参数,即可以列出的对象列表在编译时转换为单个基类型。

如果你有大而昂贵的对象,这个方法仍然可以用于对所述对象的const引用。我想你会花费相当多的时间来优化累积计算中涉及的运算符,如果你想从中挤出表演,那么,任何东西都可以用足够的血汗来做。

#include <array>
#include <numeric>
#include <type_traits>

using namespace std;


// Since we need to get back the base type, might as well check that the
// "optimized" code is not fed with junk that would require countless implicit
// conversions and prevent the compiler from inlining the stupid dummy function
// that should just act as a wrapper for the underlying array initialization.
template<class T, class...>
struct same_type
{
    static const bool value = true;
    typedef T type;
};

template<class Ta, class Tb, class... Types>
struct same_type<Ta, Tb, Types...>
{
    static const bool value = is_same<Ta,Tb>::value && same_type<Tb, Types...>::value;
    typedef Ta type;
};

// --------------------------------------------------------
// dummy function just here to make a copy of its arguments
// and pass it to std::accumulate
// --------------------------------------------------------
template<typename F, typename...Args> 
typename same_type<Args...>::type do_something(F fun, Args...args)
{
    // just a slight bit less of obfuscation
    using base_type = same_type<Args...>::type;

    // make sure all arguments have the same type
    static_assert(same_type<Args...>::value, "all do_something arguments must have the same type");

    // arguments as array
    array<base_type, sizeof...(Args)> values = { args... };
    return accumulate (values.begin(), values.end(), (base_type)0, fun);
}

// --------------------------------------------------------
// yet another glorious functor
// --------------------------------------------------------
struct accumulator {
    template<class T>
    T operator() (T res, T val)
    {
        return res + val;
    }
};

// --------------------------------------------------------
// C++11 in its full glory
// --------------------------------------------------------
int main(void)
{
    int    some_junk = do_something(accumulator(),1,2,3,4,6,6,7,8,9,10,11,12,13,14,15,16);
    double more_junk = do_something(accumulator(),1.0,2.0,3.0,4.0,6.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
    return some_junk+(int)more_junk;
}

我看了一下最新的Microsoft编译器生成的muck。 它完全内联了double版本。大多数代码都忙于初始化数组,其余的是六个指令循环。请注意,循环也未展开。

完全内联int版本。它删除了虚函数调用,但生成了累积模板的实例。

毫不奇怪,如果算法认为传递的参数的数量和大小不合理,编译器就不会费心优化函数,因为它无法知道这段代码会被称为几十亿次每秒钟都是一个愚蠢的设计。

你可以通过寄存器和内联指令和编译指示以及调整编译器优化选项来获得很多乐趣,但它是一个死胡同,恕我直言。

如果你问我,那就是利用所谓的前沿技术将岩石粘在一起的糟糕设计的完美例子。