Почему Range-v3 медленнее, чем STL в этом примере?
Я играю в библиотеке Range-v3, чтобы выполнить прославленный find_if
, и мне было любопытно, почему в Google-критерие последовательно мой код 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-сканера (не уверен, что правильно)
#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
Ответы
Ответ 1
view::partial_sum
в диапазоне int
выдает диапазон int
. Если to_find > INT_MAX
, внутренний накопитель переполнится, что приведет к UB. На практике алгоритм, скорее всего, просматривает весь вход и возвращает конечный итератор.
И наоборот, ваш accumulated_length
в подходе без диапазона v3 - это long
. Он не переполняется и, таким образом, определил поведение/возврат перед обработкой всего ввода.
Подход диапазона-v3 будет иметь правильное поведение, если вы transform
диапазон ввода в диапазон long
, прежде чем передать его через partial_sum
:
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
, идентичен для обоих случаев. Существуют заметные различия в промежуточном разделе между инициализацией elapsed1
и elapsed2
. gcc делает намного лучшую работу по оптимизации версии std: горячий цикл объединяется в один код с ветвями для условий завершения. Версия range-v3 уродливее и прыгает вокруг совсем немного; Я предполагаю, что нам нужно отобразить детали реализации partial_sum
, чтобы сделать его более прозрачным для оптимизаторов.