ครอบครัว“ * นำไปใช้” ไม่ได้รับการปรับเวกเตอร์จริงๆเหรอ?


138

ดังนั้นเราจึงเคยพูดกับผู้ใช้ R ใหม่ทุกคนว่า " applyไม่ใช่ vectorized ลองดู Patrick Burns R Inferno Circle 4 " ซึ่งบอกว่า (ฉันพูด):

การสะท้อนที่พบบ่อยคือการใช้ฟังก์ชั่นในครอบครัวใช้ นี้ไม่ได้เป็น vectorization ก็เป็นห่วงซ่อน ฟังก์ชั่นใช้มีห่วงสำหรับในความหมายของมัน ฟังก์ชัน lapply ฝังลูป แต่เวลาดำเนินการมีแนวโน้มที่จะเท่ากับคร่าว ๆ อย่างชัดเจนสำหรับลูป

อันที่จริงการดูอย่างรวดเร็วของ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 จะ vectorised จริง (เมื่อเทียบกับapplyซึ่งเป็นforห่วงว่ายังเรียกร้องlapply) และสิ่งที่ไม่แพทริคเบิร์นส์หมายความว่าจริงๆจะพูด?


8
ทั้งหมดนี้อยู่ในความหมาย แต่ฉันไม่คิดว่ามันจะเป็นเวกเตอร์ ฉันพิจารณาวิธีการแบบเวกเตอร์ถ้าฟังก์ชั่น R ถูกเรียกเพียงครั้งเดียวและสามารถผ่านเวกเตอร์ของค่า *applyฟังก์ชั่นซ้ำ ๆ เรียกฟังก์ชั่น R ซึ่งทำให้พวกเขาลูป เกี่ยวกับประสิทธิภาพที่ดีของsapply(m, mean): อาจเป็นไปได้ว่ารหัส C ของlapplyวิธีการจัดส่งเพียงครั้งเดียวแล้วเรียกวิธีการซ้ำแล้วซ้ำอีก? mean.defaultได้รับการปรับปรุงสวย
Roland

4
คำถามที่ยอดเยี่ยมและขอบคุณสำหรับการตรวจสอบรหัสพื้นฐาน ฉันกำลังดูว่ามีการเปลี่ยนแปลงเมื่อเร็ว ๆ นี้ แต่ไม่มีอะไรเกี่ยวกับเรื่องนี้ในบันทึกประจำรุ่น R จากรุ่น 2.13.0 เป็นต้นไป
ilir

1
ประสิทธิภาพขึ้นอยู่กับทั้งแพลตฟอร์มและแฟล็ก C-compiler และตัวเชื่อมโยงที่ใช้
smci

3
@DavidArenburg ที่จริงแล้วฉันไม่คิดว่ามันจะถูกกำหนดไว้อย่างดี อย่างน้อยฉันก็ไม่รู้ข้อมูลอ้างอิงที่เป็นที่ยอมรับ คำจำกัดความภาษากล่าวถึงการดำเนินการ "vectorized" แต่ไม่ได้กำหนด vectorization
Roland

3
เกี่ยวข้องมาก: R ประยุกต์ใช้กับครอบครัวมากกว่าน้ำตาลหรือไม่ (และเช่นเดียวกับคำตอบเหล่านี้อ่านได้ดี)
Gregor Thomas

คำตอบ:


73

ครั้งแรกของทั้งหมดในตัวอย่างของคุณให้คุณทดสอบใน "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 / ฯลฯ และ R-loops ตรงไปตรงมาคือการวนซ้ำ ตามบันทึกของ Roland ทั้งลูป C และ R จำเป็นต้องประเมินฟังก์ชัน R ในแต่ละการวนซ้ำซึ่งมีค่าใช้จ่ายสูงที่สุด ฟังก์ชั่น C ที่เร็วจริงๆคือฟังก์ชั่นที่ทำทุกอย่างใน C ดังนั้นฉันเดาว่านี่ควรเป็น "vectorised" ที่เกี่ยวกับอะไร

