Author: Chloé Lourseyre
Context
This week, I will present you a piece of code I bumped into not so long ago.
It looked like that:
#include <vector>
#include <algorithm>
int count_even(const std::vector<int>& v)
{
const auto& my_func = [] (int i)->bool
{
return i%2==0;
};
return std::count_if(std::cbegin(v), std::cend(v), my_func);
}
Seeing this, VisualStudio was unsure if it should compile or not.
And by unsure, I mean that a compilation error stating something like “my_func
is used but it is already destroyed” was periodicly prompted then discarded during the compilation of the library. But in the end, it compiled.
When I saw this, I thought two things:
“Wait, we can bind a temporary to a const ref?”
and
“What’s the use of binding a lambda to a const ref?”
These are the questions I will answer today.
Binding a rvalue to a const reference
To be short: yes, you can bind a rvalue to a const ref.
Intuitivly, I would have said that trying to do so will only result in a dangling reference, but it will not.
This language is smart, I tend to forget it, but it is only logical that if you try to bind a cons ref to a temporary object, the object will be put on the memory stack and accessed via the const ref.
To illustrate this, here are two functions:
void foo()
{
const int& i = 1;
const int& j = i+1;
}
void bar()
{
int iv = 1;
const int& i = iv;
int jv = i+1;
const int& j = jv;
}
The function foo
direclty binds the rvalues to const refs, as the function bar
instanciate const values before binding them to cons refs (thus binding a lvalue to a const ref).
The assembly code that clang generate for this piece of code is the following:
foo(): # @foo()
push rbp
mov rbp, rsp
mov dword ptr [rbp - 12], 1
lea rax, [rbp - 12]
mov qword ptr [rbp - 8], rax
mov rax, qword ptr [rbp - 8]
mov eax, dword ptr [rax]
add eax, 1
mov dword ptr [rbp - 28], eax
lea rax, [rbp - 28]
mov qword ptr [rbp - 24], rax
pop rbp
ret
bar(): # @bar()
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 1
lea rax, [rbp - 4]
mov qword ptr [rbp - 16], rax
mov rax, qword ptr [rbp - 16]
mov eax, dword ptr [rax]
add eax, 1
mov dword ptr [rbp - 20], eax
lea rax, [rbp - 20]
mov qword ptr [rbp - 32], rax
pop rbp
ret
These two functions are (except from the stack alignment details, which are not really important) the same.
This fact is confirmed by the IBM documentation: Initialization of references (C++ only) – IBM Documentation, and, of course, by the standard (The C++11 Programming Language, §7.7.1).
This is a very simple fact that is really clear in the standard, but it is rarely used in code and referenced on the web.
The reason for that is that binding to a const ref instead of a const value – or even a plain value – seems useless.
But is it?
Binding a lambda to a const ref
Coming back to the initial context, the question was why it is legal to bind a lambda to a const ref and if it is useful.
As a reminder, here is the example I showed you earlier:
#include <vector>
#include <algorithm>
int count_even(const std::vector<int>& v)
{
const auto& my_func = [] (int i)->bool
{
return i%2==0;
};
return std::count_if(std::cbegin(v), std::cend(v), my_func);
}
When we put it in C++ Insights (cppinsights.io), we obtain the following code:
#include <vector>
#include <algorithm>
int count_even(const std::vector<int, std::allocator<int> > & v)
{
class __lambda_6_27
{
public:
inline /*constexpr */ bool operator()(int i) const
{
return (i % 2) == 0;
}
using retType_6_27 = auto (*)(int) -> bool;
inline /*constexpr */ operator retType_6_27 () const noexcept
{
return __invoke;
};
private:
static inline bool __invoke(int i)
{
return (i % 2) == 0;
}
public:
// inline /*constexpr */ __lambda_6_27(const __lambda_6_27 &) noexcept = default;
// inline /*constexpr */ __lambda_6_27(__lambda_6_27 &&) noexcept = default;
// /*constexpr */ __lambda_6_27() = default;
};
const __lambda_6_27 & my_func = __lambda_6_27{};
return static_cast<int>(std::count_if(std::cbegin(v), std::cend(v), __lambda_6_27(my_func)));
}
As you might have guessed, a lambda function is in fact a functor (here called __lambda_6_27
). Thus, the assignation calls the constructor of that functor, which is a rvalue.
We just saw that we could bind a rvalue to a const ref, thus binding a lambda to a const ref is legal.
This is why we can bind a lambda to a const ref.
Performance and optimization
To answer the question that if we should bind a lambda to a const ref instead of a value, we have to evaluate if one method is faster that the other.
Execution time
I’ll use Quick C++ Benchmarks (quick-bench.com) to evaluate execution time.
Here are the snippets I’ll use:
std::vector<int> v = {0,1,2,3,4};
static void ConstRef(benchmark::State& state)
{
const auto& l = [](int i)->bool{ return i%2 == 0;};
for (auto _ : state)
{
std::count_if(cbegin(v), cend(v), l);
}
}
BENCHMARK(ConstRef);
static void Plain(benchmark::State& state)
{
auto l = [](int i)->bool{ return i%2 == 0;};
for (auto _ : state)
{
std::count_if(cbegin(v), cend(v), l);
}
}
BENCHMARK(Plain);
I ran benchmarks using Clang 11.0 and GCC 10.2, with all optimization options between -O0 and -O3.
And here are the results:
Compiler | Optiomization option | ET with const ref | ET with plain value | const ref / plain value ratio |
---|---|---|---|---|
Clang 11.0 | -O0 | 32.306 | 33.732 | 0.958 |
Clang 11.0 | -O1 | 224.96 | 204.92 | 1.097 |
Clang 11.0 | -O2 | 3.9982e-6 | 4.0088e-6 | 0.997 |
Clang 11.0 | -O3 | 3.7273e-6 | 4.1281e-6 | 0.903 |
GCC 10.2 | -O0 | 64.379 | 65.017 | 0.990 |
GCC 10.2 | -O1 | 11.754 | 11.871 | 0.990 |
GCC 10.2 | -O2 | 3.7470e-6 | 4.0196e-6 | 0.932 |
GCC 10.2 | -O3 | 3.6523e-6 | 3.9021e-6 | 0.936 |
What you want to look at is the last column, which computes the ratio between const-ref and plain value (the units of the execution times have unlabeled units, it’s useless to try and compare them directly).
A value greater that 1 means the const-ref version is slower than the plain version, while a value lower that 1 means the const-ref is slower.
Here are the charts:
All in all we can see that while the const-ref version is still faster than the value version, the difference doesn’t go above 10%. The highest differences are at the maximum optimization level.
If the code in a bottleneck (in the 20% of Pareto’s law) then this 10% can make a little difference, but I wouldn’t expect more than one or two percent gain overall (including other parts of your code).
However, if this is not a bottleneck, then there’s no version to prefer over the other one.
Compilation time
Does the const ref binding can affect compilation time? To answer this, I used C++ Build Benchmarks (build-bench.com).
I compiled the following codes in the same circumstances, using Clang 11.0 and GCC 10.2, with all optimization options between -O0 and -O3:
#include <vector>
int main()
{
const auto& l = [](int i)->bool{ return i%2 == 0;};
std::vector<int> v= {0,1,2,3,4};
std::count_if(cbegin(v), cend(v), l);
}
Then, without using const ref:
#include <vector>
int main()
{
auto l = [](int i)->bool{ return i%2 == 0;};
std::vector<int> v= {0,1,2,3,4};
std::count_if(cbegin(v), cend(v), l);
}
And here are the results:
Compiler | Optimization option | BT with const ref | BT with plain value | const ref / plain value ratio |
---|---|---|---|---|
Clang 11.0 | -O0 | 0.3629 | 0.3510 | 1.034 |
Clang 11.0 | -O1 | 0.4010 | 0.4035 | 0.994 |
Clang 11.0 | -O2 | 0.3755 | 0.3765 | 0.997 |
Clang 11.0 | -O3 | 0.3745 | 0.3735 | 1.003 |
GCC 10.2 | -O0 | 0.3915 | 0.3900 | 1.004 |
GCC 10.2 | -O1 | 0.3830 | 0.3810 | 1.005 |
GCC 10.2 | -O2 | 0.3765 | 0.3775 | 0.997 |
GCC 10.2 | -O3 | 0.3765 | 0.3750 | 1.004 |
In any case, there is less than 4% difference between the two versions, and in most cases it’s event under 1%. We can say that there is no effective difference.
Conclusion
There is not real advantage in binding a rvalue to a const ref instead of a plain value. Most of the time, you’ll prefer to use a const value just for the sake of saving a symbol (but there is no huge difference).
In case you are in a bottleneck, you might consider using const refs instead of values, but I suggest you do your own benchmarks, tied to your specific context, in order to detemine which version is the better.
Thanks for reading, and see you next week!
Author: Chloé Lourseyre