Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the copy not being elided for function arguments

Tags:

c++

g++

c++17

I have the following code

#include <cstdlib>
#include <vector>
#include <chrono>
#include <iostream>

static const uint64_t BENCHMARK_RUNS(1000000);

std::vector<float> vec_mul_no_ref(const std::vector<float> x,
                                  const std::vector<float> y) {
  if(x.size() != y.size()) {
    throw std::runtime_error("vectors are not the same size!");
  }
  std::vector<float> ans(x.size());
  for (size_t ii=0; ii<x.size(); ii++) {
    ans[ii] = x[ii] * y[ii];
  }
  return ans;
}

std::vector<float> vec_mul_ref_args(const std::vector<float>& x,
                                   const std::vector<float>& y) {
  if(x.size() != y.size()) {
    throw std::runtime_error("vectors are not the same size!");
  }
  std::vector<float> ans(x.size());
  for (size_t ii=0; ii<x.size(); ii++) {
    ans[ii] = x[ii] * y[ii];
  }
  return ans;
}

void vec_mul_all_ref(const std::vector<float>& x,
                   const std::vector<float>& y, std::vector<float>& ans) {
  if(x.size() != y.size() || y.size() != ans.size()) {
    throw std::runtime_error("vectors are not the same size!");
  }
  for (size_t ii=0; ii<x.size(); ii++) {
    ans[ii] = x[ii] * y[ii];
  }
}

void bench_vec_mul() {
  size_t vec_size(10000);
  std::vector<float> x(vec_size);
  std::vector<float> y(vec_size);
  for(size_t ii=0; ii<vec_size; ii++) {
    x[ii] = (static_cast<double>(rand()) / RAND_MAX) * 100.0;
    y[ii] = (static_cast<double>(rand()) / RAND_MAX) * 100.0;
  }
  // bench no_ref
  auto start = std::chrono::steady_clock::now();
  for (uint64_t ii=0; ii < BENCHMARK_RUNS; ii++) {
    std::vector<float> ans = vec_mul_no_ref(x, y);
  }
  auto end = std::chrono::steady_clock::now();
  double time = static_cast<double>(
                std::chrono::duration_cast<
                std::chrono::microseconds>(end-start).count());
  std::cout << "Time to multiply vectors (no_ref) = " 
            << time / BENCHMARK_RUNS * 1e3 << " ns" << std::endl;

  // bench ref_args
  start = std::chrono::steady_clock::now();
  for (uint64_t ii=0; ii < BENCHMARK_RUNS; ii++) {
    std::vector<float> ans = vec_mul_ref_args(x, y);
  }
  end = std::chrono::steady_clock::now();
  time = static_cast<double>(
                std::chrono::duration_cast<
                std::chrono::microseconds>(end-start).count());
  std::cout << "Time to multiply vectors (ref_args) = " 
            << time / BENCHMARK_RUNS * 1e3 << " ns" << std::endl;

  // bench all_ref
  start = std::chrono::steady_clock::now();
  for (uint64_t ii=0; ii < BENCHMARK_RUNS; ii++) {
    std::vector<float> ans(x.size());
    vec_mul_all_ref(x, y, ans);
  }
  end = std::chrono::steady_clock::now();
  time = static_cast<double>(
                std::chrono::duration_cast<
                std::chrono::microseconds>(end-start).count());
  std::cout << "Time to multiply vectors (all_ref) = " 
            << time / BENCHMARK_RUNS * 1e3 << " ns" << std::endl;
}

int main() {
  bench_vec_mul();
  return 0;
}

an example output from this code on my laptop (compiled with g++ -o benchmark main.cc -std=c++17 -O3) is:

Time to multiply vectors (no_ref) = 5117.05 ns                                      
Time to multiply vectors (ref_args) = 3000.69 ns
Time to multiply vectors (all_ref) = 2996.84 ns

The second and third times being so similar indicates that return value optimization is being performed and that copy is being elided. My question is: why isn't the copy for the function arguments also elided by the compiler so that the first time matches the second two? Declaring the function arguments as const guarantees they won't change, so there's no chance of accidentally editing the original variables.

I have gcc (GCC) 8.1.1 20180531

like image 988
Matt Spicer Avatar asked Oct 31 '25 00:10

Matt Spicer


2 Answers

My question is: why isn't the copy for the function arguments also elided by the compiler so that the first time matches the second two?

Because the standard doesn't allow them to be.

Elision is not something that just happens; it isn't a part of the ubiquitous as-if rule. Because it affects user-visible behavior (copy/move constructors are potentially user-defined code), the standard has to explicitly say that sometimes, implementations are free not to call them. As such, the standard permits elision only under very specific sets of circumstances.

When it comes to initializing parameters from argument expressions, elision is permitted only if the parameter is a value type and the argument expression is a temporary of that type. Well if you want to get technical, in C++17 there is no elision in this case: the prvalue would initialize the parameter directly rather than manifesting a temporary, so there is no copy/move to elide.

But that's not the case here. x and y are not prvalues, and therefore are not eligible for elision. They will be copied into those parameters.

Indeed, the only time a named object is eligible for elision is if you return it. And even then, it doesn't work if it's a parameter of that function.

like image 199
Nicol Bolas Avatar answered Nov 01 '25 15:11

Nicol Bolas


Nicol explained the technical reasons that copy elision is not allowed by the standard in your case.

As for the rationale, I think it's just that that pass-by-value has different lifetime and ownership semantics than pass-by-reference. When a function receives an argument by value, it is not just saying "I want a copy so that I can modify it and have the original be unaffected". It is also saying, "I want to own (my copy of) this object, and control its lifetime, so that it doesn't get destructed before I return."

In your program, there is no real difference between vec_mul_no_ref and vec_mul_ref_args. But imagine someone else called those functions differently. In particular, suppose I called vec_mul_ref_args(x, y) and somehow x and y get destructed mid-call in another thread. This would be a data race. However if I had called vec_mul_no_ref instead, it would be fine.

like image 40
jcai Avatar answered Nov 01 '25 15:11

jcai



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!