ตัวอย่างที่เราพบค่าเฉลี่ยในองค์ประกอบ "รายการ" แต่ละรายการ:

( แก้ไข 11 พฤษภาคม '16 : ฉันเชื่อว่าตัวอย่างที่มีการค้นหา "หมายถึง" ไม่ใช่การตั้งค่าที่ดีสำหรับความแตกต่างระหว่างการประเมินฟังก์ชั่น R ซ้ำ ๆ และรวบรวมรหัส (1) เนื่องจากความพิเศษของอัลกอริทึมเฉลี่ยของ R "ตัวเลข" ง่ายกว่าsum(x) / length(x)และ (2) ควรทดสอบรายการ "s" ด้วย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)
})

Benchmarking:

#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

10
จุดที่ดีเกี่ยวกับค่าใช้จ่ายในการแปลง data.frame เป็น matrix และขอขอบคุณสำหรับการจัดทำเกณฑ์มาตรฐาน
Joshua Ulrich

นั่นเป็นคำตอบที่ดีมากแม้ว่าฉันจะไม่สามารถรวบรวมall_CและC_and_Rฟังก์ชั่นของคุณ ฉันยังพบในเอกสารของR รุ่นเก่า lapplyซึ่งมี R จริงห่วงฉันเริ่มที่จะสงสัยว่าเบิร์นส์หมายถึงว่ารุ่นเก่าซึ่งถูก vectorised ตั้งแต่นั้นมาและนี่คือคำตอบที่เกิดขึ้นจริงกับคำถามของฉัน .. ..compiler::cmpfunfor
David Arenburg

@DavidArenburg: การเปรียบเทียบla1จากที่ปรากฏ?compiler::cmpfunยังคงให้ประสิทธิภาพเดียวกันกับทุกอย่างยกเว้นall_Cฟังก์ชัน ฉันคิดว่ามันเป็น - จำเป็น - มาเป็นเรื่องของความหมาย; "vectorised" หมายถึงฟังก์ชันใด ๆ ที่ยอมรับสเกลาร์ไม่เพียง แต่ฟังก์ชั่นใด ๆ ที่มีรหัส C ฟังก์ชั่นใด ๆ ที่ใช้การคำนวณใน C เท่านั้น?
alexis_laz

1
ฉันเดาว่าฟังก์ชั่นทั้งหมดใน R มีโค้ด C อยู่ในนั้นเพราะทุกอย่างใน R เป็นฟังก์ชั่น (ซึ่งต้องเขียนในบางภาษา) โดยพื้นฐานแล้วถ้าฉันเข้าใจถูกต้องคุณกำลังบอกว่านั่นlapplyไม่ใช่เวกเตอร์เพียงเพราะมันประเมินฟังก์ชัน R ในการทำซ้ำแต่ละครั้งด้วยรหัส C
David Arenburg

5
@DavidArenburg: ถ้าฉันต้องนิยาม "vectorization" ในบางวิธีฉันเดาว่าฉันจะเลือกวิธีการทางภาษาศาสตร์ คือฟังก์ชั่นที่ยอมรับและรู้วิธีจัดการกับ "เวกเตอร์" ไม่ว่าจะรวดเร็วช้าเขียนด้วย C ใน R หรืออะไรก็ตาม ใน R ความสำคัญของ vectorisation คือในหลาย ๆ ฟังก์ชั่นถูกเขียนใน C และจัดการกับเวคเตอร์ในขณะที่ภาษาอื่น ๆ ที่ผู้ใช้มักจะวนรอบอินพุตเพื่อค้นหาค่าเฉลี่ย นั่นทำให้ vectorisation สัมพันธ์โดยอ้อมด้วยความเร็วประสิทธิภาพความปลอดภัยและความทนทาน
alexis_laz

65

