33

"Premature optimization is the root of all evil"

I think this we can all agree upon. And I try very hard to avoid doing that.

But recently I have been wondering about the practice of passing parameters by const Reference instead of by Value. I have been taught / learned that non-trivial function arguments (i.e. most non-primitive types) should preferably be passed by const reference - quite a few books I've read recommend this as a "best practice".

Still I cannot help but wonder: Modern compilers and new language features can work wonders, so the knowledge I have learned may very well be outdated, and I never actually bothered to profile if there are any performance differences between

void fooByValue(SomeDataStruct data);   

and

void fooByReference(const SomeDataStruct& data);

Is the practice that I have learned - passing const references (by default for non-trivial types) - premature optimization?

15
  • 3
    See also: F.call in the C++ Core Guidelines for a discussion of various parameter passing strategies.
    – amon
    Commented Jun 5, 2018 at 11:18
  • 1
    Possible duplicate of Is it bad practice to write code that relies on compiler optimizations?
    – Doc Brown
    Commented Jun 5, 2018 at 11:20
  • 2
    @DocBrown The accepted answer to that question refers to the least astonishment principle, which may apply here too (i.e. using const references is industry standard, etc. etc.). That said, I disagree that the question is a duplicate: The question you refer to asks if it is bad practice to (generally) rely on compiler optimization. This question asks the inverse: Is passing const references a (premature) optimization?
    – CharonX
    Commented Jun 5, 2018 at 12:18
  • 1
    @DocBrown: So, before you can declare it a dupe, point out where in the question it says that the compiler will be allowed and able to "optimize" that. Commented Jun 5, 2018 at 13:38
  • 2
    @David Yes, it is still necessary (and will remain so). The compiler is able to perform some (well documented) optimizations like Copy elision, but what you suggest would require changing the type of a variable (from const T to const T&) which is not permitted by the standart for a good number of reasons: From thread-safety (are the functions reentrant?) to application logic (the class may e.g. use a mutable cache to optimize calculations) where using a (const) reference instead of a (const) copy would break things.
    – CharonX
    Commented Sep 14, 2018 at 7:13

5 Answers 5

77

"Premature optimisation" is not about using optimisations early. It is about optimising before the problem is understood, before the runtime is understood, and often making code less readable and less maintainable for dubious results.

Using "const&" instead of passing an object by value is a well-understood optimisation, with well-understood effects on runtime, with practically no effort, and without any bad effects on readability and maintainability. It actually improves both, because it tells me that a call will not modify the object passed in. So adding "const&" right when you write the code is NOT PREMATURE.

9
  • 7
    I agree on the "practically no effort" part of your answer. But premature optimization is first and foremost about optimization before their is an noteable, measured performance impact. And I don't think most C++ programmers (which includes myself) make any measurements before using const&, so I think the question is quite sensible.
    – Doc Brown
    Commented Jun 5, 2018 at 20:59
  • 1
    You measure before optimising to know whether any tradeoffs are worth it. With const& the total effort is typing seven characters, and it has other advantages. When you don't intend to modify the variable being passed in, it is of advantage even if there is no speed improvement.
    – gnasher729
    Commented Jun 5, 2018 at 21:23
  • 3
    I'm not a C expert, so a question:. const& foo says the function won't modify foo, so the caller is safe. But a copied value says that no other thread can change foo, so the callee is safe. Right? So, in a multi-threaded app, the answer depends on correctness, not optimization.
    – user949300
    Commented Jun 5, 2018 at 22:35
  • 1
    @user949300 I would posit that (1) it is far better to put one of these into the design of an object: (1.1) immutability, or (1.2) mutability (better with protection), or (1.3) value-semantic; (2) doing so takes the guesswork out of parameter-passing; it reduces mental effort on the user; (3) that this advice applies to both the object on which the method(s) are defined, as well as objects being passed as arguments; (4) when done properly, const-reference can be used without introducing bugs; (5) if we choose to ignore these points, the matter of multithreading doesn't lead to a clear answer.
    – rwong
    Commented Jun 5, 2018 at 23:16
  • 1
    @DocBrown you may eventually quetion the motivation of the developer which put the const & ? If he did it for performance only without considering the rest it might be considered as premature optimization. Now if he puts it because he knows it will be a const parameters then he's only self documenting his code and giving the opportunity to the compiler to optimize, which is better.
    – Walfrat
    Commented Jun 6, 2018 at 14:59
22

