Là gia đình * áp dụng gia đình trực tiếp thực sự không vector hóa?


138

Vì vậy, chúng tôi thường nói với mọi người dùng R mới rằng " applykhông được vector hóa, hãy xem Patrick Burns R Inferno Circle 4 " có nội dung (tôi trích dẫn):

Một phản xạ phổ biến là sử dụng một chức năng trong gia đình áp dụng. Đây không phải là vector hóa, nó là ẩn vòng lặp . Hàm áp dụng có một vòng lặp for trong định nghĩa của nó. Hàm lapply chôn vòng lặp, nhưng thời gian thực hiện có xu hướng gần bằng với một vòng lặp rõ ràng.

Thật vậy, một cái nhìn nhanh về applymã nguồn cho thấy vòng lặp:

grep("for", capture.output(getAnywhere("apply")), value = TRUE)
## [1] "        for (i in 1L:d2) {"  "    else for (i in 1L:d2) {"

Ok cho đến nay, nhưng một cái nhìn vào lapplyhoặc vapplythực sự tiết lộ một bức tranh hoàn toàn khác:

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>

Vì vậy, rõ ràng không có forvòng lặp R ẩn ở đó, thay vào đó họ đang gọi hàm viết C nội bộ.

Một cái nhìn nhanh trong lỗ thỏ cho thấy khá nhiều hình ảnh tương tự

Hơn nữa, hãy lấy colMeanschức năng làm ví dụ, không bao giờ bị buộc tội khi không được véc tơ

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>

Huh? Nó cũng chỉ gọi .Internal(colMeans(...những gì chúng ta cũng có thể tìm thấy trong lỗ thỏ . Vậy điều này khác với .Internal(lapply(..như thế nào?

Trên thực tế, một điểm chuẩn nhanh cho thấy hiệu suất sapplykhông kém hơn colMeansvà tốt hơn nhiều so với forvòng lặp cho một tập dữ liệu lớn

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 

Nói cách khác, có đúng không khi nói điều đó lapplyvapply thực sự được véc tơ (so với applyđó là một forvòng lặp cũng gọi lapply) và Patrick Burns thực sự có ý gì để nói?


8
Đây là tất cả trong ngữ nghĩa, nhưng tôi sẽ không xem xét chúng. Tôi xem xét một cách tiếp cận được vector hóa nếu một hàm R chỉ được gọi một lần và có thể được truyền một vectơ giá trị. *applycác hàm liên tục gọi các hàm R, làm cho chúng lặp lại. Về hiệu suất tốt của sapply(m, mean): Có thể mã C của lapplyphương thức chỉ gửi một lần và sau đó gọi phương thức đó nhiều lần? mean.defaultlà khá tối ưu.
Roland

4
Câu hỏi tuyệt vời, và cảm ơn vì đã kiểm tra mã cơ bản. Tôi đã tìm kiếm nếu nó đã được thay đổi gần đây, nhưng không có gì về điều này trong ghi chú phát hành R từ phiên bản 2.13.0 trở đi.
ilir 11/03/2015

1
Hiệu suất phụ thuộc vào cả nền tảng và trình biên dịch C và cờ liên kết được sử dụng ở mức độ nào?
smci 11/03/2015

3
@DavidArenburg Thật ra, tôi không nghĩ nó được xác định rõ. Ít nhất tôi không biết một tài liệu tham khảo kinh điển. Định nghĩa ngôn ngữ đề cập đến các hoạt động "vectorized", nhưng không định nghĩa vector hóa.
Roland

3
Rất liên quan: Là gia đình áp dụng R hơn đường cú pháp? (Và, giống như những câu trả lời này, cũng là một bài đọc tốt.)
Gregor Thomas

Câu trả lời:


73

Trước hết, trong ví dụ của bạn, bạn thực hiện các bài kiểm tra trên cơ sở "data.frame" mà không phải là công bằng cho colMeans, apply"[.data.frame"kể từ khi họ có một chi phí:

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

Trên một ma trận, hình ảnh hơi khác một chút:

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

Lấy lại phần chính của câu hỏi, sự khác biệt chính giữa lapply/ mapply/ etc và các vòng lặp R đơn giản là nơi thực hiện vòng lặp. Như Roland lưu ý, cả hai vòng lặp C và R cần đánh giá một hàm R trong mỗi lần lặp là tốn kém nhất. Các hàm C thực sự nhanh là những hàm làm mọi thứ trong C, vì vậy, tôi đoán, đây có phải là "véc tơ" nói về cái gì không?

Một ví dụ nơi chúng tôi tìm thấy giá trị trung bình trong mỗi thành phần của "danh sách":

( EDIT 11/05/2016 : Tôi tin rằng ví dụ về việc tìm "trung bình" không phải là một thiết lập tốt cho sự khác biệt giữa việc đánh giá hàm R lặp và mã được biên dịch, (1) vì tính đặc biệt của thuật toán trung bình của R trên "số" s đơn giản sum(x) / length(x)và (2) sẽ có ý nghĩa hơn khi kiểm tra "danh sách" với length(x) >> lengths(x). Vì vậy, ví dụ "trung bình" được chuyển đến cuối và thay thế bằng một cái khác.)

Như một ví dụ đơn giản, chúng ta có thể xem xét việc tìm ra sự đối lập của từng length == 1yếu tố của một "danh sách":

Trong một tmp.ctập tin:

#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);
}

Và ở bên R:

system("R CMD SHLIB /home/~/tmp.c")
dyn.load("/home/~/tmp.so")

với dữ liệu:

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)
})

