为什么标准R中值函数比简单的C ++替代品慢得多?

时间:2016-01-13 15:50:41

标签: c++ r performance rcpp microbenchmark

我在a.SelectMany(_ => b, combine.Compile());中对中位数进行了以下实施,并通过C++R中使用了它:

Rcpp

如果我随后将性能与标准内置R中值函数进行比较,我会通过// [[Rcpp::export]] double median2(std::vector<double> x){ double median; size_t size = x.size(); sort(x.begin(), x.end()); if (size % 2 == 0){ median = (x[size / 2 - 1] + x[size / 2]) / 2.0; } else { median = x[size / 2]; } return median; } 获得以下结果

microbenchmark

为什么标准中位数功能要慢得多?这不是我期望的......

4 个答案:

答案 0 :(得分:13)

正如@joran所指出的,你的代码是非常专业的,一般而言,不那么通用的函数,算法等......通常更具性能。看看median.default

median.default
# function (x, na.rm = FALSE) 
# {
#   if (is.factor(x) || is.data.frame(x)) 
#     stop("need numeric data")
#   if (length(names(x))) 
#     names(x) <- NULL
#   if (na.rm) 
#     x <- x[!is.na(x)]
#   else if (any(is.na(x))) 
#     return(x[FALSE][NA])
#   n <- length(x)
#   if (n == 0L) 
#     return(x[FALSE][NA])
#   half <- (n + 1L)%/%2L
#   if (n%%2L == 1L) 
#     sort(x, partial = half)[half]
#   else mean(sort(x, partial = half + 0L:1L)[half + 0L:1L])
# }

有几种操作可以容纳缺失值的可能性,这些肯定会影响函数的整体执行时间。由于您的函数不会复制此行为,因此可以消除一堆计算,但因此不会为缺少值的向量提供相同的结果:

median(c(1, 2, NA))
#[1] NA

median2(c(1, 2, NA))
#[1] 2

其他一些因素可能没有像处理NA那样效果,但值得指出:

  • median以及它使用的一些函数是S3泛型,因此在方法调度上花费了少量时间
  • median不仅可以使用整数和数字向量;它还将处理DatePOSIXt,可能还有许多其他类,并正确保存属性:
median(Sys.Date() + 0:4)
#[1] "2016-01-15"

median(Sys.time() + (0:4) * 3600 * 24)
#[1] "2016-01-15 11:14:31 EST"

修改 我应该提到下面的函数将导致原始向量被排序,因为NumericVector是代理对象。如果您想避免这种情况,您可以Rcpp::clone输入向量并对克隆进行操作,或者使用原始签名(使用std::vector<double>),这需要在{SEXP的转换中隐式复制1}}到std::vector

另请注意,使用NumericVector代替std::vector<double>可以节省更多时间:

#include <Rcpp.h>

// [[Rcpp::export]]
double cpp_med(Rcpp::NumericVector x){
  std::size_t size = x.size();
  std::sort(x.begin(), x.end());
  if (size  % 2 == 0) return (x[size / 2 - 1] + x[size / 2]) / 2.0;
  return x[size / 2];
}
microbenchmark::microbenchmark(
  median(x),
  median2(x),
  cpp_med(x),
  times = 200L
)
# Unit: microseconds
#       expr    min      lq      mean  median      uq     max neval
#  median(x) 74.787 81.6485 110.09870 92.5665 129.757 293.810   200
# median2(x)  6.474  7.9665  13.90126 11.0570  14.844 151.817   200
# cpp_med(x)  5.737  7.4285  11.25318  9.0270  13.405  52.184   200

Yakk在上述评论中提出了一个很好的观点 - Jerry Coffin也详细阐述了关于完成这种做法的低效率。这是使用std::nth_element重写,以更大的向量为基准:

#include <Rcpp.h>

// [[Rcpp::export]]
double cpp_med2(Rcpp::NumericVector xx) {
  Rcpp::NumericVector x = Rcpp::clone(xx);
  std::size_t n = x.size() / 2;
  std::nth_element(x.begin(), x.begin() + n, x.end());

  if (x.size() % 2) return x[n]; 
  return (x[n] + *std::max_element(x.begin(), x.begin() + n)) / 2.;
}
set.seed(123)
xx <- rnorm(10e5)

all.equal(cpp_med2(xx), median(xx))
all.equal(median2(xx), median(xx))

microbenchmark::microbenchmark(
  cpp_med2(xx), median2(xx), 
  median(xx), times = 200L
)
# Unit: milliseconds
#         expr      min       lq     mean   median       uq       max neval
# cpp_med2(xx) 10.89060 11.34894 13.15313 12.72861 13.56161  33.92103   200
#  median2(xx) 84.29518 85.47184 88.57361 86.05363 87.70065 228.07301   200
#   median(xx) 46.18976 48.36627 58.77436 49.31659 53.46830 250.66939   200

答案 1 :(得分:2)

[这是一个扩展的评论,而不是你实际问过的问题的答案。]

即使您的代码可能会有明显的改进。特别是,即使您只关心一个或两个元素,也要对整个输入进行排序。

您可以使用std::nth_element代替std::sort将其从O(n log n)更改为O(n)。在偶数个元素的情况下,您通常希望使用std::nth_element在中间之前找到元素,然后使用std::min_element来查找紧接着的元素 - 但{{{ 1}}也对输入项进行分区,因此std::nth_element只需要在std::min_element之后的中间项目上运行,而不是整个输入数组。也就是说,在nth_element之后,你会遇到这样的情况:

enter image description here

nth_element的复杂度是&#34;平均线性&#34;和(当然)std::nth_element也是线性的,因此总体复杂度是线性的。

因此,对于简单的情况(奇数个元素),你会得到类似的东西:

std::min_element

...对于更复杂的情况(偶数个元素):

auto pos = x.begin() + x.size()/2;

std::nth_element(x.begin(), pos, x.end());
return *pos;

答案 2 :(得分:0)

我不确定&#34;标准&#34;您将参考的实施。

无论如何:如果有一个,它将作为标准库的一部分,当然不允许改变向量中元素的顺序(正如你的实现所做的那样),所以它肯定要在副本上工作。

创建此副本需要时间和CPU(以及大量内存),这会影响运行时间。

答案 3 :(得分:0)

here 可以预期 max_element( ForwardIt first, ForwardIt last ) 提供从头到尾的最大值,但是通过执行:return (x[n] + *std::max_element(x.begin(), x.begin() + n)) / 2. x.begin() + n 元素似乎被排除在计算之外。为什么会有这种差异?

例如cpp_med2({6, 2, 1, 5, 3, 4}) 产生 x={2, 1, 3, 4, 5, 6},其中:

n = 3
*x[n] = 4
*x.begin() = 2
*(x.begin() + n) = 4
*std::max_element(x.begin(), x.begin() + n) = 3

因此 cpp_med2({6, 2, 1, 5, 3, 4}) 返回 (4+3)/2=3.5,这是正确的中位数。 但是为什么 *std::max_element(x.begin(), x.begin() + n) 等于 3 而不是 4?该函数实际上似乎排除了最大值计算中的最后一个元素 (4)。

已解决(我认为):在:

<块引用>

在[first, last)范围内找到最大的元素

) 结束表示最后被排除在计算之外。对吗?

最好的问候