TL;DR: Pass by const reference is still a good idea in C++, all things considered. Not a premature optimization.

TL;DR2: Most adages don't make sense, until they do.


Aim

This answer just tries to extend the linked item on the C++ Core Guidelines(first mentioned in amon's comment) a little bit.

This answer does not try to address the issue of how to think and apply properly the various adages that were widely circulated within programmers' circles, especially the issue of reconciling between conflicting conclusions or evidence.


Applicability

This answer applies to function calls (non-detachable nested scopes on the same thread) only.

(Side note.) When passable things can escape the scope (i.e. have a lifetime that potentially exceeds the outer scope), it becomes more important to satisfy the application's need for object lifetime management before anything else. Usually, this requires using references that are also capable of lifetime management, such as smart pointers. An alternative might be using a manager. Note that, lambda is a kind of detachable scope; lambda captures behave like having object scope. Therefore, be careful with lambda captures. Also be careful with how the lambda itself is passed - by copy or by reference.


When to pass by value

For values that are scalar (standard primitives that fit within a machine register and have value semantic) for which there is no need for communication-by-mutability (shared reference), pass by value.

For situations where callee require a cloning of an object or aggregate, pass by value, in which the callee's copy fulfills the need for a cloned object.


When to pass by reference, etc.

for all other situations, pass by pointers, references, smart pointers, handles (see: handle-body idiom), etc. Whenever this advice is followed, apply the principle of const-correctness as usual.

Things (aggregates, objects, arrays, data structures) that are sufficiently large in memory footprint should always be designed to facilitate pass-by-reference, for performance reasons. This advice definitely applies when it is hundreds of bytes or more. This advice is borderline when it is tens of bytes.


Unusual paradigms

There are special-purpose programming paradigms which are copy-heavy by intention. For example, string processing, serialization, network communication, isolation, wrapping of third-party libraries, shared-memory inter-process communication, etc. In these application areas or programming paradigms, data is copied from structs to structs, or sometimes repackaged into byte arrays.


How the language specification affects this answer, before optimization is considered.

Sub-TL;DR Propagating a reference should invoke no code; passing by const-reference satisfies this criterion. However, all other languages satisfy this criterion effortlessly.

(Novice C++ programmers are advised to skip this section entirely.)

(The beginning of this section is partly inspired by gnasher729's answer. However, a different conclusion is reached.)

C++ allows user-defined copy constructors and assignment operators.

(This is (was) a bold choice that is (was) both amazing and regrettable. It is definitely a divergence from today's acceptable norm in language design.)

Even if the C++ programmer does not define one, the C++ compiler must generate such methods based on language principles, and then determine whether additional code needs to be executed other than memcpy. For example, a class/struct that contains a std::vector member has to have a copy-constructor and an assignment operator that is non-trivial.

In other languages, copy constructors and object cloning are discouraged (except where absolutely necessary and/or meaningful to the application's semantics), because objects have reference semantics, by language design. These languages will typically have garbage collection mechanism that is based on reachability instead of scope-based ownership or reference-counting.

When a reference or pointer (including const reference) is passed around in C++ (or C), the programmer is assured that no special code (user-defined or compiler-generated functions) will be executed, other than the propagation of the address value (reference or pointer). This is a clarity of behavior that C++ programmers find comfortable with.

However, the backdrop is that the C++ language is unnecessarily complicated, such that this clarity of behavior is like an oasis (a survivable habitat) somewhere around a nuclear fallout zone.

To add more blessings (or insult), C++ introduces universal references (r-values) in order to facilitate user-defined move operators (move-constructors and move-assignment operators) with good performance. This benefits a highly relevant use case (the moving (transfer) of objects from one instance to another), by means of reducing the need for copying and deep-cloning. However, in other languages, it is illogical to speak of such moving of objects.


(Off-topic section) A section dedicated to an article, "Want Speed? Pass by Value!" written in circa 2009.

That article was written in 2009 and explains the design justification for r-value in C++. That article presents a valid counter-argument to my conclusion in the previous section. However, the article's code example and performance claim has long been refuted.

Sub-TL;DR The design of r-value semantics in C++ allows for a surprisingly elegant user-side semantics on a Sort function, for example. This elegant is impossible to model (imitate) in other languages.

A sort function is applied to a whole data structure. As mentioned above, it would be slow if a lot of copying is involved. As a performance optimization (that is practically relevant), a sort function is designed to be destructive in quite a few languages other than C++. Destructive means that the target data structure is modified to achieve the sorting goal.

In C++, the user can choose to call one of the two implementations: a destructive one with better performance, or a normal one which does not modify the input. (Template is omitted for brevity.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

Aside from sorting, this elegance is also useful in the implementation of destructive median finding algorithm in an array (initially unsorted), by recursive partitioning.

However, note that, most languages would apply a balanced binary search tree approach to sorting, instead of applying a destructive sorting algorithm to arrays. Therefore, the practical relevance of this technique is not as high as it seems.


How compiler optimization affects this answer

When inlining (and also whole-program optimization / link-time optimization) is applied across several levels of function calls, the compiler is able to see (sometimes exhaustively) the flow of data. When this happens, compiler can apply many optimizations, some of which can eliminate the creation of whole objects in memory. Typically, when this situation applies, it doesn't matter if the parameters are passed by value or by const-reference, because the compiler can analyze exhaustively.

However, if the lower level function calls something that is beyond analysis (e.g. something in a different library outside compilation, or a call graph that is simply too complicated), then the compiler must optimize defensively.

Objects larger than a machine register value might be copied by explicit memory load/store instructions, or by a call to the venerable memcpy function. On some platforms, the compiler generates SIMD instructions in order to move between two memory locations, each instruction moving tens of bytes (16 or 32).


Discussion on the issue of verbosity or visual clutter

C++ programmers are accustomed to this, i.e. as long as a programmer doesn't hate C++, the overhead of writing or reading const-reference in source code isn't horrible.

The cost-benefit analyses might have been done many times before. I don't know if there's any scientific ones which should be cited. I guess most analyses would be non-scientific or non-reproducible.

Here is what I imagine (without proof or credible references)...

  • Yes, it affects the performance of software written in this language.
  • If compilers can understand the purpose of code, it could potentially be smart enough to automate that
  • Unfortunately, in languages that favor mutability (as opposed to functional purity), the compiler would classify most things as being mutated, therefore the automated deduction of constness would reject most things as non-const
  • The mental overhead depends on people; people who find this to be a high mental overhead would have rejected C++ as a viable programming language.
1
  • This is one of the situations where I wish I could accept two answers instead of having to chose only one... sigh
    – CharonX
    Commented Jun 8, 2018 at 10:15
12

In DonaldKnuth's paper "StructuredProgrammingWithGoToStatements", he wrote: "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." - Premature Optimization

This isn't advising programmers to use the slowest techniques available. It's about focusing on clarity when writing programs. Often, clarity and efficiency are a trade-off: if you must pick only one, prefer clarity. But if you can achieve both easily, there's no need to cripple clarity (like signaling that something is a constant) just to avoid efficiency.

11
  • 4
    "if you must pick only one, pick clarity." The second should be prefer instead, as you might be forced to choose the other. Commented Jun 5, 2018 at 23:26
  • @Deduplicator Thank you. In the OP’s context, though, the programmer has the freedom to choose.
    – Lawrence
    Commented Jun 5, 2018 at 23:44
  • Your answer reads a bit more general than that though... Commented Jun 5, 2018 at 23:57
  • @Deduplicator Ah, but the context of my answer is (also) that the programmer chooses. If the choice was forced on the programmer, it wouldn't be "you" that does the picking :) . I considered the change you suggested and wouldn't object to you editing my answer accordingly, but I prefer the existing wording for its clarity.
    – Lawrence
    Commented Jun 6, 2018 at 0:01
  • Is const X more or less clear than const X &?
    – Spencer
    Commented Apr 28, 2023 at 15:06
11

Passing by ([const][rvalue]reference)|(value) should be about the intent and promises made by the interface. It has nothing to do with performance.

Richy's Rule of Thumb:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state
0
3

Theoretically, the answer should be yes. And, in fact, it is yes some of the time--as a matter of fact, passing by const reference instead of just passing a value can be a pessimization, even in cases where the passed value is too large to fit in a single register (or most of the other heuristics people try to use to determine when to pass by value or not). Years ago, David Abrahams wrote an article named "Want Speed? Pass by Value!" covering some of these cases. It's no longer easy to find, but if you can dig up a copy it's worth reading (IMO).

In the specific case of passing by const reference, however, I'd say the idiom is so well established that the situation is more or less reversed: unless you know the type will be char/short/int/long, people expect to see it passed by const reference by default, so it's probably best to go along with that unless you have a fairly specific reason to do otherwise.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.