I am tasked with investigating performance improvement of some legacy C++ code, by leveraging CPP20. One potential area of improvement I see is this pattern in the hot path.
std::vector<engagementIndex> getEngagementsForDay(DayIndex day) {
std::vector<engagementIndex> outputVector;
for(EngagementIndex engagement : engagements){
if(getDay(engagement) == day) {
outputVector.pushback(engagement);
}
}
return outputVector;
}
These are small vectors (less than 10 values typically) and the index it stores is just a short (not storing a whole object). Still this function gets called a few hundred thousand times, and I suspect that avoiding the vector memory allocation should help.
I want to replace the return type with a view instead.
Question #1: What is the return type for the appropriate view?
All the examples and discussions seem to utilize auto. At my tiny shop, auto is discouraged but if that indeed is the best option we can probably live with that.
But the bigger problem is that I can't even get my code to compile with auto.
Basically depending on some runtime conditions, I need to be able to return 4 "kinds" of views from the function.
An empty view
return std::ranges::empty_view<EngagementIndex>();
A whole vector
return engagements | std::views::all;
Part of a vector
return engagements | std::views::take(size);
Vector filtered by some condition
return engagements | std::views::filter(engagementDayFilter(day));
Each of my return statements produces a different type, preventing me from putting them in the same function.
What are my options?
I don't have any experience with CPP20, but google hasn't been too helpful.
EDIT: 10/1/24
I ended up going with an approach similar to one of the answers, but instead creating a custom single filter for each of my use cases. Effectively I am giving up on the ability to pipe filters. I am not qualified enough to say if this is better or worse, but I found it slightly easier to read.
return engagements | std::views::filter(myFilter1(day));
But what I really wanted to share is that going from a vector copy to a view had a meaningful impact. My two target functions now are no longer on the hot path. I don't have a good way to measure the exact performance impact (our test cases don't naturally lend themselves), but I am fairly confident that the improvement is greater than 1% and less than 5%
Each one of those views can have a different size, you cannot contain them all in some type-erased object without heap allocation, which is what you need in order to return it from a function. you can use a custom allocator or pmr::vector to reduce heap allocations.
Since you want to remove heap allocations to speed up performance then your best option is to use a std::variant of them all, you can use std::declval to help you declare the variant type.
using empty_engagement_view = std::ranges::empty_view<engagementIndex>;
using all_engagement_view = decltype(std::declval<std::vector<engagementIndex>&>() | std::views::all);
using partial_view = decltype(std::declval<std::vector<engagementIndex>&>() | std::views::take(std::declval<size_t>()));
using filtered_view = decltype(std::declval<std::vector<engagementIndex>&>() | std::views::filter(engagementDayFilter{std::declval<int>()}));
using engagement_view_variant = std::variant<
empty_engagement_view,
all_engagement_view,
partial_view,
filtered_view
>;
there is a distinction between passing an rvalue and an lvalue to the view, the above ones only store lvalues, which means the vector must stay alive unit this view is destructed.
iterating over it is a bit tricky, you need to use std::for_each, this helper struct can help.
struct engagementView
{
engagement_view_variant var;
template <typename Func>
void for_each(Func&& f)
{
std::visit([&](auto& view){
std::for_each(view.begin(), view.end(), f);
}, var);
}
};
then you can just use it
int main()
{
std::vector<engagementIndex> indicies;
indicies.push_back(engagementIndex{1});
indicies.push_back(engagementIndex{2});
indicies.push_back(engagementIndex{3});
engagementView view{indicies | std::ranges::views::all};
view.for_each([](const auto& t) {
std::cout << t.id << '\n';
});
}
full godbolt demo
the biggest disadvantages are
To solve those disadvantages you can create a type-erased wrapper that accepts any view that uses the heap or maybe a custom allocator .... which won't be much different than just returning a vector or a pmr::vector to start with.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With