@F.Prive 指出,从头到尾命名一个对象需要时间,将向量转换为字符非常昂贵。一般来说,命名一个对象更多的是为了方便而不是
必要性,当“需要速度”的用户面临选择时,他们会很乐意放弃这种便利。这就是为什么一些开发人员提供返回命名对象的选项。
尽管如此,还有其他用于命名对象的选项,所以让我们看看它们中的任何一个是否可以加快进程。
选项Base R:
有时,base R 中非常基本的功能,例如names,可能非常高效。
// [[Rcpp::export]]
Rcpp::NumericVector test_namesBaseNameCpp(int N){
Rcpp::RNGScope scope;
return Rcpp::runif(N, 1, 100);
}
## in base R define the following:
test_namesBaseNameR <- function(n, named) {
v <- test_namesBaseNameCpp(N = n)
if (named)
names(v) <- 1:n
v
}
结果如下:
library(microbenchmark)
microbenchmark(OP = test_names(10^5, T),
baseR = test_namesBaseNameR(10^5, T),
unit = "relative")
Unit: relative
expr min lq mean median uq max neval
OP 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 100
baseR 1.008021 1.001896 1.015566 1.001912 1.016359 1.101542 100
几乎一样。
选项将名称作为参数传递:
让我们看看将名称作为参数传递是否有帮助。
// [[Rcpp::export]]
Rcpp::NumericVector test_namesPassChar(int N, bool name,
Rcpp::CharacterVector RNames) {
Rcpp::RNGScope scope;
Rcpp::NumericVector data = Rcpp::runif(N, 1, 100);
if(name)data.attr("names")=RNames;
return data;
}
microbenchmark(OP = test_names(10^5, T),
passAndConvert = test_namesPassChar(10^5, T, 1:10^5),
passAndConvert2 = test_namesPassChar(10^5, T, as.character(1:10^5)),
unit = "relative")
Unit: relative
expr min lq mean median uq max neval
OP 0.9878606 1.0015865 0.9820272 1.000169 0.9471021 0.8645719 100
passAndConvert 1.0000000 1.0000000 1.0000000 1.000000 1.0000000 1.0000000 100
passAndConvert2 1.0085636 0.9998202 1.0332923 1.000702 1.0517044 1.0507859 100
这里也没有运气。
看来,如果我们真的想加快速度,我们将不得不预先定义一个名称向量,并根据需要将该向量子集。例如,在您的项目中,如果您知道您想要的命名对象的最大大小是maxLength,我们可以定义myNames <- as.character(1:maxLength)。现在在实践中,当我们通过N 时,我们也通过myNames[1:N]。
选项预制名称:
maxLength <- 10^7
myNames <- as.character(1:maxLength) ## this steps takes a while, but you only do it once
microbenchmark(OP = test_names(10^5, T),
passPreMade = test_namesPassChar(10^5, T, myNames[1:10^5]),
unit = "relative")
Unit: relative
expr min lq mean median uq max neval
OP 8.149265 6.321411 6.617447 6.097933 6.042956 31.8545 100
passPreMade 1.000000 1.000000 1.000000 1.000000 1.000000 1.0000 100
这是一个很好的改进,但它仍然不是那么理想,因为它不是通用的。
最后,我极力反对有一个选项,我之所以提及它只是为了向您展示为什么Rcpp 如此出色。我们转向C,我们利用递增整数的结构来获得我们的收益。 SET_STRING_ELT 需要 chars 的数组,通常,我们会通过 temp = std::to_string(i) 将每个整数 i 转换为字符串,然后使用 .c_str() 函数将 temp 转换为字符,如下所示myChar = temp.c_str()。这再次与前几个方法一样慢,因为我们将每个整数从头开始转换为chars 的数组。在这种情况下,好消息是我们不需要这样做,因为我们只是用序列1:n 命名我们的向量。因此,对于 90% 的整数,只有 1 位数字在变化(即个位数字)。牢记这一点,我们可以做这样的事情(注意,这肯定可以改进,因为我的 C 技能不强):
我不推荐的选项
#include <R.h>
#include "Rinternals.h"
// [[Rcpp::export]]
SEXP test_namesSuperHard(int N, bool name) {
Rcpp::RNGScope scope;
Rcpp::NumericVector data = Rcpp::runif(N, 1, 100);
if (name) {
SEXP myNames = PROTECT(Rf_allocVector(STRSXP, N));
int base = (int) log10(N) + 1;
char *myChar;
myChar = (char *) malloc(base * sizeof(char));
int count = 1, index = base - 1;
for (std::size_t i = 0; i < base; i++)
myChar[i] = '0';
for (std::size_t i = 0; i < N; i++, count++) {
if ((count % 10) == 0) {
while (myChar[index] == '9') {
myChar[index] = '0';
index--;
}
count = 0;
myChar[index]++;
index = base - 1;
} else {
myChar[index] = count + '0';
}
SET_STRING_ELT(myNames, i, Rf_mkChar(myChar));
}
Rf_setAttrib(data, R_NamesSymbol, myNames);
UNPROTECT(1);
}
return data;
}
测试输出:
test_namesSuperHard(20, TRUE)
01 02 03 04 05
13.417850 29.633416 35.221770 17.377710 97.139458
06 07 08 09 10
24.187230 60.962993 23.307580 61.151013 12.892655
11 12 13 14 15
48.303439 28.875226 37.264403 78.196955 29.705689
16 17 18 19 20
3.533349 86.505015 62.784809 95.785053 94.273097
现在我们进行基准测试:
microbenchmark(OP = test_names(10^5, T),
dontRecommend = test_namesSuperHard(10^5, T),
unit = "relative")
Unit: relative
expr min lq mean median uq max neval
OP 3.753499 3.702676 3.885055 3.691977 3.504271 8.883482 100
dontRecommend 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 100
我们看到几乎4x 的改进速度一样快。不错。但是,正如我在开篇中所说的那样,这是为了方便而做的很多工作。它还展示了Rcpp 团队(Dirk 等人)在使这些便利轻松实施方面所做的真正令人惊叹的工作。