性能差异:std :: accumulate vs std :: inner_product vs Loop

时间:2018-09-04 13:40:27

标签: c++ stl c++14 profiling language-lawyer

今天,我想分享一下当我尝试执行此简单操作时令人惊讶的事情:

enter image description here

我发现了执行相同操作的不同方法:

  1. 使用std::inner_product
  2. 实现谓词并使用std::accumulate函数。
  3. 使用C风格的循环。

我想通过使用Quick Bench并启用所有优化来执行一些基准测试。

首先,我将两个C ++替代方案与浮点值进行了比较。这是使用std::accumulate使用的代码:

const auto predicate = [](const double previous, const double current) {
    return previous + current * current;
};
const auto result = std::accumulate(input.cbegin(), input.cend(), 0, predicate);

使用std::inner_product功能来比较此代码:

const auto result = std::inner_product(input.cbegin(), input.cend(), input.cbegin(), 1);

在启用所有优化后运行基准测试后,我得到了以下结果:

enter image description here

这两种算法似乎都能达到相同的性能。我确实想进一步尝试C的实现:

double result = 0;
for (auto i = 0; i < input.size(); ++i) {
  result += input[i] * input[i];
}

令人惊讶的是,我发现:

enter image description here

我没想到会有这个结果。我确定出了点问题,所以我检查了GCC的实现:

template<typename _InputIterator1, typename _InputIterator2, typename _Tp>
inline _Tp
inner_product(_InputIterator1 __first1, _InputIterator1 __last1,
      _InputIterator2 __first2, _Tp __init)
{
  // concept requirements
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator1>)
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator2>)
  __glibcxx_requires_valid_range(__first1, __last1);

  for (; __first1 != __last1; ++__first1, (void)++__first2)
__init = __init + (*__first1 * *__first2);
  return __init;
}

我发现它的作用与C实现相同。在回顾了实现之后,我发现了一些奇怪的东西(或者至少我并不期望产生如此大的影响):在所有内部累积中,它都在进行从迭代器value_type到初始值类型的转换。

在我的情况下,我将初始值初始化为0或1,这些值被视为整数,并且在每次累加中,编译器都在进行强制转换。在不同的测试案例中,我的输入数组存储了截断的浮点,因此结果没有改变。

将初始值更新为双精度类型后:

const auto result = std::accumulate(input.cbegin(), input.cend(), 0.0, predicate);

并且:

const auto result = std::inner_product(input.cbegin(), input.cend(), input.cbegin(), 0.0);

我得到了预期的结果:

enter image description here

现在,我知道将初始值保留为与迭代器的基础类型无关的独立类型可能会使函数更灵活并允许执行更多操作。但是

如果我要堆积一个数组的元素,那么我期望得到相同的类型。内部产品也一样。

应该是默认行为吗?

标准为何决定以这种方式执行?

1 个答案:

答案 0 :(得分:2)

  

如果我要堆积数组的元素,那么我期望得到相同的类型。

您的期望是错误的(尽管并不清楚“相同类型的结果”是什么意思),正如您可以从std::accumulate文档中清楚地看到的那样:

template< class InputIt, class T >
T accumulate( InputIt first, InputIt last, T init );

template< class InputIt, class T, class BinaryOperation >
T accumulate( InputIt first, InputIt last, T init,
              BinaryOperation op );

返回类型与用于初始值的类型完全相同。您可以在循环中获得相同的效果:

auto result = 0; // vs auto result = 0.0;
for (auto i = 0; i < input.size(); ++i) {
  result += input[i] * input[i];
}
  

为什么标准决定以这种方式执行它?

通过这种方式,您可以决定要使用哪种类型进行汇总。注意std::accumulate可用于左折,并且T不等于std::iterator_traits<InputIt>::value_type的情况比匹配时的频率要低(可能甚至更多)。