I wonder if it makes sense to use references when using literal constants. Below i made a few examples:
bool funcWithoutReference(const std::string Text)
{
return Text == "Hello!";
}
bool funcWithReference(const std::string& Text)
{
return Text == "Hello!";
}
int main()
{
// Example 1: Average execution time: 13.652 sec
for (int i = 0; i < 10000000; i ) {
std::string text = "Hello";
funcWithoutReference(text);
}
// Example 2: Average execution time: 7.283 sec
for (int i = 0; i < 10000000; i ) {
funcWithoutReference("Hello");
}
// Example 3: Average execution time: 7.249 sec
for (int i = 0; i < 10000000; i ) {
std::string text = "Hello";
funcWithReference(text);
}
// Example 4: Average execution time: 7.292 sec
for (int i = 0; i < 10000000; i ) {
funcWithReference("Hello");
}
return 0;
}
Why is there such a difference between examples 1 and 2? I understand that using a literal constant in memory a "temporary" variable is created and passed as a parameter to the function. But why does example 2 get results almost like 3 and 4? Is a copy only made when we pass a variable as a parameter?
Time was measured as standard, using clock_t. Thanks for your help.
CodePudding user response:
Examples 2, 3, and 4 are identical. In each, you create a single std::string
per loop iteration. Example 1 does this too, but also copies it.
Let's break it down...
Example 1
Total string objects: 2
std::string text = "Hello";
funcWithoutReference(text);
Every time around the loop, you create a new string, initialized from the literal "Hello"
. You then pass it by value, so it is copied as a result of the function call.
Example 2
Total string objects: 1
funcWithoutReference("Hello");
Every time around the loop, you create a temporary string, initialized from the literal "Hello"
. This happens as a result of resolving the function call that will implicitly convert the literal to a std::string
by way of one of its available constructors. This string is an rvalue, meaning a temporary result. It only exists for the duration of the function call.
Example 3
Total string objects: 1
std::string text = "Hello";
funcWithReference(text);
Every time around the loop, you create a new string, initialized from the literal "Hello"
. You then pass this by reference, so no additional copies are made.
Example 4
Total string objects: 1
funcWithReference("Hello");
Every time around the loop, you create a temporary string, initialized from the literal "Hello"
. This happens as a result of resolving the function call that requires a const std::string&
. To achieve that, the literal is first implicitly converted to a std::string
rvalue by way of one of its available constructors. That is then provided by reference.
Notes
It should be pointed out that your test might be a bit bogus. These loops have no actual side-effects, and could potentially be optimized away completely. And Example 1 could also be optimized to elide the copy because there is no other use or side-effect for the local string.
Improvement
To avoid the overhead of allocating a new std::string
, you may choose to use std::string_view (since C 17). This is a light-weight string-like interface that works on both literals and std::string
. It does not do any dynamic allocation.
The cost of copying a string_view is therefore very low so you can pass them by value or reference without incurring a significant difference in computational cost.
#include <string_view>
bool funcWithoutReference(std::string_view Text)
{
return Text == "Hello!";
}
bool funcWithReference(const std::string_view& Text)
{
return Text == "Hello!";
}
int main()
{
// No allocation per iteration
for (int i = 0; i < 10000000; i ) {
funcWithoutReference("Hello");
}
for (int i = 0; i < 10000000; i ) {
funcWithReference("Hello");
}
// Single allocation per iteration
for (int i = 0; i < 10000000; i ) {
funcWithoutReference(std::string{"Hello"});
}
// Single allocation per iteration
for (int i = 0; i < 10000000; i ) {
funcWithReference(std::string{"Hello"});
}
}