【问题标题】:Why is my Rcpp implementation for finding the number of unique items slower than base R?为什么我的 Rcpp 实现查找唯一项目的数量比基本 R 慢?
【发布时间】:2017-07-11 02:50:45
【问题描述】:

我正在尝试编写一个函数来计算字符串向量中唯一项的数量(我的问题稍微复杂一些,但这是可重现的。我是根据我为 C++ 找到的答案做的。这是我的代码:

C++

int unique_sort(vector<string> x) {
    sort(x.begin(), x.end());
    return unique(x.begin(), x.end()) - x.begin();
}

int unique_set(vector<string> x) {
    unordered_set<string> tab(x.begin(), x.end());
    return tab.size();
}

R:

x <- paste0("x", sample(1:1e5, 1e7, replace=T))
microbenchmark(length(unique(x)),unique_sort(x), unique_set(x), times=3)

结果:

Unit: milliseconds
              expr        min         lq       mean     median         uq
 length(unique(x))   365.0213   373.4018   406.0209   381.7823   426.5206
    unique_sort(x) 10732.1918 10847.0532 10907.6882 10961.9146 10995.4363
     unique_set(x)  1948.6517  2230.3383  2334.4040  2512.0249  2527.2802

查看unique 函数的 R 源代码(有点难以理解),它似乎使用数组上的循环将唯一元素添加到散列,并检查该散列是否已经存在。

因此,我认为它应该等效于 unordered_set 方法。我不明白为什么 unordered_set 方法慢了 5 倍。

TLDR:为什么我的 C++ 代码很慢?

【问题讨论】:

  • 什么是“高度优化的编译代码”,如何实现? Rcpp 已经编译。也可以将其他函数编写为与已编译的 R 代码(例如,制表函数)的性能相同。我觉得我错过了一些慢 5 倍的算法。
  • Dirk 解释得更好here。我认为这是一个类似的情况。
  • 在那篇文章中,由于开销引起的差异以纳秒为单位,在这里差异为秒时并不特别相关。我可以向您展示其他 R 函数,其中差异远小于秒。
  • 我知道这并不能回答你关于为什么的问题,但这里有一个 Rcpp 函数,它比基本 R 快 5 倍:int unique_size(CharacterVector x) {return Rcpp::unique(x).size();}。我一直在查看源代码,但实际上找不到任何东西。确实很有趣。
  • 谢谢,非常感谢。希望有人能回答“为什么”的问题:)

标签: c++ r rcpp


【解决方案1】:

首先,请使示例可重现。以上缺少 Rcpp 属性、C++11 插件和必要的头文件导入。

其次,这里显示的问题是从 RC++ 的数据执行深度复制的成本结构体。基准测试中的大部分时间都花在了复制过程中。这个过程是通过选择使用std::vector&lt;std::string&gt; 而不是Rcpp::CharacterVector 来触发的,Rcpp::CharacterVector 包含SEXP、s 表达式或指向数据的指针。通过否定仅执行浅拷贝的 Rcpp 对象提供的代理模型,将数据导入 C++ 的直接成本将远大于本文所述的微秒Why is this simplistic cpp function version slower?

说了这么多,我们来谈谈如何修改上面的例子来使用Rcpp对象。首先,请注意,Rcpp 对象有一个名为 .sort() 的成员函数,它可以准确地对缺少值的 Rcpp::CharacterVector 进行排序(有关详细信息,请参阅 Rcpp FAQ Section 5.5: Sorting with STL on a CharacterVector produces problematic results,这假定没有大写或特殊语言环境)。其次,SEXP 类型可以用作构造std::unordered_set 的一种方式,即使数据作为Rcpp::CharacterVector 导入也是如此。这些修改可以在声明中带有“native”的C++函数中找到。

#include <Rcpp.h>
#include <unordered_set>
#include <algorithm>

// [[Rcpp::plugins(cpp11)]]

// [[Rcpp::export]]
int unique_sort(std::vector<std::string> x) {
  sort(x.begin(), x.end());
  return unique(x.begin(), x.end()) - x.begin();
}

// [[Rcpp::export]]
int unique_set(std::vector<std::string> x) {
  std::unordered_set<std::string> tab(x.begin(), x.end());
  return tab.size();
}

// [[Rcpp::export]]
int unique_sort_native(Rcpp::CharacterVector x) {
  x.sort();
  return std::unique(x.begin(), x.end()) - x.begin();
}

// [[Rcpp::export]]
int unique_set_native(Rcpp::CharacterVector x) {
  std::unordered_set<SEXP> tab(x.begin(), x.end());
  return tab.size();
}

测试代码:

# install.packages(c("microbenchmark"))

# Note, it is more efficient to supply an integer rather than a vector
# in sample()'s first parameter.
x <- paste0("x", sample(1e5, 1e7, replace=T))

# Run a microbenchmark
microbenchmark::microbenchmark(
  length(unique(x)),
  length(unique.default(x)),
  unique_sort(x),
  unique_set(x),
  unique_sort_native(x),
  unique_set_native(x),
  times = 10
)

输出:

Unit: milliseconds
                      expr     min      lq    mean  median      uq     max neval
         length(unique(x))   208.0   235.3   235.7   237.2   240.2   247.4    10
 length(unique.default(x))   230.9   232.8   238.8   233.7   241.8   266.6    10
            unique_sort(x) 12759.4 12877.1 12993.8 12920.1 13043.2 13416.7    10
             unique_set(x)  2528.1  2545.3  2590.1  2590.3  2631.3  2670.1    10
     unique_sort_native(x)  7452.6  7482.4  7568.5  7509.0  7563.6  7917.8    10
      unique_set_native(x)   175.8   176.9   179.2   178.3   182.3   183.4    10

因此,当使用 Rcpp 对象避免深度复制时,unique_set_native 函数比 length(unique()) 调用快大约 30 毫秒。

【讨论】:

  • 您能否在基准测试中添加一个直接调用 S3 方法而不是 S3 泛型的基本 R 方法?如果您避免方法分派,如果 Rcpp 仍然更快,那将会很有趣(我希望它不是)。
  • @Roland,在这种情况下,删除调度似乎没有太大作用。我增加了评估尝试的数量以进一步调查...潜在地,额外的时间差异可能归因于 R 中的length() 调用?
  • length 也是一个泛型。但这似乎并不能完全解释这种差异。嗯。
  • 不错的答案,再一次!
  • 谢谢,非常感谢您的解释和代码示例!
猜你喜欢
  • 2019-08-28
  • 1970-01-01
  • 2018-04-18
  • 2019-11-16
  • 1970-01-01
  • 2015-07-17
  • 2019-01-14
  • 2014-02-09
相关资源
最近更新 更多