그래서 우리는 모든 R 신규 사용자에게 ” apply
벡터화되지 않았으며 Patrick Burns R Inferno Circle 4 “를 확인하는 데 익숙합니다 .
일반적인 반사는 apply 제품군에서 기능을 사용하는 것입니다. 이것은
벡터화 가 아니며 루프 숨김 입니다. apply 함수의 정의에는 for 루프가 있습니다. lapply 함수는 루프를 묻지 만 실행 시간은 명시적인 for 루프와 거의 같은 경향이 있습니다.
실제로 apply
소스 코드를 간단히 살펴보면 루프가 드러납니다.
grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] " for (i in 1L:d2) {" " else for (i in 1L:d2) {"
좋아 지금까지,하지만 봐 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>
따라서 분명히 거기에 for
숨어있는 R 루프 가 없으며 내부 C 작성 함수를 호출합니다.
또한 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
실제로 벡터화된다 (비교 apply
이다 for
또한 호출하는 루프 lapply
)와 패트릭 번스 정말 의미 무슨 말을 했습니까?
답변
첫째, 귀하의 예제에서 당신은 공평하지 않은 “data.frame”에 시험을 colMeans
, apply
그리고 "[.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) “숫자”에 대한 R의 평균 알고리즘의 특수성 때문에 간단한 통해 s의 sum(x) / length(x)
그것과의 “리스트”또는 시험 더 이해하게한다 (2) length(x) >> lengths(x)
. 따라서, “평균”예 끝으로 이동하고 서로 대체된다.)
간단한 예로 우리 length == 1
는 “목록” 의 각 요소의 반대 결과를 고려할 수 있습니다 .
A의 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))
따라서 동일한 내용 (입력 데이터)과 다른 내용 (적용중인 기능)을보다 쉽게 확인할 수 있습니다.
벡터화의 두 번째 장점은 for 루프가 종종 R이 아닌 C로 작성된다는 것입니다. 이는 상당한 성능 이점이 있지만 이것이 벡터화의 주요 특성이라고 생각하지 않습니다. 벡터화는 기본적으로 컴퓨터 작업을 저장하는 것이 아니라 뇌를 절약하는 것입니다.
답변
나는 오히려 것을 패트릭 번즈보기에 동의 루프 숨어 아니라 코드 vectorisation . 이유는 다음과 같습니다.
이 C
코드 스 니펫을 고려하십시오 .
for (int i=0; i<n; i++)
c[i] = a[i] + b[i]
무엇을 우리가하고자하는 것은 매우 분명하다. 그러나 어떻게 작업이 수행 또는 어떻게 수행 할 수있는 것은 정말 아니다. 에 대한 루프 기본적으로는 직렬 구조입니다. 작업을 병렬로 수행 할 수 있는지 여부와 방법을 알려주지 않습니다.
가장 확실한 방법은 코드가 순차적으로 실행되는 것 입니다. 레지스터에 로드 a[i]
하고 b[i]
on하여 레지스터를 추가하고에 결과를 저장 한 다음 c[i]
각에 대해이 작업을 수행하십시오 i
.
그러나, 현대 프로세서는 동일한 동작을 수행 할 때 (예를 들어, 상기 도시 된 바와 같이 2 개의 벡터를 추가하는) 동일한 명령 동안 데이터 의 벡터 에 대해 동작 할 수있는 벡터 또는 SIMD 명령 세트를 갖는다 . 프로세서 / 아키텍처에 따라서는, 추가 말, 네 개의 숫자에서 할 수있을 하고 한 번에 동일한 명령에 따라, 대신 중 하나.a
b
예를 들어, 단일 명령어 다중 데이터 를 활용하고 데이터 레벨 병렬 처리를 수행 하려고합니다. 즉, 한 번에 4 개의 항목을로드하고 한 번에 4 개의 항목을 추가하고 한 번에 4 개의 항목을 저장합니다. 그리고 이것은 코드 벡터화 입니다.
이는 여러 계산을 동시에 수행하는 코드 병렬화와 다릅니다.
컴파일러가 그러한 코드 블록을 식별하고 자동으로 벡터화하는 것이 좋을 것 입니다. 이는 어려운 작업입니다. 자동 코드 벡터화 는 컴퓨터 과학의 까다로운 연구 주제입니다. 그러나 시간이 지남에 따라 컴파일러가 더 좋아졌습니다. 여기서 자동 벡터화 기능을 확인할 수 있습니다 . 마찬가지로 여기에 . 또한 마지막 링크에서 (Intel C ++ 컴파일러) 와 비교 한 일부 벤치 마크를 찾을 수 있습니다 .GNU-gcc
LLVM-clang
gcc
ICC
gcc
(I가있어 v4.9
예를 들어 것은)에서 자동으로 코드 벡터화하지 않는 -O2
수준의 최적화. 따라서 위에 표시된 코드를 실행하면 순차적으로 실행됩니다. 다음은 길이 5 억의 정수 벡터 두 개를 추가하는 타이밍입니다.
플래그를 추가 -ftree-vectorize
하거나 최적화를 level로 변경해야합니다 -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]
이를 통해 컴파일러는 무엇이든 상관없이 벡터화하도록 요청합니다. 컴파일 타임 플래그를 사용하여 OpenMP 확장을 활성화해야합니다 -fopenmp
. 그렇게함으로써 :
# 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
대단해! 이것은 gcc v6.2.0 및 llvm clang v3.9.0 (둘 다 Homebrew, MacOS 10.12.3을 통해 설치됨)으로 테스트되었으며 둘 다 OpenMP 4.0을 지원합니다.
이러한 의미에서, 비록 배열 프로그래밍에 대한 위키 백과 페이지가 전체 어레이에서 작동하는 언어를 언급 일반적으로한다는 전화를 벡터화 작업 , 그것은 정말 루프 숨어 (가 실제로 벡터화되지 않는 한) IMO.
R의 경우, 짝수 rowSums()
또는 colSums()
C의 코드는 코드 벡터화 IIUC를 이용하지 않습니다 . 그것은 단지 C의 루프입니다 lapply()
. 의 경우 apply()
R에 있습니다. 따라서 모두 루프 숨기기 입니다.
간단히 말해서 R 함수를 다음과 같이 래핑합니다.
코드를 벡터화하기 위해 for 루프 를 작성
C
하십시오! 코드를 벡터화하기 위해 for 루프 를
작성 하십시오!R
예를 들어 인텔 수학 커널 라이브러리 (MKL) 는 벡터화 된 형태의 함수를 구현합니다.
HTH
참고 문헌 :
- Intel, James Reinders의 대화 (이 답변은 주로이 뛰어난 대화를 요약하려는 시도입니다)
답변
따라서 큰 답변 / 의견을 일반적인 답변으로 요약하고 배경을 제공하십시오 .R에는 4 가지 유형의 루프가 있습니다 ( 벡터화되지 않은 순서에서 벡터화 된 순서로 )
for
각 반복에서 R 함수를 반복적으로 호출하는 R 루프 ( 벡터화되지 않음 )- 각 반복에서 R 함수를 반복적으로 호출하는 C 루프 ( 벡터화되지 않음 )
- R 함수를 한 번만 호출하는 C 루프 ( 약간 벡터화 됨 )
- 호출하지 않는 일반 C 루프 어떤 모두에서 R 기능을하고 컴파일 된 함수를 소유하고 사용 ( 벡터화 )
그래서 *apply
가족은 두 번째 유형입니다. apply
첫 번째 유형 중 더 많은 것을 제외하고
소스 코드 의 주석에서 이것을 이해할 수 있습니다.
/ *. 내부 (lapply (X, FUN)) * /
/ * 이것은 특별한 .Internal이므로 평가되지 않은 인수가 있습니다. 되는
X과 재미가 약속 그래서, 폐쇄 래퍼에서 호출. bquote 등에서 사용하려면 FUN을 평가하지 않아야합니다. * /
즉, lapply
s C 코드는 R의 평가되지 않은 함수를 수락하고 나중에 C 코드 자체에서이를 평가합니다. 이것은 기본적으로 차이입니다 lapply
님의 .Internal
전화
.Internal(lapply(X, FUN))
가 어떤 FUN
에 R 기능을 보유하고 인수를
그리고 colMeans
.Internal
호출하는이 되지 않습니다 이 FUN
인수를
.Internal(colMeans(Re(x), n, prod(dn), na.rm))
colMeans
사용하는 함수를 정확히lapply
아는 것과 달리 C 코드 내에서 평균을 내부적으로 계산합니다.
C 코드 내의 각 반복에서 R 함수의 평가 프로세스를 명확하게 볼 수 있습니다lapply
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);
}
요약 하면 일반화 R 루프에 비해 두 가지 가능한 장점이 있지만 lapply
벡터화되지는 않습니다.for
-
루프에서 액세스하고 할당하는 것은 C에서 더 빠릅니다 (즉
lapply
, 함수를 수행하는 것). 비록 차이는 크지 만 여전히 마이크로 초 수준을 유지하고 비용이 많이 드는 것은 각 반복에서 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)
. - 기억해야 할 또 다른 사항은 R에는
.Primitive
, 및 generic (S3
,S4
) 과 같은 다양한 기능 유형이 있으며 여기 에서 추가 정보를 참조 하십시오 . 일반 함수는 때로는 값 비싼 작업 인 메소드 디스패치를 수행해야합니다. 예를 들어mean
is은 일반S3
함수sum
입니다Primitive
. 따라서 위에 나열된 이유lapply(df, sum)
와 비교colSums
하여 시간 이 매우 효율적일 수 있습니다.
답변
