【问题标题】:Why is Range-v3 slower than the STL in this example?在这个例子中,为什么 Range-v3 比 STL 慢?
【发布时间】:2017-06-07 13:02:44
【问题描述】:

我正在使用 Range-v3 库来执行美化的 find_if 并且很好奇为什么 google-benchmark 始终将我的 Range-v3 代码排名比我的 std::find_if 方法更差。 g++ 和 clang 都给出了与-O3#define NDEBUG 相同的模式。

我想到的具体示例是以下使用 STL:

std::vector<int> lengths(large_number, random_number);

auto const to_find = std::accumulate(lengths.begin(), lengths.end(), 0l) / 2;
auto accumulated_length = 0l;
auto found = std::find_if(lengths.begin(), lengths.end(), [&](auto const &val) {
                              accumulated_length += val;
                              return to_find < accumulated_length;
                          });
auto found_index = std::distance(lengths.begin(), found);    

出于本说明的目的,这有些人为设计,但通常会有一个随机生成器用于to_find 变量和lengths 向量中的随机值。

使用 Range-v3 库,我得到以下代码

using namespace ranges;    

std::vector<int> lengths(large_number, random_number);

auto const to_find = accumulate(lengths, 0l) / 2;

auto found_index = distance(lengths | view::partial_sum()
                                    | view::take_while([=](auto const i) {
                                         return i < to_find;
                                      }));

我的问题是为什么 Range-v3 比 STL 实现慢。我知道这仍然是一个实验性库,但代码示例可能有问题还是我滥用了范围概念?

编辑

一个示例 google-bench 驱动程序(不确定是否正确)

#define NDEBUG

#include <numeric>
#include <vector>

#include <benchmark/benchmark.h>

#include <range/v3/all.hpp>

static void stl_search(benchmark::State &state) {

    using namespace ranges;

    std::vector<long> lengths(state.range(0), 1l);

    auto const to_find = std::accumulate(lengths.begin(), lengths.end(), 0l) / 2;

    while (state.KeepRunning()) {

        auto accumulated_length = 0l;
        auto const found = std::find_if(lengths.begin(), lengths.end(), [&](auto const& val) {
                               accumulated_length += val;
                               return to_find < accumulated_length;
                           });
        volatile long val = std::distance(lengths.begin(), found);
    }
    state.SetBytesProcessed(int64_t(state.iterations()) *
                            int64_t(state.range(0)) * sizeof(long));
}

static void ranges_search(benchmark::State &state) {

    using namespace ranges;

    std::vector<long> lengths(state.range(0), 1l);

    auto const to_find = accumulate(lengths, 0l) / 2;

    while (state.KeepRunning())
    {
        volatile long val = distance(lengths | view::partial_sum()
                                             | view::take_while([=](auto const& i) {
                                                   return i <= to_find;
                                               }));
    }
    state.SetBytesProcessed(int64_t(state.iterations()) *
                            int64_t(state.range(0)) * sizeof(long));
}

BENCHMARK(ranges_search)->Range(8 << 8, 8 << 16);
BENCHMARK(stl_search)->Range(8 << 8, 8 << 16);

BENCHMARK_MAIN();

给予

ranges_search/2048          756 ns        756 ns     902091   20.1892GB/s
ranges_search/4096         1495 ns       1494 ns     466681   20.4285GB/s
ranges_search/32768       11872 ns      11863 ns      58902   20.5801GB/s
ranges_search/262144      94982 ns      94892 ns       7364   20.5825GB/s
ranges_search/524288     189870 ns     189691 ns       3688   20.5927GB/s
stl_search/2048             348 ns        348 ns    2000964   43.8336GB/s
stl_search/4096             690 ns        689 ns    1008295   44.2751GB/s
stl_search/32768           5497 ns       5492 ns     126097    44.452GB/s
stl_search/262144         44725 ns      44681 ns      15882   43.7122GB/s
stl_search/524288         91027 ns      90936 ns       7616   42.9563GB/s

使用 clang 4.0.1 和

ranges_search/2048         2309 ns       2307 ns     298507   6.61496GB/s
ranges_search/4096         4558 ns       4554 ns     154520   6.70161GB/s
ranges_search/32768       36482 ns      36454 ns      19191   6.69726GB/s
ranges_search/262144     287072 ns     286801 ns       2438   6.81004GB/s
ranges_search/524288     574230 ns     573665 ns       1209   6.80928GB/s
stl_search/2048             299 ns        298 ns    2340691   51.1437GB/s
stl_search/4096             592 ns        591 ns    1176783   51.6363GB/s
stl_search/32768           4692 ns       4689 ns     149460   52.0711GB/s
stl_search/262144         37718 ns      37679 ns      18611   51.8358GB/s
stl_search/524288         75247 ns      75173 ns       9244   51.9633GB/s

使用 gcc 6.3.1。我的机器有一个 Haswell 代处理器。两者都是用