Điểm chuẩn:

#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

(Theo ví dụ ban đầu về tìm kiếm trung bình):

#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

10
Điểm tuyệt vời về chi phí chuyển đổi data.frame thành ma trận và cảm ơn vì đã cung cấp điểm chuẩn.
Joshua Ulrich

Đó là một câu trả lời rất hay, mặc dù tôi không thể biên dịch all_Cvà các C_and_Rchức năng của bạn . Tôi cũng được tìm thấy trong các tài liệu của compiler::cmpfunmột phiên bản cũ của R lapply trong đó có một R thực tế forvòng lặp, tôi bắt đầu nghi ngờ rằng Burns đã đề cập đến rằng phiên bản cũ được vectorised kể từ đó và đây là câu trả lời thực tế cho câu hỏi của tôi .. ..
David Arenburg 11/03/2015

@DavidArenburg: Điểm chuẩn la1từ ?compiler::cmpfundường như, vẫn, để mang lại hiệu quả tương tự với tất cả trừ các all_Cchức năng. Tôi đoán, đó là - vấn đề định nghĩa; là "vectơ" có nghĩa là bất kỳ hàm nào chấp nhận không chỉ vô hướng, bất kỳ hàm nào có mã C, bất kỳ hàm nào chỉ sử dụng tính toán trong C?
alexis_laz 11/03/2015

1
Tôi đoán tất cả các hàm trong R đều có mã C trong đó, đơn giản vì mọi thứ trong R là một hàm (phải được viết bằng một số ngôn ngữ). Về cơ bản, nếu tôi hiểu đúng, bạn đang nói rằng lapplynó không được vector hóa đơn giản bởi vì nó đánh giá một hàm R trong mỗi lần lặp với mã C của nó?
David Arenburg

5
@DavidArenburg: Nếu tôi phải định nghĩa "vector hóa" theo một cách nào đó, tôi đoán, tôi sẽ chọn một phương pháp ngôn ngữ; tức là một hàm chấp nhận và biết cách xử lý một "vectơ", cho dù đó là nhanh, chậm, được viết bằng C, bằng R hoặc bất cứ điều gì khác. Trong R, tầm quan trọng của vector hóa là ở chỗ nhiều hàm được viết bằng C và xử lý các vectơ trong khi ở các ngôn ngữ khác, thông thường, người dùng sẽ lặp qua đầu vào để tìm ra giá trị trung bình. Điều đó làm cho vector hóa liên quan, gián tiếp, với tốc độ, hiệu quả, an toàn và mạnh mẽ.
alexis_laz