สำหรับฉัน vectorisation นั้นเกี่ยวกับการทำให้โค้ดของคุณง่ายต่อการเขียนและเข้าใจได้ง่ายขึ้น

เป้าหมายของฟังก์ชั่น vectorised คือการกำจัดการเก็บหนังสือที่เกี่ยวข้องกับการวนรอบ ตัวอย่างเช่นแทนที่จะเป็น:

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

ทำให้ง่ายขึ้นที่จะเห็นว่ามีอะไรเหมือนกัน (ข้อมูลอินพุต) และสิ่งที่แตกต่าง (ฟังก์ชั่นที่คุณใช้)

ข้อได้เปรียบที่สองของ vectorisation คือ for-loop นั้นถูกเขียนด้วยภาษา C แทนที่จะเป็น R สิ่งนี้มีประโยชน์ด้านประสิทธิภาพอย่างมาก แต่ฉันไม่คิดว่ามันเป็นคุณสมบัติที่สำคัญของ vectorisation Vectorisation เป็นพื้นฐานเกี่ยวกับการบันทึกสมองของคุณไม่บันทึกงานคอมพิวเตอร์


5
ฉันไม่คิดว่าจะมีความหมายที่แตกต่างระหว่างforลูปC และ R ตกลงคอมไพเลอร์อาจถูกปรับให้เหมาะสมโดยคอมไพเลอร์ แต่ประเด็นหลักสำหรับประสิทธิภาพคือว่าเนื้อหาของลูปนั้นมีประสิทธิภาพหรือไม่ และรหัสที่คอมไพล์ได้ชัดมักจะเร็วกว่าโค้ดตีความ แต่นั่นอาจเป็นสิ่งที่คุณตั้งใจจะพูด
Roland

3
@Roland ใช่ไม่ใช่ for-loop ตัวเองต่อ se มันคือทุกสิ่งรอบ ๆ (ค่าใช้จ่ายของการเรียกใช้ฟังก์ชันความสามารถในการปรับเปลี่ยนในสถานที่, ... )
hadley

10
@DavidArenburg "ความมั่นคงที่ไม่จำเป็นเป็นเรื่องเล็ก ๆ น้อย ๆ ของความคิดเล็ก ๆ น้อย ๆ ";)
hadley

6
ไม่ฉันไม่คิดว่าประสิทธิภาพเป็นจุดหลักของการเพิ่มโค้ดของคุณ การเขียนวนซ้ำลงใน lapply นั้นมีประโยชน์แม้ว่าจะไม่เร็วกว่าก็ตาม จุดหลักของ dplyr คือมันทำให้ง่ายต่อการแสดงการจัดการข้อมูล (และมันก็ดีมากที่มันเร็วเกินไป)
hadley

12
@DavidArenburg นั่นเป็นเพราะคุณเป็นผู้ใช้ R ที่มีประสบการณ์ ผู้ใช้ใหม่ส่วนใหญ่พบว่ามีลูปเป็นธรรมชาติมากกว่าและจำเป็นต้องมีการส่งเสริมให้ใช้เวกเตอร์ สำหรับฉันแล้วการใช้ฟังก์ชั่นอย่าง colMeans ไม่จำเป็นต้องเกี่ยวกับ vectorisation มันเป็นการใช้รหัสอย่างรวดเร็วที่มีคนเขียนไปแล้ว
hadley

49

ผมเห็นด้วยกับมุมมองแพทริกเบิร์นส์ว่ามันค่อนข้างที่หลบซ่อนตัวห่วงและไม่รหัส vectorisation นี่คือเหตุผล:

พิจารณาCตัวอย่างโค้ดนี้:

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

สิ่งที่เราต้องการจะทำค่อนข้างชัดเจน แต่วิธีการทำงานหรือวิธีการปฏิบัติไม่จริง สำหรับวงโดยเริ่มต้นคือสร้างอนุกรม ไม่ได้แจ้งว่าจะทำสิ่งใดควบคู่กันหรือไม่

