だから私たちはすべてのRの新しいユーザーに「apply
がベクトル化されていないので、Patrick Burnsをチェックしてください R Inferno Circle 4 " (引用):
一般的な反射は、適用ファミリで関数を使用することです。 これはnotベクトル化ではなく、ループ非表示です。適用関数の定義にはforループがあります。 lapply関数はループを埋めますが、実行時間は明示的なforループとほぼ同じになる傾向があります。
実際、apply
ソースコードをざっと見てみると、ループが明らかになっています。
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
これまでのところ、OKですが、lapply
またはvapply
を見ると、実際にはまったく異なる画像が明らかになっています。
lapply
## function (X, FUN, ...)
## {
## FUN <- match.fun(FUN)
## if (!is.vector(X) || is.object(X))
## X <- as.list(X)
## .Internal(lapply(X, FUN))
## }
## <bytecode: 0x000000000284b618>
## <environment: namespace:base>
したがって、そこに隠れているR for
ループはないようで、むしろ内部のC記述関数を呼び出しています。
rabbithole をざっと見てみると、ほぼ同じ画像が明らかになります。
さらに、例えばcolMeans
関数を見てみましょう。これはベクトル化されていないと非難されませんでした
colMeans
# function (x, na.rm = FALSE, dims = 1L)
# {
# if (is.data.frame(x))
# x <- as.matrix(x)
# if (!is.array(x) || length(dn <- dim(x)) < 2L)
# stop("'x' must be an array of at least two dimensions")
# if (dims < 1L || dims > length(dn) - 1L)
# stop("invalid 'dims'")
# n <- prod(dn[1L:dims])
# dn <- dn[-(1L:dims)]
# z <- if (is.complex(x))
# .Internal(colMeans(Re(x), n, prod(dn), na.rm)) + (0+1i) *
# .Internal(colMeans(Im(x), n, prod(dn), na.rm))
# else .Internal(colMeans(x, n, prod(dn), na.rm))
# if (length(dn) > 1L) {
# dim(z) <- dn
# dimnames(z) <- dimnames(x)[-(1L:dims)]
# }
# else names(z) <- dimnames(x)[[dims + 1]]
# z
# }
# <bytecode: 0x0000000008f89d20>
# <environment: namespace:base>
え?また、.Internal(colMeans(...
を呼び出します。これは、 ウサギの穴 にもあります。では、これは.Internal(lapply(..
とどう違うのですか?
実際、簡単なベンチマークでは、sapply
のパフォーマンスはcolMeans
より悪くなく、ビッグデータセットのfor
ループよりもはるかに優れていることがわかります。
m <- as.data.frame(matrix(1:1e7, ncol = 1e5))
system.time(colMeans(m))
# user system elapsed
# 1.69 0.03 1.73
system.time(sapply(m, mean))
# user system elapsed
# 1.50 0.03 1.60
system.time(apply(m, 2, mean))
# user system elapsed
# 3.84 0.03 3.90
system.time(for(i in 1:ncol(m)) mean(m[, i]))
# user system elapsed
# 13.78 0.01 13.93
言い換えれば、lapply
とvapply
が実際にvectorised(apply
これはfor
を呼び出すlapply
ループであり、Patrick Burnsが本当に言ったことは何ですか?
まず、あなたの例では、colMeans
、apply
、"[.data.frame"
にオーバーヘッドがあるため、フェアではない "data.frame"でテストを行います。
system.time(as.matrix(m)) #called by `colMeans` and `apply`
# user system elapsed
# 1.03 0.00 1.05
system.time(for(i in 1:ncol(m)) m[, i]) #in the `for` loop
# user system elapsed
# 12.93 0.01 13.07
マトリックスでは、画像は少し異なります。
mm = as.matrix(m)
system.time(colMeans(mm))
# user system elapsed
# 0.01 0.00 0.01
system.time(apply(mm, 2, mean))
# user system elapsed
# 1.48 0.03 1.53
system.time(for(i in 1:ncol(mm)) mean(mm[, i]))
# user system elapsed
# 1.22 0.00 1.21
質問の主な部分に関して、lapply
/mapply
/etcと単純なRループの主な違いは、ループが行われる場所です。 Rolandが指摘しているように、CループとRループの両方で、最もコストのかかる各反復でR関数を評価する必要があります。本当に高速なC関数は、Cですべてを実行する関数です。したがって、これが「ベクトル化」とは何でしょうか。
「リスト」要素のそれぞれで平均を見つける例:
(EDIT May 11 '16:「平均」を見つける例は、R関数の反復評価とコンパイルされたコードの違いの良いセットアップではないと思います、(1)単純なsum(x) / length(x)
に対する「数値」に対するRの平均アルゴリズムの特殊性のため、および(2)length(x) >> lengths(x)
を使用した「リスト」に対するテストの方が意味があります。したがって、「平均」の例は最後に移動され、別の例に置き換えられます。)
簡単な例として、「リスト」の各length == 1
要素の反対の発見を検討できます。
tmp.c
ファイル内:
#include <R.h>
#define USE_RINTERNALS
#include <Rinternals.h>
#include <Rdefines.h>
/* call a C function inside another */
double oppC(double x) { return(ISNAN(x) ? NA_REAL : -x); }
SEXP sapply_oppC(SEXP x)
{
SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x)));
for(int i = 0; i < LENGTH(x); i++)
REAL(ans)[i] = oppC(REAL(VECTOR_ELT(x, i))[0]);
UNPROTECT(1);
return(ans);
}
/* call an R function inside a C function;
* will be used with 'f' as a closure and as a builtin */
SEXP sapply_oppR(SEXP x, SEXP f)
{
SEXP call = PROTECT(allocVector(LANGSXP, 2));
SETCAR(call, install(CHAR(STRING_ELT(f, 0))));
SEXP ans = PROTECT(allocVector(REALSXP, LENGTH(x)));
for(int i = 0; i < LENGTH(x); i++) {
SETCADR(call, VECTOR_ELT(x, i));
REAL(ans)[i] = REAL(eval(call, R_GlobalEnv))[0];
}
UNPROTECT(2);
return(ans);
}
そしてR側で:
system("R CMD SHLIB /home/~/tmp.c")
dyn.load("/home/~/tmp.so")
データあり:
set.seed(007)
myls = rep_len(as.list(c(NA, runif(3))), 1e7)
#a closure wrapper of `-`
oppR = function(x) -x
for_oppR = compiler::cmpfun(function(x, f)
{
f = match.fun(f)
ans = numeric(length(x))
for(i in seq_along(x)) ans[[i]] = f(x[[i]])
return(ans)
})
ベンチマーク:
#call a C function iteratively
system.time({ sapplyC = .Call("sapply_oppC", myls) })
# user system elapsed
# 0.048 0.000 0.047
#evaluate an R closure iteratively
system.time({ sapplyRC = .Call("sapply_oppR", myls, "oppR") })
# user system elapsed
# 3.348 0.000 3.358
#evaluate an R builtin iteratively
system.time({ sapplyRCprim = .Call("sapply_oppR", myls, "-") })
# user system elapsed
# 0.652 0.000 0.653
#loop with a R closure
system.time({ forR = for_oppR(myls, "oppR") })
# user system elapsed
# 4.396 0.000 4.409
#loop with an R builtin
system.time({ forRprim = for_oppR(myls, "-") })
# user system elapsed
# 1.908 0.000 1.913
#for reference and testing
system.time({ sapplyR = unlist(lapply(myls, oppR)) })
# user system elapsed
# 7.080 0.068 7.170
system.time({ sapplyRprim = unlist(lapply(myls, `-`)) })
# user system elapsed
# 3.524 0.064 3.598
all.equal(sapplyR, sapplyRprim)
#[1] TRUE
all.equal(sapplyR, sapplyC)
#[1] TRUE
all.equal(sapplyR, sapplyRC)
#[1] TRUE
all.equal(sapplyR, sapplyRCprim)
#[1] TRUE
all.equal(sapplyR, forR)
#[1] TRUE
all.equal(sapplyR, forRprim)
#[1] TRUE
(平均値発見の元の例に従う):
#all computations in C
all_C = inline::cfunction(sig = c(R_ls = "list"), body = '
SEXP tmp, ans;
PROTECT(ans = allocVector(REALSXP, LENGTH(R_ls)));
double *ptmp, *pans = REAL(ans);
for(int i = 0; i < LENGTH(R_ls); i++) {
pans[i] = 0.0;
PROTECT(tmp = coerceVector(VECTOR_ELT(R_ls, i), REALSXP));
ptmp = REAL(tmp);
for(int j = 0; j < LENGTH(tmp); j++) pans[i] += ptmp[j];
pans[i] /= LENGTH(tmp);
UNPROTECT(1);
}
UNPROTECT(1);
return(ans);
')
#a very simple `lapply(x, mean)`
C_and_R = inline::cfunction(sig = c(R_ls = "list"), body = '
SEXP call, ans, ret;
PROTECT(call = allocList(2));
SET_TYPEOF(call, LANGSXP);
SETCAR(call, install("mean"));
PROTECT(ans = allocVector(VECSXP, LENGTH(R_ls)));
PROTECT(ret = allocVector(REALSXP, LENGTH(ans)));
for(int i = 0; i < LENGTH(R_ls); i++) {
SETCADR(call, VECTOR_ELT(R_ls, i));
SET_VECTOR_ELT(ans, i, eval(call, R_GlobalEnv));
}
double *pret = REAL(ret);
for(int i = 0; i < LENGTH(ans); i++) pret[i] = REAL(VECTOR_ELT(ans, i))[0];
UNPROTECT(3);
return(ret);
')
R_lapply = function(x) unlist(lapply(x, mean))
R_loop = function(x)
{
ans = numeric(length(x))
for(i in seq_along(x)) ans[i] = mean(x[[i]])
return(ans)
}
R_loopcmp = compiler::cmpfun(R_loop)
set.seed(007); myls = replicate(1e4, runif(1e3), simplify = FALSE)
all.equal(all_C(myls), C_and_R(myls))
#[1] TRUE
all.equal(all_C(myls), R_lapply(myls))
#[1] TRUE
all.equal(all_C(myls), R_loop(myls))
#[1] TRUE
all.equal(all_C(myls), R_loopcmp(myls))
#[1] TRUE
microbenchmark::microbenchmark(all_C(myls),
C_and_R(myls),
R_lapply(myls),
R_loop(myls),
R_loopcmp(myls),
times = 15)
#Unit: milliseconds
# expr min lq median uq max neval
# all_C(myls) 37.29183 38.19107 38.69359 39.58083 41.3861 15
# C_and_R(myls) 117.21457 123.22044 124.58148 130.85513 169.6822 15
# R_lapply(myls) 98.48009 103.80717 106.55519 109.54890 116.3150 15
# R_loop(myls) 122.40367 130.85061 132.61378 138.53664 178.5128 15
# R_loopcmp(myls) 105.63228 111.38340 112.16781 115.68909 128.1976 15
私にとって、ベクトル化とは主に、コードを記述しやすく理解しやすくすることです。
ベクトル化された関数の目標は、forループに関連する簿記をなくすことです。たとえば、次の代わりに:
means <- numeric(length(mtcars))
for (i in seq_along(mtcars)) {
means[i] <- mean(mtcars[[i]])
}
sds <- numeric(length(mtcars))
for (i in seq_along(mtcars)) {
sds[i] <- sd(mtcars[[i]])
}
あなたは書ける:
means <- vapply(mtcars, mean, numeric(1))
sds <- vapply(mtcars, sd, numeric(1))
これにより、同じもの(入力データ)と異なるもの(適用している関数)を簡単に確認できます。
ベクトル化の2番目の利点は、forループがRではなくCで記述されることが多いことです。これにより、パフォーマンスが大幅に向上しますが、ベクトル化の重要な特性とは思いません。ベクトル化とは基本的に、コンピューターの作業を保存するのではなく、脳を保存することです。
Patrick Burnsの見解では、loop hidingであり、code vectorisationではないという意見に同意します。その理由は次のとおりです。
このC
コードスニペットを検討してください。
_for (int i=0; i<n; i++)
c[i] = a[i] + b[i]
_
Whatやりたいことは非常に明確です。しかし、howタスクが実行されるか、実行方法は実際にはそうではありません。for-loopは、デフォルトではシリアル構造です。物事を並行して行うことができるかどうか、またはどのように行うことができるかは通知しません。
最も明白な方法は、コードをsequential fashionで実行することです。 _a[i]
_および_b[i]
_をレジスターにロードし、それらを追加して、結果を_c[i]
_に保存し、i
ごとにこれを実行します。
ただし、最新のプロセッサには vectorまたは[〜#〜] simd [〜#〜]データのベクトルで操作可能な命令セット同じ命令同じ操作(たとえば、上記の2つのベクトルを追加)。プロセッサ/アーキテクチャによっては、一度に1つではなく、同じ命令の下で、たとえばa
およびb
から4つの数字を追加できる場合があります。
Single Instruction Multiple Data を活用して、data level parallelismを実行します。たとえば、一度に4つのものをロードし、一度に4つのものを追加し、一度に4つのものを保存します。そして、これはcode vectorisationです。
これは、複数の計算が同時に実行されるコード並列化とは異なることに注意してください。
コンパイラーがそのようなコードのブロックを識別し、automaticallyでそれらをベクトル化するのは素晴らしいことです。これは難しいタスクです。 自動コードベクトル化 は、コンピュータサイエンスの挑戦的な研究トピックです。しかし、時間の経過とともに、コンパイラーはそれをより良くしてきました。 _GNU-gcc
_ here のauto vectorisation機能を確認できます。 _LLVM-clang
_ here についても同様です。また、gcc
およびICC
(Intel C++コンパイラー)と比較した最後のリンクでベンチマークを見つけることもできます。
gcc
(私は_v4.9
_を使用しています)たとえば、_-O2
_レベルの最適化でコードを自動的にベクトル化しません。したがって、上記のコードを実行する場合、順番に実行されます。長さ5億の2つの整数ベクトルを追加するタイミングを次に示します。
フラグ_-ftree-vectorize
_を追加するか、最適化をレベル_-O3
_に変更する必要があります。 (_-O3
_は その他の追加の最適化 も実行することに注意してください)。フラグ _-fopt-info-vec
_ は、ループが正常にベクトル化されたことを通知するので便利です)。
_# compiling with -O2, -ftree-vectorize and -fopt-info-vec
# test.c:32:5: note: loop vectorized
# test.c:32:5: note: loop versioned for vectorization because of possible aliasing
# test.c:32:5: note: loop peeled for vectorization to enhance alignment
_
これは、関数がベクトル化されていることを示しています。長さ5億の整数ベクトルで、ベクトル化されていないバージョンとベクトル化されたバージョンの両方を比較するタイミングを次に示します。
_x = sample(100L, 500e6L, TRUE)
y = sample(100L, 500e6L, TRUE)
z = vector("integer", 500e6L) # result vector
# non-vectorised, -O2
system.time(.Call("Csum", x, y, z))
# user system elapsed
# 1.830 0.009 1.852
# vectorised using flags shown above at -O2
system.time(.Call("Csum", x, y, z))
# user system elapsed
# 0.361 0.001 0.362
# both results are checked for identicalness, returns TRUE
_
この部分は、連続性を失わずに安全にスキップできます。
コンパイラーは、ベクトル化するのに十分な情報を常に持っているとは限りません。 並列プログラミングのOpenMP仕様 を使用できます。これは、simdコンパイラディレクティブを提供して、コンパイラにコードをベクトル化するよう指示します。コードを手動でベクトル化する場合、メモリのオーバーラップ、競合状態などがないことを確認することが不可欠です。そうしないと、誤った結果になります。
_#pragma omp simd
for (i=0; i<n; i++)
c[i] = a[i] + b[i]
_
これを行うことにより、コンパイラーに具体的に何をしてもベクトル化するように依頼します。コンパイル時フラグ _-fopenmp
_ を使用して、OpenMP拡張機能をアクティブにする必要があります。それにより:
_# timing with -O2 + OpenMP with simd
x = sample(100L, 500e6L, TRUE)
y = sample(100L, 500e6L, TRUE)
z = vector("integer", 500e6L) # result vector
system.time(.Call("Cvecsum", x, y, z))
# user system elapsed
# 0.360 0.001 0.360
_
素晴らしいです!これは、OpenMP 4.0をサポートするgcc v6.2.0およびllvm clang v3.9.0(両方ともhomebrew、MacOS 10.12.3を介してインストールされます)でテストされました。
この意味で、 配列プログラミングのウィキペディアページ は、配列全体を操作する言語は通常、vectorised operationsとして呼び出すと述べていますが、実際にはloop hidingIMO(実際にベクトル化されていない限り)。
Rの場合、CのrowSums()
またはcolSums()
コードでさえcode vectorisationIIUCを利用しません。 Cの単なるループです。lapply()
についても同様です。 apply()
の場合、Rにあります。したがって、これらはすべてloop hidingです。
要するに、R関数を次のようにラップします。
コードをベクトル化する
C
!=にfor-loopを記述するだけです。
_for-loopをR
!=に記述するだけで、コードをベクトル化します。Intel Math Kernel Library(MKL) たとえば、関数のベクトル化された形式を実装します。
HTH
参照:
したがって、素晴らしい答え/コメントをいくつかの一般的な答えにまとめて、いくつかの背景を提供します:Rには4種類のループがあります(in-vectorized to vectorized order)
for
ループは、各反復でR関数を繰り返し呼び出します(Not vectorised)したがって、_*apply
_ファミリーは2番目のタイプです。最初のタイプであるapply
を除く
これは、 ソースコードのコメントから理解できます
/ * .Internal(lapply(X、FUN))* /
/ *これは特別な.Internalであるため、未評価の引数があります。それは
クロージャーラッパーから呼び出されるため、XとFUNは約束です。 FUNは、たとえばbquote。 * /
つまり、lapply
s CコードはRから未評価の関数を受け入れ、後でCコード内で評価します。これは基本的にlapply
s _.Internal
_呼び出しの違いです
_.Internal(lapply(X, FUN))
_
R関数を保持するFUN
引数を持つもの
そしてcolMeans
_.Internal
_呼び出ししないはFUN
引数を持ちます
_.Internal(colMeans(Re(x), n, prod(dn), na.rm))
_
colMeans
は、lapply
とは異なり、exactly使用する必要がある関数を知っているため、Cコード内で内部的に平均を計算します。
lapply
C code 内の各反復でのR関数の評価プロセスを明確に見ることができます。
_ for(R_xlen_t i = 0; i < n; i++) {
if (realIndx) REAL(ind)[0] = (double)(i + 1);
else INTEGER(ind)[0] = (int)(i + 1);
tmp = eval(R_fcall, rho); // <----------------------------- here it is
if (MAYBE_REFERENCED(tmp)) tmp = lazy_duplicate(tmp);
SET_VECTOR_ELT(ans, i, tmp);
}
_
まとめると、lapply
はベクトル化されません。ただし、単純なR for
ループよりも2つの利点があります。
ループ内でのアクセスと割り当ては、Cの方が高速のようです(つまり、関数のlapply
ing)違いは大きいように見えますが、それでもマイクロ秒レベルにとどまり、コストのかかるのはR関数の評価です各反復。簡単な例:
_ffR = function(x) {
ans = vector("list", length(x))
for(i in seq_along(x)) ans[[i]] = x[[i]]
ans
}
ffC = inline::cfunction(sig = c(R_x = "data.frame"), body = '
SEXP ans;
PROTECT(ans = allocVector(VECSXP, LENGTH(R_x)));
for(int i = 0; i < LENGTH(R_x); i++)
SET_VECTOR_ELT(ans, i, VECTOR_ELT(R_x, i));
UNPROTECT(1);
return(ans);
')
set.seed(007)
myls = replicate(1e3, runif(1e3), simplify = FALSE)
mydf = as.data.frame(myls)
all.equal(ffR(myls), ffC(myls))
#[1] TRUE
all.equal(ffR(mydf), ffC(mydf))
#[1] TRUE
microbenchmark::microbenchmark(ffR(myls), ffC(myls),
ffR(mydf), ffC(mydf),
times = 30)
#Unit: microseconds
# expr min lq median uq max neval
# ffR(myls) 3933.764 3975.076 4073.540 5121.045 32956.580 30
# ffC(myls) 12.553 12.934 16.695 18.210 19.481 30
# ffR(mydf) 14799.340 15095.677 15661.889 16129.689 18439.908 30
# ffC(mydf) 12.599 13.068 15.835 18.402 20.509 30
_
@Rolandが述べたように、解釈されたRループではなく、コンパイルされたCループを実行します
ただし、コードをベクトル化する際には、考慮すべきことがいくつかあります。
df
と呼びましょう)がクラス_data.frame
_である場合、いくつかのベクトル化関数(colMeans
、colSums
、rowSums
など)これが彼らが設計された方法であるという理由だけで、それを最初にマトリックスに変換する必要があります。これは、大きなdf
の場合、これにより大きなオーバーヘッドが発生する可能性があることを意味します。 lapply
は、df
から実際のベクトルを抽出するため(_data.frame
_は単なるベクトルのリストであるため)、これを行う必要はありません。しかし、多くの行では、lapply(df, mean)
がcolMeans(df)
よりも優れている場合があります。.Primitive
_やジェネリック(_S3
_、_S4
_)などの多種多様な関数タイプがあることです( here いくつかの追加情報。ジェネリック関数は、時にはコストのかかる操作であるメソッドディスパッチを行う必要があります。たとえば、mean
は汎用_S3
_関数ですが、sum
はPrimitive
です。したがって、lapply(df, sum)
は、上記の理由からcolSums
と比較して非常に効率的である場合があります