65

Đối với tôi, vectorisation chủ yếu là làm cho mã của bạn dễ viết hơn và dễ hiểu hơn.

Mục tiêu của chức năng véc tơ là loại bỏ việc giữ sách liên quan đến vòng lặp for. Ví dụ: thay vì:

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]])
}

Bạn có thể viết:

means <- vapply(mtcars, mean, numeric(1))
sds   <- vapply(mtcars, sd, numeric(1))

Điều đó giúp dễ dàng thấy những gì giống nhau (dữ liệu đầu vào) và những gì khác nhau (chức năng bạn đang áp dụng).

Một lợi thế thứ hai của vector hóa là vòng lặp for thường được viết bằng C, thay vì bằng R. Điều này có lợi ích hiệu suất đáng kể, nhưng tôi không nghĩ đó là thuộc tính chính của vector hóa. Vectorisation về cơ bản là tiết kiệm bộ não của bạn, không lưu công việc máy tính.


5
Tôi không nghĩ rằng có một sự khác biệt hiệu suất có ý nghĩa giữa forcác vòng lặp C và R. OK, một vòng lặp C có thể được tối ưu hóa bởi trình biên dịch, nhưng điểm chính cho hiệu suất là liệu nội dung của vòng lặp có hiệu quả hay không. Và rõ ràng mã được biên dịch thường nhanh hơn mã được giải thích. Nhưng đó có lẽ là những gì bạn muốn nói.
Roland

3
@Roland yeah, bản thân nó không phải là vòng lặp for, đó là tất cả những thứ xung quanh nó (chi phí của một cuộc gọi chức năng, khả năng sửa đổi tại chỗ, ...).
hadley

10
@DavidArenburg "Sự nhất quán không cần thiết là hobgoblin của những bộ óc nhỏ";)
hadley

6
Không, tôi không nghĩ hiệu suất là điểm chính của việc mã hóa mã của bạn. Viết lại một vòng lặp thành một cách nhanh chóng có lợi ngay cả khi nó không nhanh hơn. Điểm chính của dplyr là nó giúp thể hiện thao tác dữ liệu dễ dàng hơn (và thật tuyệt khi nó cũng nhanh).
hadley

12
@DavidArenburg đó là vì bạn là người dùng R có kinh nghiệm. Hầu hết người dùng mới tìm thấy các vòng lặp tự nhiên hơn nhiều, và cần được khuyến khích để vectorise. Đối với tôi, sử dụng một chức năng như colMeans không nhất thiết là về vector hóa, đó là về việc sử dụng lại mã nhanh mà ai đó đã viết
hadley

49

Tôi đồng ý với quan điểm của Patrick Burns rằng đó là ẩn vòng lặp chứ không phải mã hóa . Đây là lý do tại sao:

Xem xét Cđoạn mã này:

for (int i=0; i<n; i++)
  c[i] = a[i] + b[i]

Những gì chúng tôi muốn làm là khá rõ ràng. Nhưng làm thế nào nhiệm vụ được thực hiện hoặc làm thế nào nó có thể được thực hiện không thực sự. Một vòng lặp for theo mặc định là một cấu trúc nối tiếp. Nó không thông báo nếu hoặc làm thế nào mọi thứ có thể được thực hiện song song.

Cách rõ ràng nhất là mã được chạy theo cách liên tiếp . Tải a[i]b[i]bật vào các thanh ghi, thêm chúng, lưu trữ kết quả c[i]và thực hiện điều này cho từng thanh ghi i.

Tuy nhiên, các bộ xử lý hiện đại có tập lệnh vectơ hoặc SIMD có khả năng hoạt động trên một vectơ dữ liệu trong cùng một lệnh khi thực hiện cùng một thao tác (ví dụ: thêm hai vectơ như hình trên). Tùy thuộc vào bộ xử lý / kiến ​​trúc, có thể thêm bốn số từ abtheo cùng một hướng dẫn, thay vì một số tại một thời điểm.

