R 中的算术在数字上比整数更快。这是怎么回事?
Arithmetic in R faster on numerics as opposed to integers. What's going on?
我正在将一些主要使用数字数据(即双精度(的代码转换为整数,并做了一个快速的基准测试,看看我获得了多少效率。
令我惊讶的是,它的速度变慢了...约20%。我以为我做错了什么,但原始代码只是中等大小向量上的一些基本算术运算,所以我知道事实并非如此。也许我的环境搞砸了?我重新开始,结果是一样的...整数效率较低。
这开始了一系列的测试和潜入兔子洞。这是我的第一个测试。我们使用基数 R 的sum
对一百万个元素求和。请注意,对于 R 版本3.5.0
,时序有很大不同,而在 v 3.5.1 中,时序大致相同(仍然不是人们期望的(:
set.seed(123)
int1e6 <- sample(1:10, 1e6, TRUE)
dbl1e6 <- runif(1e6, 1, 10)
head(int1e6)
# [1] 5 3 6 8 6 2
class(int1e6)
# [1] "integer"
head(dbl1e6)
# [1] 5.060628 2.291397 2.992889 5.299649 5.217105 9.769613
class(dbl1e6)
#[1] "numeric"
mean(dbl1e6)
# [1] 5.502034
mean(int1e6)
# [1] 5.505185
## R 3.5.0
library(microbenchmark)
microbenchmark(intSum = sum(int1e6), dblSum = sum(dbl1e6), times = 1000)
Unit: microseconds
expr min lq mean median uq max neval
intSum 1033.677 1043.991 1147.9711 1111.438 1200.725 2723.834 1000
dblSum 817.719 835.486 945.6553 890.529 998.946 2736.024 1000
## R 3.5.1
Unit: microseconds
expr min lq mean median uq max neval
intSum 836.243 877.7655 966.4443 950.1525 997.9025 2077.257 1000
dblSum 866.939 904.7945 1015.3445 986.4770 1046.4120 2541.828 1000
class(sum(int1e6))
# [1] "integer"
class(sum(dbl1e6))
#[1] "numeric"
从这里开始,版本 3.5.0 和 3.5.1 给出的结果几乎相同。
这是我们第一次潜入兔子洞。连同sum
的文档(见?sum
(,我们看到sum
只是一个通过standardGeneric
调度的泛型函数。深入挖掘,我们看到它最终在第516行调用R_execMethod
。这就是我迷路的地方。在我看来,就像R_execClosure
被称为下一个,然后是许多不同的可能分支。我认为标准路径是接下来打电话给eval
,但我不确定。我的猜测是,最终,在arithimetic.c中调用了一个函数,但我找不到任何专门对数字向量求和的东西。无论哪种方式,基于我对方法调度和一般C
的有限了解,我的天真假设是调用如下所示的函数:
template <typename T>
T sum(vector<T> x) {
T mySum = 0;
for (std::size_t i = 0; i < x.size(); ++i)
mySum += x[i];
return mySum;
}
我知道C
中没有函数重载或向量,但你明白我的意思。我的信念是,最终,一堆相同类型的元素被添加到相同类型的元素中并最终返回。在Rcpp
中,我们将有类似的东西:
template <typename typeReturn, typename typeRcpp>
typeReturn sumRcpp(typeRcpp x) {
typeReturn mySum = 0;
unsigned long int mySize = x.size();
for (std::size_t i = 0; i < mySize; ++i)
mySum += x[i];
return mySum;
}
// [[Rcpp::export]]
SEXP mySumTest(SEXP Rx) {
switch(TYPEOF(Rx)) {
case INTSXP: {
IntegerVector xInt = as<IntegerVector>(Rx);
int resInt = sumRcpp<int>(xInt);
return wrap(resInt);
}
case REALSXP: {
NumericVector xNum = as<NumericVector>(Rx);
double resDbl = sumRcpp<double>(xNum);
return wrap(resDbl);
}
default: {
Rcpp::stop("Only integers and numerics are supported");
}
}
}
基准测试证实了我对整数继承效率优势的正常思考:
microbenchmark(mySumTest(int1e6), mySumTest(dbl1e6))
Unit: microseconds
expr min lq mean median uq max neval
mySumTest(int1e6) 103.455 160.776 185.2529 180.2505 200.3245 326.950 100
mySumTest(dbl1e6) 1160.501 1166.032 1278.1622 1233.1575 1347.1660 1644.494 100
二元运算符
这让我进一步思考。也许只是围绕standardGeneric
的复杂性使不同的数据类型行为奇怪。所以,让我们跳过所有的爵士乐,直接进入二进制运算符(+, -, *, /, %/%
(
set.seed(321)
int1e6Two <- sample(1:10, 1e6, TRUE)
dbl1e6Two <- runif(1e6, 1, 10)
## addition
microbenchmark(intPlus = int1e6 + int1e6Two,
dblPlus = dbl1e6 + dbl1e6Two, times = 1000)
Unit: milliseconds
expr min lq mean median uq max neval
intPlus 2.531220 3.214673 3.970903 3.401631 3.668878 82.11871 1000
dblPlus 1.299004 2.045720 3.074367 2.139489 2.275697 69.89538 1000
## subtraction
microbenchmark(intSub = int1e6 - int1e6Two,
dblSub = dbl1e6 - dbl1e6Two, times = 1000)
Unit: milliseconds
expr min lq mean median uq max neval
intSub 2.280881 2.985491 3.748759 3.166262 3.379755 79.03561 1000
dblSub 1.302704 2.107817 3.252457 2.208293 2.382188 70.24451 1000
## multiplication
microbenchmark(intMult = int1e6 * int1e6Two,
dblMult = dbl1e6 * dbl1e6Two, times = 1000)
Unit: milliseconds
expr min lq mean median uq max neval
intMult 2.913680 3.573557 4.380174 3.772987 4.077219 74.95485 1000
dblMult 1.303688 2.020221 3.078500 2.119648 2.299145 10.86589 1000
## division
microbenchmark(intDiv = int1e6 %/% int1e6Two,
dblDiv = dbl1e6 / dbl1e6Two, times = 1000)
Unit: milliseconds
expr min lq mean median uq max neval
intDiv 2.892297 3.210666 3.720360 3.228242 3.373456 62.12020 1000
dblDiv 1.228171 1.809902 2.558428 1.842272 1.990067 64.82425 1000
这些类也被保留:
unique(c(class(int1e6 + int1e6Two), class(int1e6 - int1e6Two),
class(int1e6 * int1e6Two), class(int1e6 %/% int1e6Two)))
# [1] "integer"
unique(c(class(dbl1e6 + dbl1e6Two), class(dbl1e6 - dbl1e6Two),
class(dbl1e6 * dbl1e6Two), class(dbl1e6 / dbl1e6Two)))
# [1] "numeric"
在每种情况下,我们都看到算术在数值数据类型上快 40% - 70%。真正奇怪的是,当正在操作的两个向量相同时,我们得到更大的差异:
microbenchmark(intPlus = int1e6 + int1e6,
dblPlus = dbl1e6 + dbl1e6, times = 1000)
Unit: microseconds
expr min lq mean median uq max neval
intPlus 2522.774 3148.464 3894.723 3304.189 3531.310 73354.97 1000
dblPlus 977.892 1703.865 2710.602 1767.801 1886.648 77738.47 1000
microbenchmark(intSub = int1e6 - int1e6,
dblSub = dbl1e6 - dbl1e6, times = 1000)
Unit: microseconds
expr min lq mean median uq max neval
intSub 2236.225 2854.068 3467.062 2994.091 3214.953 11202.06 1000
dblSub 893.819 1658.032 2789.087 1730.981 1873.899 74034.62 1000
microbenchmark(intMult = int1e6 * int1e6,
dblMult = dbl1e6 * dbl1e6, times = 1000)
Unit: microseconds
expr min lq mean median uq max neval
intMult 2852.285 3476.700 4222.726 3658.599 3926.264 78026.18 1000
dblMult 973.640 1679.887 2638.551 1754.488 1875.058 10866.52 1000
microbenchmark(intDiv = int1e6 %/% int1e6,
dblDiv = dbl1e6 / dbl1e6, times = 1000)
Unit: microseconds
expr min lq mean median uq max neval
intDiv 2879.608 3355.015 4052.564 3531.762 3797.715 11781.39 1000
dblDiv 945.519 1627.203 2706.435 1701.512 1829.869 72215.51 1000
unique(c(class(int1e6 + int1e6), class(int1e6 - int1e6),
class(int1e6 * int1e6), class(int1e6 %/% int1e6)))
# [1] "integer"
unique(c(class(dbl1e6 + dbl1e6), class(dbl1e6 - dbl1e6),
class(dbl1e6 * dbl1e6), class(dbl1e6 / dbl1e6)))
# [1] "numeric"
对于每种操作员类型,这几乎增加了 100%!!
基数 R 中的常规 for 循环怎么样?
funInt <- function(v) {
mySumInt <- 0L
for (element in v)
mySumInt <- mySumInt + element
mySumInt
}
funDbl <- function(v) {
mySumDbl <- 0
for (element in v)
mySumDbl <- mySumDbl + element
mySumDbl
}
microbenchmark(funInt(int1e6), funDbl(dbl1e6))
Unit: milliseconds
expr min lq mean median uq max neval
funInt(int1e6) 25.44143 25.75075 26.81548 26.09486 27.60330 32.29436 100
funDbl(dbl1e6) 24.48309 24.82219 25.68922 25.13742 26.49816 29.36190 100
class(funInt(int1e6))
# [1] "integer"
class(funDbl(dbl1e6))
# [1] "numeric"
差异并不惊人,但人们仍然期望整数总和优于双倍和。我真的不知道该怎么想。
所以我的问题是:
为什么数值数据类型在基数 R 中的基本算术运算上优于整数数据类型?
编辑。忘了提这个:
sessionInfo()
R version 3.5.1 (2018-07-02)
Platform: x86_64-apple-darwin15.6.0 (64-bit)
Running under: macOS High Sierra 10.13.6
F.Privé在评论中的"随机猜测"真的很好!函数do_arith
似乎是arithmetic.c
的起点。首先,对于标量,我们看到REALSXP
的情况很简单:例如,使用标准+
。对于INTSXP
,例如,有一个调度到R_integer_plus
,它确实检查整数溢出:
static R_INLINE int R_integer_plus(int x, int y, Rboolean *pnaflag)
{
if (x == NA_INTEGER || y == NA_INTEGER)
return NA_INTEGER;
if (((y > 0) && (x > (R_INT_MAX - y))) ||
((y < 0) && (x < (R_INT_MIN - y)))) {
if (pnaflag != NULL)
*pnaflag = TRUE;
return NA_INTEGER;
}
return x + y;
}
其他二进制操作类似。对于向量,它也类似。在integer_binary
内,有一个调度到相同的方法,而在real_binary
中,标准操作在没有任何检查的情况下使用。
我们可以使用以下 Rcpp 代码看到这一点:
#include <Rcpp.h>
// [[Rcpp::plugins(cpp11)]]
#include <cstdint>
using namespace Rcpp;
// [[Rcpp::export]]
IntegerVector sumInt(IntegerVector a, IntegerVector b) {
IntegerVector result(no_init(a.size()));
std::transform(a.begin(), a.end(), b.begin(), result.begin(),
[] (int32_t x, int32_t y) {return x + y;});
return result;
}
// [[Rcpp::export]]
IntegerVector sumIntOverflow(IntegerVector a, IntegerVector b) {
IntegerVector result(no_init(a.size()));
std::transform(a.begin(), a.end(), b.begin(), result.begin(),
[] (int32_t x, int32_t y) {
if (x == NA_INTEGER || y == NA_INTEGER)
return NA_INTEGER;
if (((y > 0) && (x > (INT32_MAX - y))) ||
((y < 0) && (x < (INT32_MIN - y))))
return NA_INTEGER;
return x + y;
});
return result;
}
// [[Rcpp::export]]
NumericVector sumReal(NumericVector a, NumericVector b) {
NumericVector result(no_init(a.size()));
std::transform(a.begin(), a.end(), b.begin(), result.begin(),
[] (double x, double y) {return x + y;});
return result;
}
/*** R
set.seed(123)
int1e6 <- sample(1:10, 1e6, TRUE)
int1e6two <- sample(1:10, 1e6, TRUE)
dbl1e6 <- runif(1e6, 1, 10)
dbl1e6two <- runif(1e6, 1, 10)
microbenchmark::microbenchmark(int1e6 + int1e6two,
sumInt(int1e6, int1e6two),
sumIntOverflow(int1e6, int1e6two),
dbl1e6 + dbl1e6two,
sumReal(dbl1e6, dbl1e6two),
times = 1000)
*/
结果:
Unit: microseconds
expr min lq mean median uq max neval
int1e6 + int1e6two 1999.698 2046.2025 2232.785 2061.7625 2126.970 5461.816 1000
sumInt 812.560 846.1215 1128.826 861.9305 892.089 44723.313 1000
sumIntOverflow 1664.351 1690.2455 1901.472 1702.6100 1760.218 4868.182 1000
dbl1e6 + dbl1e6two 1444.172 1501.9100 1997.924 1526.0695 1641.103 47277.955 1000
sumReal 1459.224 1505.2715 1887.869 1530.5995 1675.594 5124.468 1000
在C++代码中引入溢出检查会导致性能显著降低。即使它没有标准+
那么糟糕.因此,如果您知道您的整数"表现良好",您可以通过跳过 R 的错误检查直接转到 C/C++ 来获得相当多的性能。这让我想起了另一个具有类似结论的问题。R 完成的错误检查可能代价高昂。
对于具有相同向量的情况,我得到以下基准测试结果:
Unit: microseconds
expr min lq mean median uq max neval
int1e6 + int1e6 1761.285 2000.720 2191.541 2011.5710 2029.528 47397.029 1000
sumInt 648.151 761.787 1002.662 767.9885 780.129 46673.632 1000
sumIntOverflow 1408.109 1647.926 1835.325 1655.6705 1670.495 44958.840 1000
dbl1e6 + dbl1e6 1081.079 1119.923 1443.582 1137.8360 1173.807 44469.509 1000
sumReal 1076.791 1118.538 1456.917 1137.2025 1250.850 5141.558 1000
双打(R和C++(的性能显着提高。对于整数,性能也有一些提高,但不像双精度值那样容易抓住。
- 使用递归的数组的最小值.这是怎么回事
- 我不小心调用了一个没有自己类对象的成员函数.但这是怎么回事呢
- 我使用 OpenMP 的线程越多,执行时间就越长,这是怎么回事?
- 绕道重铸瓷砖层是怎么回事
- 架 (U+67B6) 与en_US不符.UTF-8.这是怎么回事?
- R 中的算术在数字上比整数更快。这是怎么回事?
- 我的代码运行良好,但现在当尝试将其制作成模板时,我遇到了许多看似无关的错误。这是怎么回事?
- 在变量名后声明带有 () 的非内部类型与不使用变量名的行为不同。即 std::map<int,char>x(); - 这是怎么回事?
- 弄清楚这段代码是怎么回事(递归下降案例研究)
- 分配器密集和稀疏是怎么回事?
- 传递引用C++,指针的向量.这是怎么回事?
- __vector_base_common是怎么回事?
- (对象)不命名类型.怎么回事
- 变量已在 .obj 中定义;这是怎么回事?
- Sublime上的EasyClangComplete是怎么回事?
- 重复输出的类型是怎么回事,它如何区分迭代次数和 Ascii 代码?
- 标准::地图使用.这是怎么回事?核心转储?我做得不正确?
- llvm libcxx的源代码是怎么回事?
- 这一行是怎么回事: istringstream is( line );
- 按位运算符和整数提升是怎么回事