简体   繁体   English

引用传递和值传递会导致C++中程序的时间复杂度发生变化吗?

[英]Does pass by reference and pass by value cause a change in time complexity of a program in C++?

In Java I never really had to worry about thinking if the argument is being passed by reference because it is not possible.在 Java 中,我从来不用担心参数是否通过引用传递,因为这是不可能的。 In C++ though both ways to pass arguments either by value or reference are possible, would either way passing the argument have an effect on the time complexity analysis through big O notation?在 C++ 中,尽管通过值或引用传递 arguments 的两种方式都是可能的,但通过大 O 表示法传递参数是否会影响时间复杂度分析? How would one determine the difference or change when the arguments are being passed by reference or value when calculating the big O notation?I have been finding some people say yes while others say no, is there a clear answer to this?在计算大 O 表示法时,当 arguments 通过引用或值传递时,如何确定差异或变化?我发现有些人说是,而另一些人说不是,对此有明确的答案吗?

There is no a yes-no answer for this question, since sometimes converting one to the other may not work.这个问题没有是或否的答案,因为有时将一个转换为另一个可能不起作用。

For explaining what happen, I take two simple examples for two directions.为了解释发生了什么,我为两个方向举了两个简单的例子。

From pass-by-reference to pass-by-value从引用传递到值传递

In this direction, the answer is yes.在这个方向上,答案是肯定的。 eg例如

bool binary_search(std::vector<int> &arr, int key)
{
    return std::binary_search(arr.begin(),arr.end(),key);
}

This is a binary search function in a vector , and the complexity is O(log n) where n is arr.size() .这是一个vector中的二进制搜索 function ,复杂度为O(log n) ,其中narr.size()

But if we modify it to pass-by-value like:但是,如果我们将其修改为按值传递,例如:

bool binary_search(std::vector<int> arr, int key)
{
    return std::binary_search(arr.begin(),arr.end(),key);
}

The complexity become O(n) since the function should copy the arr .复杂度变为O(n)因为 function 应该复制arr

From pass-by-value to pass-by-reference从值传递到引用传递

In this direction, the conversion may not work.在这个方向上,转换可能不起作用。 eg例如

std::vector<bool> batch_search(std::vector<int> arr, std::vector<int> keys)
{
    std::sort(arr.begin(), arr.end());
    std::vector<bool> res;
    for(auto key:keys)
    {
        res.push_back(std::binary_search(arr.begin(),arr.end(),key));
    }
    return res;
}

This is a batch search function.这是批量搜索 function。 The function frist sort a copy of the vector , and search for each key. function 首先对vector的副本进行sort ,然后搜索每个键。 The complexity is O(n log n + m log n) where m is keys.size() .复杂度是O(n log n + m log n)其中mkeys.size()

As you can see, the function sort the arr before using it.如您所见,function 在使用前对arr进行sort So directly converting arr from pass-by-reference to pass-by-value will not work.所以直接将arr从传递引用转换为传递值是行不通的。

One way to convert to pass-by-reference is like:转换为传递引用的一种方法是:

std::vector<bool> batch_search(std::vector<int> &arr, std::vector<int> keys)
{
    std::vector<int> copy_arr(arr);
    std::sort(copy_arr.begin(), copy_arr.end());
    std::vector<bool> res;
    for(auto key:keys)
    {
        res.push_back(std::binary_search(copy_arr.begin(),copy_arr.end(),key));
    }
    return res;
}

Just copy it, and sort the copy since what you need is the valus of the vector .只需复制它,并对副本sort ,因为您需要的是vectorvector Or in some sense, it's implement the pass-by-value using pass-by-referance.或者在某种意义上,它是使用传递引用来实现传递值。

Or an other way is like:或者另一种方式是:

std::vector<bool> batch_search(std::vector<int> &arr, std::vector<int> keys)
{
    std::vector<bool> res;
    for(auto key:keys)
    {
        res.push_back(std::find(arr.begin(),arr.end(),key)!=arr.end());
    }
    return res;
}