Chúng tôi muốn khai thác Đa hướng dẫn nhiều dữ liệu và thực hiện song song mức dữ liệu , ví dụ, tải 4 thứ cùng một lúc, thêm 4 thứ cùng một lúc, ví dụ lưu trữ 4 thứ cùng một lúc. Và đây là vector hóa .

Lưu ý rằng điều này khác với song song mã - trong đó nhiều phép tính được thực hiện đồng thời.

Sẽ thật tuyệt nếu trình biên dịch xác định các khối mã như vậy và tự động vectơ chúng, đây là một nhiệm vụ khó khăn. Tự động hóa mã hóa là một chủ đề nghiên cứu đầy thách thức trong Khoa học Máy tính. Nhưng theo thời gian, trình biên dịch đã trở nên tốt hơn về nó. Bạn có thể kiểm tra khả năng vector hóa tự động GNU-gcc ở đây . Tương tự cho LLVM-clang ở đây . Và bạn cũng có thể tìm thấy một số điểm chuẩn trong liên kết cuối cùng được so sánh với gccICC(trình biên dịch Intel C ++).

gcc(Tôi đang bật v4.9) chẳng hạn mã vectorise tự động ở -O2mức tối ưu hóa. Vì vậy, nếu chúng ta thực thi mã được hiển thị ở trên, nó sẽ được chạy tuần tự. Đây là thời điểm để thêm hai vectơ số nguyên có chiều dài 500 triệu.

Chúng ta cần thêm cờ -ftree-vectorizehoặc thay đổi tối ưu hóa để lên cấp -O3. (Lưu ý rằng cũng -O3thực hiện các tối ưu hóa bổ sung khác ). Cờ -fopt-info-vecnày rất hữu ích vì nó thông báo khi vòng lặp được vector hóa thành công).

# 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    

Điều này cho chúng ta biết rằng hàm được vector hóa. Dưới đây là thời gian so sánh cả hai phiên bản không vectơ và vectơ trên các vectơ nguyên có độ dài 500 triệu:

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

Phần này có thể được bỏ qua một cách an toàn mà không mất liên tục.

Trình biên dịch sẽ không luôn luôn có đủ thông tin để vectorise. Chúng ta có thể sử dụng OpenMP đặc điểm kỹ thuật cho lập trình song song , mà còn cung cấp một SIMD trình biên dịch chỉ thị để hướng dẫn các trình biên dịch để vectorise mã. Điều cần thiết là phải đảm bảo rằng không có sự chồng chéo bộ nhớ, điều kiện cuộc đua, v.v. khi mã hóa bằng tay, nếu không nó sẽ dẫn đến kết quả không chính xác.

#pragma omp simd
for (i=0; i<n; i++) 
  c[i] = a[i] + b[i]

Bằng cách này, chúng tôi đặc biệt yêu cầu trình biên dịch vector hóa nó bất kể điều gì. Chúng ta sẽ cần kích hoạt các phần mở rộng OpenMP bằng cách sử dụng cờ thời gian biên dịch -fopenmp. Bằng việc thực hiện điều đó:

# 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

thật là tuyệt Điều này đã được thử nghiệm với gcc v6.2.0 và llvm clang v3.9.0 (cả hai được cài đặt qua homebrew, MacOS 10.12.3), cả hai đều hỗ trợ OpenMP 4.0.


Theo nghĩa này, mặc dù trang Wikipedia về Lập trình mảng đề cập rằng các ngôn ngữ hoạt động trên toàn bộ mảng thường gọi đó là các hoạt động được véc tơ , nhưng nó thực sự là vòng lặp ẩn IMO (trừ khi nó thực sự được vector hóa).

Trong trường hợp R, chẵn rowSums()hoặc colSums()mã trong C không khai thác véc tơ mã hóa IIUC; nó chỉ là một vòng lặp trong C. Tương tự như vậy lapply(). Trong trường hợp apply(), nó ở R. Tất cả những thứ này đều ẩn vòng lặp .

Nói tóm lại, gói một hàm R bằng cách:

