Is there a difference in the overall time complexity of an algorithm in C++ when using pass by value versus pass by reference?
This depends on how technical you want to get. The actual time complexity of the algorithm itself does not change. If a said sorting or searching algorithm is expressed as O(n) or O(log n) then that algorithm will always have that property.
As for the structure of your code in C++ that can vary on a wide variety of things. Such as your current hardware, Motherboard and CPU combo, the type of cache you have, how many cores and threads it has, your ram, etc... The operating system you are using, what kind of background processes are running. Your current compiler, what kind of optimizations you are using, are you multithreading or writing parallel programming source code, are you utilizing MMX registers, etc... There are a lot of factors that come into play.
Will your execution time change? Slightly, but almost insignificantly. Here's a demo application of sorting a vector that has a random size between [50k,100k] elements where their values range from [1,10k].
The output will vary each time you run this as a different size vector will be generated, but this is to demonstrate the same algorithm being performed both ways and to show the minimal time difference between the two: I write the values of both vectors before and after sorting to a file just to verify that they are the same before being sorted and that they were actually sorted. I only display the time executions to the console.
#include <algorithm>
#include <exception>
#include <chrono>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <random>
template<class Resolution = std::chrono::milliseconds>
class ExecutionTimer {
public:
using Clock = std::conditional_t < std::chrono::high_resolution_clock::is_steady,
std::chrono::high_resolution_clock,
std::chrono::steady_clock>;
private:
const Clock::time_point mStart = Clock::now();
public:
ExecutionTimer() = default;
~ExecutionTimer() {
const auto end = Clock::now();
std::ostringstream strStream;
strStream << "Destructor Elapsed: "
<< std::chrono::duration_cast<Resolution>(end - mStart).count()
<< std::endl;
std::cout << strStream.str() << std::endl;
}
inline void stop() {
const auto end = Clock::now();
std::ostringstream strStream;
strStream << "Stop Elapsed: "
<< std::chrono::duration_cast<Resolution>(end - mStart).count()
<< std::endl;
std::cout << strStream.str() << std::endl;
}
};
std::vector<uint32_t> sortVectorByValue(std::vector<uint32_t> values) {
std::vector<uint32_t> temp{ values };
std::sort(temp.begin(), temp.end());
return temp;
}
void sortVectorByReference(std::vector<uint32_t>& values) {
std::sort(values.begin(), values.end());
}
void writeVectorToFile(std::ofstream& file, std::vector<uint32_t>& values) {
int count = 0;
for (auto& v : values) {
if (count % 15 == 0)
file << '\n';
file << std::setw(5) << v << ' ';
count++;
}
file << "\n\n";
}
int main() {
try {
std::random_device rd;
std::mt19937 gen{ rd() };
std::uniform_int_distribution<uint32_t> distSize(50000, 100000);
std::uniform_int_distribution<uint32_t> distRange(1, 10000);
std::vector<uint32_t> values;
values.resize(distSize(gen));
for (auto& v : values)
v = distRange(gen);
std::ofstream file;
file.open("random numbers.txt");
file << "values:\n";
writeVectorToFile(file, values);
std::vector<uint32_t> values2{ values };
file << "values2:\n";
writeVectorToFile(file, values2);
// Using a local block scope to cause the Execution Timer to call its destructor
{
std::cout << "Evaluated Execution Time for Pass By Value of Sorting " << values.size() << " Elements:\n";
ExecutionTimer timer;
values = sortVectorByValue(values);
timer.stop();
}
{
std::cout << "Evaluated Execution Time for Pass By Reference of Sorting " << values2.size() << " Elements:\n";
ExecutionTimer timer;
sortVectorByReference(values2);
timer.stop();
}
file << "values1:\n";
writeVectorToFile(file, values);
file << "values2:\n";
writeVectorToFile(file, values2);
file.close();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
I have an Intel Core 2 Quad Extreme 3.0Ghz with 8GB Ram running Windows 7 64bit. I'm using Visual Studio 2017 with C++17. I'm compiling everything in x64 mode.
Here are some of my random outputs in Debug Mode:
Trial 1
Evaluated Execution Time for Pass By Value of Sorting 93347 Elements:
Stop Elapsed: 247
Destructor Elapsed: 247
Evaluated Execution Time for Pass By Reference of Sorting 93347 Elements:
Stop Elapsed: 247
Destructor Elapsed: 247
Trial 2
Evaluated Execution Time for Pass By Value of Sorting 58782 Elements:
Stop Elapsed: 174
Destructor Elapsed: 174
Evaluated Execution Time for Pass By Reference of Sorting 58782 Elements:
Stop Elapsed: 172
Destructor Elapsed: 172
Trial 3
Evaluated Execution Time for Pass By Value of Sorting 67137 Elements:
Stop Elapsed: 194
Destructor Elapsed: 194
Evaluated Execution Time for Pass By Reference of Sorting 67137 Elements:
Stop Elapsed: 191
Destructor Elapsed: 191
Here are some trials in release mode with optimizations set to /O2
Trial 1
Evaluated Execution Time for Pass By Value of Sorting 61078 Elements:
Stop Elapsed: 4
Destructor Elapsed: 5
Evaluated Execution Time for Pass By Reference of Sorting 61078 Elements:
Stop Elapsed: 4
Destructor Elapsed: 4
Trial 2
Evaluated Execution Time for Pass By Value of Sorting 87909 Elements:
Stop Elapsed: 6
Destructor Elapsed: 6
Evaluated Execution Time for Pass By Reference of Sorting 87909 Elements:
Stop Elapsed: 6
Destructor Elapsed: 6
Trial 3
Evaluated Execution Time for Pass By Value of Sorting 93007 Elements:
Stop Elapsed: 7
Destructor Elapsed: 8
Evaluated Execution Time for Pass By Reference of Sorting 93007 Elements:
Stop Elapsed: 9
Destructor Elapsed: 9
All times are measured in milliseconds. It is safe to say that with the std::sort algorithm there is very minimal difference in passing by value as by reference. As you can see from the output above, even the initialization of values to the temp vector and the returning of the copy within the by-value version has very little overhead compared to its by-reference counterpart.
Yes, there is a little more work to do, but the leading factor in complexity is the term of the polynomial of the highest order. Performing a copy may not always be that expensive, especially with the optimization tricks that modern compilers will use, and how modern CPU's can utilize their cache as well as their vectorized memory registers...
I think you should be more concerned with choosing the right algorithm for the right problem and designing appropriate data structures to have proper memory alignment for better cache-hit performance than worrying about the minor semantics of pass by value versus pass by reference. Sometimes you will want to pass by value and others you will want to pass by reference. It's more of a matter of knowing when to use which feature based on the context of the problem at hand.
If a function needs a value from outside but doesn't change that outside value and later parts of your code don't require it to be updated, then pass by value... If a function requires a state of value but will change it and you will need to use it later after the function call, then pass by reference.
Also when passing containers of large size, passing by reference is normally the preferred choice... the demo that I showed only had up to 100k elements. What if a container had over 3 billion? Would you want to have to copy all 3 billion elements? Probably not. So when you have extremely large containers, it's better to pass by reference if you need to modify the contents of that container. If you only need to reference it to perform calculations for other variables within the scope of that local function, then pass by const reference.
Overall, does it change the time complexity of the algorithm? I'd say no it doesn't! Why?
Because O(n^2 + 10n) is still considered just O(n^2) and O(n + 10000) is still considered just O(n), etc.
Now on the other hand, if the object being copied is complex and requires a bunch of resources, dynamic memory allocations, etc... then yes this can change the complexity of the algorithm, but for anything that is considered RAII that is default constructible, trivially destructible, and even movable, then no it doesn't!