วิธีที่ชัดเจนที่สุดคือการที่รหัสจะทำงานในลักษณะลำดับ โหลดa[i]และb[i]ไปลงทะเบียนเพิ่มพวกเขาเก็บผลในและทำเช่นนี้สำหรับแต่ละc[i]i

อย่างไรก็ตามตัวประมวลผลที่ทันสมัยมีชุดคำสั่งเวกเตอร์หรือSIMDซึ่งสามารถทำงานบนเวกเตอร์ของข้อมูลในระหว่างการเรียนการสอนเดียวกันเมื่อทำการดำเนินการเดียวกัน (เช่นการเพิ่มเวกเตอร์สองตัวดังแสดงด้านบน) ขึ้นอยู่กับโปรเซสเซอร์ / สถาปัตยกรรมอาจเป็นไปได้ที่จะเพิ่มพูดสี่ตัวเลขจากaและbภายใต้คำสั่งเดียวกันแทนที่จะเป็นหนึ่งครั้ง

เราต้องการใช้ประโยชน์จากSingle Instruction Multiple Dataและดำเนินการคู่ขนานของระดับข้อมูลเช่นโหลด 4 สิ่งต่อครั้งเพิ่ม 4 อย่างต่อครั้งจัดเก็บ 4 รายการพร้อมกัน และนี่คือรหัส vectorisation

โปรดทราบว่านี่จะแตกต่างจากโค้ดขนาน - โดยที่การคำนวณหลายครั้งถูกดำเนินการพร้อมกัน

มันจะดีมากถ้าคอมไพเลอร์ระบุบล็อคของรหัสดังกล่าวและทำให้เวกเตอร์เหล่านั้นเป็นงานที่ยากโดยอัตโนมัติ vectorisation รหัสอัตโนมัติเป็นหัวข้อการวิจัยที่ท้าทายในวิทยาการคอมพิวเตอร์ แต่เมื่อเวลาผ่านไปคอมไพเลอร์ได้ดีกว่าที่มัน คุณสามารถตรวจสอบvectorisation อัตโนมัติความสามารถของที่นี่GNU-gcc ในทำนองเดียวกันสำหรับที่นี่LLVM-clang และคุณยังสามารถค้นหาเกณฑ์มาตรฐานได้ในลิงค์สุดท้ายเปรียบเทียบกับgccและICC(คอมไพเลอร์ Intel C ++)

gcc(ฉันเปิดอยู่v4.9) ตัวอย่างเช่นไม่ใช้รหัส vectorise โดยอัตโนมัติเมื่อ-O2การเพิ่มประสิทธิภาพระดับ ดังนั้นหากเราต้องรันโค้ดที่แสดงด้านบนมันจะถูกเรียกใช้ตามลำดับ นี่คือเวลาสำหรับการบวกเวกเตอร์จำนวนเต็มสองตัวที่มีความยาว 500 ล้าน

เราทั้งสองต้องเพิ่มธงหรือการเปลี่ยนแปลงการเพิ่มประสิทธิภาพให้อยู่ในระดับ-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    

นี่บอกเราว่าฟังก์ชั่นเป็นเวกเตอร์ นี่คือการกำหนดเวลาเปรียบเทียบทั้งเวอร์ชันที่ไม่ใช่เวกเตอร์และเวกเตอร์กับเวกเตอร์จำนวนเต็มความยาว 500 ล้าน:

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

ส่วนนี้สามารถข้ามได้อย่างปลอดภัยโดยไม่สูญเสียความต่อเนื่อง

คอมไพเลอร์จะไม่ได้รับข้อมูลเพียงพอที่จะ vectorise เราสามารถใช้ข้อกำหนด OpenMP สำหรับการเขียนโปรแกรมแบบขนานซึ่งยังมีคำสั่งคอมไพเลอร์simdเพื่อแนะนำคอมไพเลอร์เพื่อ vectorise รหัส มันเป็นสิ่งสำคัญเพื่อให้แน่ใจว่าไม่มีการซ้อนทับหน่วยความจำสภาวะการแข่งขัน ฯลฯ เมื่อทำการ vectorizing โค้ดด้วยตนเองมิฉะนั้นจะทำให้ผลลัพธ์ไม่ถูกต้อง

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

