为什么我的 Rcpp 实现查找唯一项的数量比基本 R 慢?
Why is my Rcpp implementation for finding the number of unique items slower than base R?
我正在尝试编写一个函数来计算字符串向量中唯一项的数量(我的问题稍微复杂一些,但这是可重现的。我是根据我为 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();
}
回复:
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 属性、C++11 插件和必要的 header 导入。
其次,这里显示的问题是对 R 中的数据执行 深度复制 到C++ 结构。基准测试中的大部分时间都花在了复制过程中。此过程是由选择使用 std::vector<std::string>
而不是 Rcpp::CharacterVector
触发的,后者保存 SEXP
、s-expression 或指向数据的指针。通过否定 Rcpp objects 提供的代理模型,它只执行 浅拷贝 ,将数据导入 C++[=45= 会立即产生成本] 比 Why is this simplistic cpp function version slower? 中描述的仅仅微秒大得多。
说了这么多,我们来说说如何修改上面的例子来使用Rcppobjects。首先,请注意 Rcpp objects 有一个名为 .sort()
的成员函数,它可以准确地对具有缺失值的 Rcpp::CharacterVector
进行排序(有关详细信息,请参阅 Rcpp FAQ Section 5.5: Sorting with STL on a CharacterVector produces problematic results,这假定没有大写或特殊语言环境)。其次,SEXP
类型可用作构造 std::unordered_set
的一种方式,即使数据作为 Rcpp::CharacterVector
导入也是如此。这些修改可以在 C++ 函数中找到,在它们的声明中带有 "native"。
#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 objects 避免 深度复制 时,unique_set_native
函数击败 length(unique())
调用大约 30毫秒。
我正在尝试编写一个函数来计算字符串向量中唯一项的数量(我的问题稍微复杂一些,但这是可重现的。我是根据我为 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();
}
回复:
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 属性、C++11 插件和必要的 header 导入。
其次,这里显示的问题是对 R 中的数据执行 深度复制 到C++ 结构。基准测试中的大部分时间都花在了复制过程中。此过程是由选择使用 std::vector<std::string>
而不是 Rcpp::CharacterVector
触发的,后者保存 SEXP
、s-expression 或指向数据的指针。通过否定 Rcpp objects 提供的代理模型,它只执行 浅拷贝 ,将数据导入 C++[=45= 会立即产生成本] 比 Why is this simplistic cpp function version slower? 中描述的仅仅微秒大得多。
说了这么多,我们来说说如何修改上面的例子来使用Rcppobjects。首先,请注意 Rcpp objects 有一个名为 .sort()
的成员函数,它可以准确地对具有缺失值的 Rcpp::CharacterVector
进行排序(有关详细信息,请参阅 Rcpp FAQ Section 5.5: Sorting with STL on a CharacterVector produces problematic results,这假定没有大写或特殊语言环境)。其次,SEXP
类型可用作构造 std::unordered_set
的一种方式,即使数据作为 Rcpp::CharacterVector
导入也是如此。这些修改可以在 C++ 函数中找到,在它们的声明中带有 "native"。
#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 objects 避免 深度复制 时,unique_set_native
函数击败 length(unique())
调用大约 30毫秒。