chỉ cần viết một cho vòng trong C! = vectorising mã của bạn.
chỉ cần viết một cho vòng trong R! = vectorising mã của bạn.

Ví dụ, Thư viện hạt nhân toán học Intel (MKL) thực hiện các dạng hàm được véc tơ.

HTH


Người giới thiệu:

  1. Nói chuyện bởi James Reinder, Intel (câu trả lời này chủ yếu là một nỗ lực để tóm tắt cuộc nói chuyện tuyệt vời này)

35

Vì vậy, để tổng hợp các câu trả lời / nhận xét tuyệt vời thành một số câu trả lời chung chung và cung cấp một số thông tin cơ bản: R có 4 loại vòng lặp ( theo thứ tự từ không được vector hóa đến thứ tự véc tơ )

  1. forVòng lặp R liên tục gọi các hàm R trong mỗi lần lặp ( Không được vector hóa )
  2. Vòng lặp C liên tục gọi các hàm R trong mỗi lần lặp ( Không được vector hóa )
  3. Vòng lặp C chỉ gọi hàm R một lần ( Hơi vectơ )
  4. Một vòng lặp C đơn giản hoàn toàn không gọi bất kỳ hàm R nào và sử dụng các hàm được biên dịch riêng của nó ( Vectorized )

Vì vậy, *applygia đình là loại thứ hai. Ngoại trừ applyloại nào là loại đầu tiên

Bạn có thể hiểu điều này từ nhận xét trong mã nguồn của nó

/ * .I Internalal (lapply (X, FUN)) * /

/ * Đây là một đặc biệt. Nội bộ, do đó, có các đối số không được đánh giá. Nó được
gọi từ một trình bao bọc đóng, vì vậy X và FUN là những lời hứa. FUN phải được đánh giá để sử dụng trong ví dụ bquote. * /

Điều đó có nghĩa là lapplymã C chấp nhận một hàm không được đánh giá từ R và sau đó đánh giá nó trong chính mã C. Về cơ bản, đây là sự khác biệt giữa cuộc gọi lapplys.Internal

.Internal(lapply(X, FUN))

Trong đó có một FUNđối số chứa hàm R

colMeans .Internalcuộc gọi khôngFUNđối số

.Internal(colMeans(Re(x), n, prod(dn), na.rm))

colMeans, không giống như lapplybiết chính xác chức năng nào nó cần sử dụng, do đó, nó tính toán giá trị trung bình bên trong mã C.

Bạn có thể thấy rõ quá trình đánh giá của hàm R trong mỗi lần lặp trong lapplymã C

 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);
   }

Để tổng hợp mọi thứ, lapplykhông được vector hóa , mặc dù nó có hai lợi thế có thể có trên forvòng lặp R đơn giản

  1. Truy cập và gán trong một vòng lặp dường như nhanh hơn trong C (nghĩa là trong lapplymột hàm) Mặc dù sự khác biệt có vẻ lớn, nhưng chúng tôi vẫn ở mức micro giây và điều tốn kém là định giá của hàm R trong mỗi lần lặp. Một ví dụ đơn giản:

    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
  2. Như được đề cập bởi @Roland, nó chạy một vòng lặp C được biên dịch thay vì vòng lặp R được giải thích


Mặc dù khi vector hóa mã của bạn, có một số điều bạn cần tính đến.

  1. Nếu dữ liệu của bạn (chúng ta hãy gọi nó df) là của lớp data.frame, một số chức năng vector hóa (ví dụ như colMeans, colSums, rowSums, vv) sẽ phải chuyển nó sang một ma trận đầu tiên, đơn giản chỉ vì đây là cách chúng được thiết kế. Điều này có nghĩa là đối với một lớn, dfđiều này có thể tạo ra một chi phí rất lớn. Mặc dù lapplysẽ không phải làm điều này vì nó trích xuất các vectơ thực tế ra khỏi df(như data.framechỉ là một danh sách các vectơ) và do đó, nếu bạn không có quá nhiều cột nhưng nhiều hàng, lapply(df, mean)đôi khi có thể là lựa chọn tốt hơn colMeans(df).
  2. Một điều cần nhớ là R có rất nhiều loại chức năng khác nhau, chẳng hạn như .Primitive, và chung ( S3, S4) xem tại đây để biết thêm thông tin. Hàm chung phải thực hiện một công thức phương thức mà đôi khi là một hoạt động tốn kém. Ví dụ, meanS3chức năng chung trong khi sumPrimitive. Do đó, một số lần lapply(df, sum)có thể rất hiệu quả so colSumsvới các lý do được liệt kê ở trên