ด้วยการทำเช่นนี้เราขอให้คอมไพเลอร์ทำการ vectorise ให้ได้ไม่ว่าจะเกิดอะไรขึ้นก็ตาม เราจะต้องเปิดใช้งานส่วนขยาย 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


ในแง่นี้แม้ว่าหน้า Wikipedia บน Array Programmingกล่าวว่าภาษาที่ทำงานในอาเรย์ทั้งหมดมักจะเรียกมันว่าเป็นการดำเนินการแบบเวกเตอร์ แต่มันกำลังซ่อน IMO เอาไว้

ในกรณีของ R, แม้rowSums()หรือcolSums()รหัสใน C จะไม่ใช้ประโยชน์จากvectorisation IIUC; มันเป็นเพียงวงใน C. lapply()กันไปสำหรับ ในกรณีที่apply()ก็อยู่ในอาร์ทั้งหมดนี้จึงเป็นห่วงที่หลบซ่อนตัว

กล่าวโดยสรุปคือการห่อฟังก์ชัน R โดย:

เพียงแค่เขียนfor-loop in C! = vectorising รหัสของคุณ
เพียงแค่เขียนfor-loop in R! = vectorising รหัสของคุณ

ตัวอย่างเช่นIntel Math Kernel Library (MKL)ใช้ฟังก์ชั่นแบบเวกเตอร์

HTH


อ้างอิง:

  1. Talk โดย James Reinders, Intel (คำตอบนี้ส่วนใหญ่จะพยายามสรุปการพูดคุยที่ยอดเยี่ยมนี้)

35

ดังนั้นเพื่อรวมคำตอบ / ความคิดเห็นที่ยอดเยี่ยมไว้ในคำตอบทั่วไปและให้พื้นหลัง: R มีลูป 4 ประเภท ( จากลำดับที่ไม่ใช่เวกเตอร์ถึงเวกเตอร์ )

  1. R forloop ที่เรียกฟังก์ชั่น R ซ้ำ ๆ ในการวนซ้ำแต่ละครั้ง ( ไม่ใช่เวกเตอร์ )
  2. C loop ที่เรียกฟังก์ชัน R ซ้ำ ๆ ในการวนซ้ำแต่ละครั้ง ( ไม่ใช่เวกเตอร์ )
  3. C loop ที่เรียกใช้ฟังก์ชัน R เพียงครั้งเดียว (ค่อนข้างเวกเตอร์ )
  4. ลูป C ธรรมดาที่ไม่เรียกใช้ฟังก์ชัน R ใด ๆเลยและใช้มันเป็นฟังก์ชั่นที่คอมไพล์แล้ว ( Vectorized )

ดังนั้น *applyครอบครัวเป็นประเภทที่สอง ยกเว้นapplyประเภทแรกที่มากกว่า

คุณสามารถเข้าใจสิ่งนี้จากความคิดเห็นใน ซอร์สโค้ด

/ * .Internal (lapply (X, FUN)) * /

/ * นี่เป็น. ภายในพิเศษดังนั้นจึงมีข้อโต้แย้งที่ไม่ได้ประเมินค่า มันคือ
เรียกจากเสื้อคลุมปิดดังนั้น X และความสนุกจึงสัญญา FUN จะต้องไม่ได้รับการประเมินเพื่อใช้ในเช่น bquote * /

นั่นหมายความว่าlapplyรหัส C ยอมรับฟังก์ชั่นที่ไม่ได้ประเมินค่าจาก R และประเมินในภายหลังด้วยรหัส C เอง นี้นั้นเป็นความแตกต่างระหว่างlapplys .Internalโทร

