【问题标题】:Difference in performance: std::accumulate vs std::inner_product vs Loop性能差异:std::accumulate vs std::inner_product vs Loop
【发布时间】:2019-02-09 13:40:52
【问题描述】:

今天,我想分享一些在尝试实现这个简单操作时让我大吃一惊的事情:

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

  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);

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

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

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

令人惊讶的是,我发现:

我没想到会有这样的结果。我确定有什么问题,所以我检查了 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,这些值被认为是整数,并且在每次累积中,编译器都在进行强制转换。在不同的测试用例中,我的输入数组存储了截断的浮点数,所以结果没有改变。

将初始值更新为double类型后:

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);

我得到了预期的结果:

现在,我知道将初始值保留为独立于迭代器底层类型的类型可能会使函数更加灵活并允许做更多事情。但是,

如果我正在累积数组的元素,我希望得到相同的类型作为结果。内积也一样。

应该是默认行为吗?

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

【问题讨论】:

  • 您回答了 为什么标准决定以这种方式执行它?现在,我明白将初始值保留为独立于底层的类型迭代器的类型可以使函数更灵活,并允许做更多的事情。。使其具有通用性允许您执行诸如使用 int64_t 作为 int32_t 容器的累加器之类的操作,从而减少溢出的机会。
  • 在 C++ 中,即使只对两个值求和,也可以获得不同的类型:decltype(char(0)+char(0))int
  • 请注意,这个问题所指的“转换”实际上是“转换”。 cast 是您在源代码中编写的内容,用于告诉编译器进行 conversion
  • 明确一点,x(i)^2 的意思是 i^2,对吧?
  • 哦,就像 x-subscript-i 而不是 x-applied-to-i。这是数学和编程符号的陌生组合。

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


【解决方案1】:

如果我正在累积数组的元素,我希望得到相同的类型作为结果。

您的期望是错误的(尽管不太清楚“与结果相同的类型”是什么意思),您可以从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&lt;InputIt&gt;::value_type 的情况,而不是比它们匹配时更少(可能甚至更多)。

【讨论】:

    猜你喜欢
    • 2021-11-10
    • 2020-07-29
    • 2015-10-13
    • 2020-05-29
    • 2013-08-18
    • 2020-01-03
    • 2015-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多