1
Tóm tắt rất gắn kết. Chỉ cần một vài lưu ý: (1) C biết cách xử lý "data.frame", vì chúng là "list" s với các thuộc tính; đó là colMeansvv được xây dựng để chỉ xử lý ma trận. (2) Tôi hơi bối rối bởi danh mục thứ ba của bạn; Tôi không thể nói những gì -exaclty- bạn đang đề cập đến. (3) Vì bạn đang đề cập cụ thể đến lapply, tôi tin rằng nó không tạo ra sự khác biệt giữa "[<-"R và C; cả hai đều phân bổ trước một "danh sách" (SEXP) và điền nó vào mỗi lần lặp ( SET_VECTOR_ELTbằng C), trừ khi tôi thiếu điểm của bạn.
alexis_laz

2
Tôi nhận thấy quan điểm của bạn về do.callviệc nó thực hiện một cuộc gọi chức năng trong môi trường C và chỉ đánh giá nó; mặc dù tôi có một thời gian khó khăn để so sánh nó với vòng lặp hoặc vector hóa vì nó làm một điều khác biệt. Thật ra, bạn đang đúng về việc truy cập và gán sự khác biệt giữa C và R, mặc dù cả hai đều ở mức micro giây và không ảnh hưởng đến kết quả của trình chạy lại rất nhiều, vì chi phí là cuộc gọi hàm R lặp (so sánh R_loopR_lapplytrong câu trả lời của tôi ). (Tôi sẽ chỉnh sửa bài đăng của bạn với điểm chuẩn; Tôi hy vọng bạn, vẫn vậy, sẽ không phiền)
alexis_laz

2
Tôi không cố gắng không đồng ý --- và tôi thực sự bối rối về những gì bạn không đồng ý. Nhận xét trước đó của tôi có thể đã được diễn đạt tốt hơn. Tôi đang cố gắng tinh chỉnh thuật ngữ đang được sử dụng vì thuật ngữ "vector hóa" có hai định nghĩa thường bị xáo trộn. Tôi không nghĩ rằng điều này là có thể tranh cãi. Burns và bạn dường như chỉ muốn sử dụng nó theo nghĩa thực hiện, nhưng Hadley và nhiều thành viên R-Core (lấy Vectorize()ví dụ) cũng sử dụng nó theo nghĩa UI. Tôi nghĩ rằng phần lớn sự bất đồng trong chủ đề này là do sử dụng một thuật ngữ cho hai khái niệm riêng biệt nhưng có liên quan.
Gregor Thomas

3
@DavidArenburg và đó không phải là vector hóa theo nghĩa UI, bất kể có vòng lặp for trong R hay C bên dưới không?
Gregor Thomas

2
@DavidArenburg, Gregor, tôi nghĩ rằng sự nhầm lẫn nằm giữa "véc tơ mã hóa" và "hàm vectơ". Trong R, việc sử dụng dường như nghiêng về phía sau. "Véc tơ mã" mô tả hoạt động trên một vectơ có độ dài 'k' trong cùng một hướng dẫn. Gói một fn. xung quanh mã vòng lặp dẫn đến "các hàm vectơ" (vâng, nó không có ý nghĩa và gây nhầm lẫn, tôi đồng ý, tốt hơn là ẩn vòng lặp hoặc các hàm vectơ i / p ) và không cần phải làm gì với vectơ mã hóa . Trong R, áp dụng sẽ là một hàm vectơ , nhưng nó không vectơ mã của bạn, mà hoạt động trên các vectơ.
Arun
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.