.Internal(lapply(X, FUN))

ซึ่งมี FUNอาร์กิวเมนต์ที่มีฟังก์ชัน R

และcolMeans .Internalการเรียกร้องที่ไม่ได้มีFUNการโต้แย้ง

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

colMeansไม่เหมือนlapplyรู้แน่ชัดจำเป็นต้องใช้ฟังก์ชั่นใดดังนั้นจึงคำนวณค่าเฉลี่ยภายในรหัส C

คุณสามารถเห็นกระบวนการประเมินผลของฟังก์ชั่น R ในแต่ละการวนซ้ำได้อย่างชัดเจนภายในlapplyรหัส 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);
   }

เพื่อสรุปสิ่งต่าง ๆlapplyไม่ใช่เวกเตอร์แม้ว่าจะมีข้อดีสองประการที่เป็นไปได้มากกว่าforลูปR ธรรมดา

  1. การเข้าถึงและการกำหนดในลูปดูเหมือนจะเร็วกว่าใน 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
  2. ตามที่กล่าวถึงโดย @Roland มันรันคอมไพล์ C loop แทนที่จะเป็น R loop ที่ตีความ


แม้ว่าเมื่อ vectorizing รหัสของคุณมีบางสิ่งที่คุณต้องคำนึงถึง

  1. หากชุดข้อมูลของคุณ (ขอเรียกว่าdf) เป็นของชั้นdata.frame, ฟังก์ชั่น vectorized บางอย่าง (เช่นcolMeans, colSums, rowSumsฯลฯ ) จะมีการแปลงเป็นเมทริกซ์เป็นครั้งแรกเพียงเพราะนี้เป็นวิธีที่พวกเขาได้รับการออกแบบ ซึ่งหมายความว่าสำหรับขนาดใหญ่dfนี้สามารถสร้างค่าใช้จ่ายมาก ในขณะที่lapplyจะไม่ต้องทำเช่นนี้เป็นสารสกัดเวกเตอร์ที่เกิดขึ้นจริงจากdf(ในฐานะdata.frameเป็นเพียงรายการของเวกเตอร์) และดังนั้นหากคุณมีหลายคอลัมน์ไม่เป็นเช่นนั้น แต่แถวจำนวนมากบางครั้งอาจเป็นตัวเลือกที่ดีกว่าlapply(df, mean)colMeans(df)
  2. สิ่งที่ควรจดจำอีกอย่างคือ R มีฟังก์ชั่นที่แตกต่างหลากหลายเช่น.Primitiveและทั่วไป ( S3, S4) ดูที่นี่สำหรับข้อมูลเพิ่มเติม ฟังก์ชั่นทั่วไปต้องทำวิธีการจัดส่งซึ่งบางครั้งการดำเนินการค่าใช้จ่ายสูง ยกตัวอย่างเช่นmeanเป็นทั่วไปS3ฟังก์ชั่นในขณะที่เป็นsum Primitiveดังนั้นบางครั้งlapply(df, sum)อาจมีประสิทธิภาพมากเมื่อเทียบcolSumsกับเหตุผลที่กล่าวข้างต้น

1
สรุปเหนียวแน่นมาก เพียงไม่กี่หมายเหตุ: (1) C รู้วิธีจัดการ "data.frame" s เนื่องจากเป็น "list" s ที่มีคุณลักษณะ มันเป็นอย่างนั้นcolMeansเป็นต้นซึ่งถูกสร้างขึ้นเพื่อจัดการเมทริกซ์เท่านั้น (2) ฉันค่อนข้างสับสนกับหมวดหมู่ที่สามของคุณ ฉันไม่สามารถบอกได้ว่า -exaclty- คุณกำลังอ้างถึง (3) เนื่องจากคุณพูดถึงโดยเฉพาะlapplyฉันเชื่อว่ามันไม่ได้สร้างความแตกต่างระหว่าง"[<-"ใน R และ C พวกเขาทั้งสองจัดสรร "รายการ" (SEXP) ล่วงหน้าและเติมลงในการทำซ้ำแต่ละครั้ง ( SET_VECTOR_ELTใน C) เว้นแต่ฉันจะพลาดจุดของคุณ
alexis_laz

