I find that occasionally in my code, I'll have a data structure and I want to get two or more values out of it, each of which can be extracted using a standard algorithm. The problem is, to use the standard algorithms means doing multiple loops through the data. Take the following example, where I have a vector<int> and want to get the sum, value of the first element above some threshold, and the total number of elements above that threshold:
constexpr auto GetValuesSTL(const std::vector<int>& testdata)
{
constexpr auto value_above_threshold = [](const auto value) constexpr { return value > THRESHOLD; };
constexpr auto optional_from_iterator = [](const auto it, const auto end) constexpr { return it != end ? std::make_optional(*it) : std::nullopt; };
return std::make_tuple(
std::accumulate(testdata.begin(), testdata.end(), 0L),
optional_from_iterator(std::find_if(testdata.begin(), testdata.end(), value_above_threshold), testdata.end()),
std::count_if(testdata.begin(), testdata.end(), value_above_threshold) );
}
but I can write this more efficiently as a raw loop:
constexpr auto GetValuesRawLoop(const std::vector<int>& testdata)
{
auto sum = 0L;
std::optional<int> first_above_threshold = std::nullopt;
auto num_above_threshold = 0;
auto it = testdata.begin();
for (; it != testdata.end(); ++it)
{
const auto value = *it;
if (value > THRESHOLD)
{
first_above_threshold = value;
break;
}
sum += value;
}
for (; it != testdata.end(); ++it)
{
const auto value = *it;
sum += value;
if (value > THRESHOLD)
{
++num_above_threshold;
}
}
return std::make_tuple( sum, first_above_threshold, num_above_threshold );
}
I was hoping that the compiler would be able to fuse the algorithm calls into a single loop, since it has enough info to know that the vector isn't being modified, but profiling these two functions on various length vectors of randomly generated ints (compiled with g++-9 -O3) shows that the STL version of the function consistently takes about 2-2.5x times as long as the raw loop, exactly as you'd expect without loop fusion.
Is there a good reason the compiler can't/doesn't apply this optimisation? Is there some sort of assumption required to be able to fuse the loops that the compiler isn't allowed to make? Or is it a fundamentally difficult thing to detect and apply? Is there an alternative way to write it that could be as efficient as the raw loop and as expressive as the algorithm version?