编译和执行的
g++ -Wall -O3 -std=c++14 Ranges.cpp -lbenchmark -lpthread && ./a.out
clang++ -Wall -O3 -std=c++14 Ranges.cpp -lbenchmark -lpthread && ./a.out

【问题讨论】:

  • 请提供minimal reproducible example、具体的性能测量结果、编译器和系统信息。
  • 只是一个观察:通过查看许多范围代码,它非常强大,但是所有 lambdas 返回 lambdas 返回 lambdas 等,它似乎并没有针对性能进行优化,而不是语法的易用性.
  • @AndyG 为什么会出现性能问题,lambdas 返回 lambdas? Lambda 不是外部链接的函数:它们不必“物理地”存在于可执行文件中。它们可以很容易地在模板代码中被省略,因为 Range-v3 是
  • @KABoissonneault:这是一个公平的观点。我想看看实际上省略了什么等等。另一方面,我记得 Eric Neibler 在 CPPCon 的演讲中谈到,由于缺乏语言支持,范围库倾向于生成比必要更多的副本。

标签: c++ performance range-v3


【解决方案1】:

view::partial_sumint 的范围内产生int 的范围。如果to_find &gt; INT_MAX,内部累加器将溢出导致UB。在实践中,该算法很可能遍历整个输入并返回结束迭代器。

相反,您在非范围 v3 方法中的 accumulated_lengthlong。它不会溢出,因此在处理整个输入之前已经定义了行为/返回。

如果您在通过partial_sum 之前将输入范围transform 输入到long 的范围内,则range-v3 方法将具有正确的行为:

auto found_index = distance(lengths
  | view::transform(convert_to<long>{}) | view::partial_sum()
  | view::take_while([=](auto const i) { return i < to_find; }));

即使有了这个正确性修复,它仍然比在我的测试中使用标准算法慢得多。编译这个测试程序:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

#ifdef USE_RV3
#include <range/v3/core.hpp>
#include <range/v3/algorithm.hpp>
#include <range/v3/numeric.hpp>
#include <range/v3/view.hpp>

#else
#include <algorithm>
#include <numeric>
#endif

int main() {
    constexpr size_t large_number = 1UL << 30;

    int random_number = 42;

    std::vector<int> const lengths(large_number, random_number);

    using clock_t = std::chrono::steady_clock;
    auto const start = clock_t::now();

#ifdef USE_RV3
    auto const to_find = ranges::accumulate(lengths, 0l) / 2;
#else
    auto const to_find = std::accumulate(lengths.begin(), lengths.end(), 0l) / 2;
#endif

    auto const elapsed1 = clock_t::now() - start;

#ifdef USE_RV3
    auto const found_index = ranges::distance(lengths
            | ranges::view::transform(ranges::convert_to<long>{})
            | ranges::view::partial_sum()
            | ranges::view::take_while([=](auto const i) { return !(to_find < i); }));
#else
    auto accumulated_length = 0l;
    auto found = std::find_if(lengths.begin(), lengths.end(), [&](auto const &val) {
                                accumulated_length += val;
                                return to_find < accumulated_length;
                            });
    auto const found_index = std::distance(lengths.begin(), found);
#endif

    auto const elapsed2 = clock_t::now() - start;

    std::cout << "elapsed1: "
        << std::chrono::duration<double, std::milli>(elapsed1).count()
        << " ms, to_find: " << to_find << "\n"
           "elapsed2: "
        << std::chrono::duration<double, std::milli>(elapsed2).count()
        << " ms, result: " << found_index << '\n';
}

g++-6 -std=c++14 -Ofast -march=native -DNDEBUG rv3.cpp -I ~/dev/range-v3/include -S -o -

不带和带-DUSE_RV3 以及区分程序集输出都很有趣。通过 elapsed1 的初始化生成的代码对于这两种情况都是相同的。 elapsed1elapsed2 的初始化之间的中间部分存在显着差异。 gcc 在优化 std 版本方面做得更好:热循环都在一个单独的代码中运行,并带有用于终止条件的分支。 range-v3 版本更丑,而且跳动很多;我推测我们需要调整partial_sum 的实现细节,以使其对优化器更加透明。

【讨论】:

  • 感谢您的好评。我已经遇到了这个溢出问题并在我的 google-bench 驱动程序中修复了...ranges::view::transform(ranges::convert_to{}) 中的代码是否只是将每个整数即时转换为 long输入?
  • @user8125498 是的。 convert_to&lt;T&gt; 是一个函数对象,它简单地将其参数 static_casted 返回到 T
  • 如果这是一个实现细节,我是否需要在 GitHub 上打开一个错误?
  • @user8125498 是的,请报告错误。我在github.com/ericniebler/range-v3/compare/… 进行了更好的返工,但问题报告将帮助我记得回来完成它;)
  • 你试过提高clang的内联阈值吗?
猜你喜欢
  • 2014-11-23
  • 1970-01-01
  • 1970-01-01
  • 2014-08-29
  • 2021-12-09
  • 2021-05-31
  • 2016-11-24
  • 1970-01-01
相关资源
最近更新 更多