2
ฉันเข้าใจdo.callว่ามันสร้างการเรียกใช้ฟังก์ชันใน C environmen และเพียงแค่ประเมินมัน แม้ว่าฉันจะมีช่วงเวลาที่ยากลำบากในการเปรียบเทียบกับการวนซ้ำหรือ vectorization เนื่องจากมันทำสิ่งที่แตกต่าง จริง ๆ แล้วคุณถูกต้องเกี่ยวกับการเข้าถึงและการกำหนดความแตกต่างระหว่าง C และ R แม้ว่าทั้งคู่จะอยู่ในระดับไมโครวินาทีและจะไม่ส่งผลต่อผลลัพธ์ที่ได้อย่างมหาศาลเนื่องจากค่าใช้จ่ายสูงคือการเรียกใช้ฟังก์ชัน R ซ้ำ (เปรียบเทียบR_loopและR_lapplyในคำตอบของฉัน ) (ฉันจะแก้ไขโพสต์ของคุณด้วยมาตรฐาน; ฉันหวังว่าคุณยังคงไม่เป็นไร)
alexis_laz

2
ฉันไม่ได้พยายามที่จะไม่เห็นด้วย - และฉันก็สับสนอย่างตรงไปตรงมาเกี่ยวกับสิ่งที่คุณไม่เห็นด้วย ความคิดเห็นก่อนหน้าของฉันน่าจะพูดดีกว่านี้ได้ ฉันกำลังพยายามปรับแต่งคำศัพท์ที่ใช้เพราะคำว่า "vectorization" มีคำจำกัดความสองคำที่มักทำให้เป็นคำ ๆ ฉันไม่คิดว่ามันจะพิสูจน์ได้ เบิร์นและดูเหมือนว่าคุณต้องการใช้มันในแง่ของการนำไปใช้เท่านั้น แต่ Hadley และสมาชิก R-Core จำนวนมาก ( Vectorize()ซึ่งเป็นตัวอย่าง) ใช้มันในความรู้สึกของ UI เช่นกัน ฉันคิดว่าความขัดแย้งในหัวข้อนี้ส่วนใหญ่เกิดจากการใช้คำหนึ่งคำสำหรับแนวคิดที่แยกกัน แต่เกี่ยวข้องกันสองแนวคิด
Gregor Thomas

3
@DavidArenburg และนั่นไม่ใช่ vectorization ในแง่ UI ไม่ว่าจะมีลูปใน R หรือ C อยู่ด้านล่างหรือไม่
Gregor Thomas

2
@DavidArenburg, Gregor ฉันคิดว่าความสับสนอยู่ระหว่าง "code vectorisation" และ "vectorised function" ใน R การใช้งานดูเหมือนจะโน้มเอียงไปทางหลัง "รหัส vectorisation" อธิบายการปฏิบัติการบนเวกเตอร์ที่มีความยาว 'k' ในคำสั่งเดียวกัน ห่อ fn รอบผลรหัสวงแหวนใน "ฟังก์ชั่น vectorised" (ใช่ก็ไม่ได้ให้ความรู้สึกและความสับสนผมเห็นด้วยดีกว่าที่จะเป็นห่วงที่หลบซ่อนตัวหรือเวกเตอร์ I / ฟังก์ชั่น P ) และต้องไม่ได้มีอะไรจะทำอย่างไรกับรหัส vectorisation ใน R การนำไปใช้จะเป็นฟังก์ชันเวกเตอร์ แต่มันไม่ได้ทำให้โค้ดของคุณเป็นเวกเตอร์แทนที่จะทำงานกับเวกเตอร์
อรุณ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.