This way change the algorithm to avoid sort , and the complexity is O(n*m) .这种方式改变算法以避免sort ,复杂度为O(n*m)

But both the two approachs does not just convert the param, but rewrite the batch_search .但是这两种方法都不仅仅是转换参数,而是重写batch_search When discussing pass-by-reference and pass-by-value, it seems some function implement by pass-by-reference cannot be directly converted to pass-by-value.在讨论 pass-by-reference 和 pass-by-value 时,似乎有些 function 实现的 pass-by-reference 不能直接转换为 pass-by-value。

Is there a difference in the overall time complexity of an algorithm in C++ when using pass by value versus pass by reference?使用按值传递与按引用传递时,C++ 中算法的整体时间复杂度是否存在差异?

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.如果所述排序或搜索算法表示为O(n)O(log n) ,则该算法将始终具有该属性。

As for the structure of your code in C++ that can vary on a wide variety of things.至于 C++ 中的代码结构,可能会因各种各样的事情而有所不同。 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.比如你当前的硬件、主板和 CPU 的组合、你拥有的缓存类型、它有多少内核和线程、你的内存等等……你正在使用的操作系统,正在运行什么样的后台进程。 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.您当前的编译器,您正在使用什么样的优化,您是多线程还是编写并行编程源代码,您是否使用 MMX 寄存器等等......有很多因素在起作用。

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] .这是一个对具有[50k,100k]元素之间随机大小的向量进行sorting的演示应用程序,其中它们的值范围为[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. output 每次运行时都会有所不同,因为会生成不同大小的向量,但这是为了演示双向执行相同的算法并显示两者之间的最小时间差:我在之前和在排序到文件之后只是为了验证它们在排序之前是否相同并且它们实际上已经排序。 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.我有一个运行 Windows 7 64 位的 8GB Ram 的 Intel Core 2 Quad Extreme 3.0Ghz。 I'm using Visual Studio 2017 with C++17.我正在使用带有 C++17 的 Visual Studio 2017。 I'm compiling everything in x64 mode.我正在 x64 模式下编译所有内容。

Here are some of my random outputs in Debug Mode:以下是我在调试模式下的一些随机输出:

Trial 1试验 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试验 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试验 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以下是发布模式下的一些试验,优化设置为/O2

Trial 1试验 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试验 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试验 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.可以肯定地说,使用std::sort算法时,按值传递和按引用传递的差异非常小。 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.从上面的 output 中可以看出,与按引用对应的版本相比,即使将值初始化到临时向量和在按值版本中返回副本,开销也很小。

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...执行复制可能并不总是那么昂贵,尤其是现代编译器将使用的优化技巧,以及现代 CPU 如何利用其缓存以及矢量化 memory 寄存器...

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.我认为您应该更关心为正确的问题选择正确的算法设计适当的数据结构以具有正确的 memory alignment以获得更好的缓存命中性能,而不是担心按值传递与按引用传递的次要语义。 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.如果 function 需要来自外部的值但不更改该外部值并且您的代码的后面部分不需要更新它,则按值传递...您需要在 function 调用之后使用它,然后通过引用传递。

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.此外,当传递大尺寸的容器时,通过引用传递通常是首选……我展示的演示最多只有 100k 个元素。 What if a container had over 3 billion?如果一个容器有超过 30 亿个呢? Would you want to have to copy all 3 billion elements?你想复制所有 30 亿个元素吗? 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.如果您只需要引用它来执行该本地 function 的 scope 内的其他变量的计算,则通过 const 引用传递。

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.因为O(n^2 + 10n)仍然被认为只是O(n^2)O(n + 10000)仍然被认为只是O(n)等等。

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!现在另一方面,如果要复制的 object 很复杂并且需要大量资源、动态 memory 分配等......那么是的,这可以改变算法的复杂性,但对于任何被认为是默认可构造的RAII ,微不足道的破坏,甚至是可移动的,那么不,它没有!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM