r-lib / cpp11

cpp11 helps you to interact with R objects using C++ code.
https://cpp11.r-lib.org/
Other
199 stars 46 forks source link

Move constructor of writable vectors seems expensive #200

Closed renkun-ken closed 3 years ago

renkun-ken commented 3 years ago

I'm experimenting with some approaches of in-place modification of vectors decribed in #42. The following are examples where fun1 uses R's C API and fun3 uses std::move() (as suggested in https://github.com/r-lib/cpp11/issues/42#issuecomment-654385362) while fun2 copies x in the beginning and thus does not modify x in place.

#include <cpp11.hpp>
using namespace cpp11;

[[cpp11::register]] void fun1(doubles x)
{
  double* y = REAL(x.data());
  for (int i = 0; i < x.size(); i++)
  {
    y[i] = i;
  }
}

[[cpp11::register]] void fun2(writable::doubles x)
{
  double *y = REAL(x.data());
  for (int i = 0; i < x.size(); i++)
  {
    y[i] = i;
  }
}

[[cpp11::register]] void fun3(doubles x)
{
  writable::doubles y(std::move(x.data()));
  for (int i = 0; i < x.size(); i++)
  {
    y[i] = i;
  }
}

I expect fun1 and fun3 to have similar performance and should be both faster than fun2. But the following benchmark shows that the move constructor seems quite expensive:

x <- rnorm(10000000)
microbenchmark::microbenchmark(
  fun1 = fun1(x),
  fun2 = fun2(x),
  fun3 = fun3(x)
)
Unit: milliseconds
 expr       min        lq      mean    median        uq       max neval
 fun1  25.21588  25.63050  28.48000  25.89659  26.46729  79.08249   100
 fun2  64.18995  74.34135  80.80762  78.37686  82.51549 165.41478   100
 fun3 135.21189 136.54212 144.82135 137.66079 140.11925 264.22990   100

I wonder if it is an expected result?

jimhester commented 3 years ago

You aren't benchmarking the move constructor, you are benchmarking the proxy object, altrep support and unwind protecting.

e.g. see foo4, which is the equivalent if you were trying to benchmark the move constructor.

#include <cpp11.hpp>
using namespace cpp11;

[[cpp11::register]] void fun1(doubles x) {
  double* y = REAL(x.data());
  for (int i = 0; i < x.size(); i++) {
    y[i] = i;
  }
}

[[cpp11::register]] void fun2(writable::doubles x) {
  double* y = REAL(x.data());
  for (int i = 0; i < x.size(); i++) {
    y[i] = i;
  }
}

[[cpp11::register]] void fun3(doubles x) {
  writable::doubles y(std::move(x.data()));
  for (int i = 0; i < x.size(); i++) {
    y[i] = i;
  }
}

[[cpp11::register]] void fun4(doubles x) {
  writable::doubles y(std::move(x.data()));
  double* y_p = REAL(y);
  for (int i = 0; i < x.size(); i++) {
    y_p[i] = i;
  }
}
cpp11::cpp_source("~/p/cpp11/test.cpp")

x <- rnorm(10000000)
bench::mark(
  fun1(x),
  fun2(x),
  fun3(x),
  fun4(x)
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 4 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 fun1(x)      4.99ms   5.39ms     177.         0B      0  
#> 2 fun2(x)     27.59ms  29.23ms      34.2    76.3MB     34.2
#> 3 fun3(x)      9.33ms   9.98ms      96.1        0B      0  
#> 4 fun4(x)       4.9ms   5.27ms     182.         0B      0

Created on 2021-07-01 by the reprex package (v2.0.0)

renkun-ken commented 3 years ago

Thanks for pointing it out!

Is it conclusive that if I don't use R's C API, the performance of in-place modifying a vector will inevitably drop to 50% in this case?