Josh Josh - 9 days ago 6
C++ Question

How true is "Want Speed? Pass by value"

Correct me if I'm wrong. Say I have:

struct X
{
std::string mem_name;

X(std::string name)
: mem_name(std::move(name))
{}
...
};
struct Y
{
std::string mem_name;

Y(const std::string &name)
: mem_name(name)
{}
...
};


In
X
's ctor,
name
is obviously a copy of whatever argument got passed to
X
,
X
invokes the move ctor of
std::string
to initialize
mem_name
, right?

Let's call that a copy-then-move on X*; two operations: COPY, MOVE.

In
Y
's ctor,
name
is a const ref, which means there's no actual copy of the element because we're dealing directly with the argument passed from wherever
Y
's object needs to be created. But then we copied
name
to initialise
mem_name
in
Y
; one operation: COPY. Surely it should therefore be a lot faster (and preferable to me)?

In Scott Meyer's GN13 talk (around time-frame 8:10 and 8:56), he talks about "Want speed? Pass by value" and I was wondering is there any performance difference or loss in passing arguments (or strings to be precise) by reference and passing by value "in order to gain speed?"

I'm aware of the fact that passing arguments by value can be expensive, especially when dealing with large data.

Maybe (clearly?) there's something I'm missing from his talk?

Answer

The idea of "Want speed? Pass by value"(1) is that sometimes, the copy can be elided. Taking your classes X and Y, consider this usecase:

// Simulating a complex operation returning a temporary:
std::string foo() { return "a" + std::string("b"); }


struct X
{
  std::string mem_name;
  X(std::string name): mem_name(std::move(name)) {}
};

struct Y
{
  std::string mem_name;
  Y(const std::string &name): mem_name(name) {}
};


int main()
{
  X(foo());
  Y(foo());
}

Now let's analyse both the construction cases.

X first. foo() returns a temporary, which is used to initialise the object name. That object is then moved into mem_name. Notice that the compiler can apply Return Value Optimisation and construct the return value of foo() (actually even the return value of operator+) directly in the space of name. So no copying actually happens, only a move.

Now let's analyse Y. foo() returns a temporary again, which is bound to the reference name. Now there's no "externally supplied" space for the return value, so it has to be constructed in its own space and bound to the reference. It is then copied into mem_name. So we are doing a copy, no way around it.

In short, the outcome is:

  • If an lvalue is being passed in, both X and Y will perform a copy (X when initialising name, Y when initialising mem_name). In addition, X will perform a move (when initialising mem_name).

  • If an rvalue is being passed in, X will potentially only perform a move, while Y has to perform a copy.

Generally, a move is expected to be an operation whose time requirements are comparable to those of passing a pointer (which is what passing by reference does). So in effect, X is no worse than Y for lvalues, and better for rvalues.

Of course, it's not an absolute rule, and must be taken with a grain of salt. When in doubt, profile.


(1) The link is prone to being temporarily unavailable, and as of 11-12-2014, it seems broken (404). A copy of the contents (albeit with weird formatting) seems available at several blog sites:

Alternatively, the original content might be accessible through the wayback machine.

Also note that the topic has in general stirred up quite a discussion. Googling the paper title brings up a lot of follow-ups and counter-points. To list an example of one of these, there's "Want speed? Don't (always) pass by value" by SO member juanchopanza

Comments