क्या "* लागू" परिवार वास्तव में सदिश नहीं है?


138

तो हम हर आर नए उपयोगकर्ता को यह कहने के लिए उपयोग किया जाता है कि " applyवेक्टर नहीं किया गया है, पैट्रिक बर्न्स आर इनफर्नो सर्कल 4 की जांच करें " जो कहता है (मैं बोली):

एक आम पलटा आवेदन परिवार में एक समारोह का उपयोग करना है। यह वैश्वीकरण नहीं है , यह लूप-छुपा है । लागू फ़ंक्शन की परिभाषा में लूप के लिए एक है। 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लूप छिपा नहीं है, बल्कि वे आंतरिक सी लिखित फ़ंक्शन को बुला रहे हैं।

खरगोश के छेद में एक त्वरित रूप से एक ही तस्वीर का पता चलता है

इसके अलावा, आइए 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) और पैट्रिक बर्न्स के वास्तव में कहने का क्या मतलब था?


8
यह सब शब्दार्थ में है, लेकिन मैं उन्हें सदिश नहीं मानता। यदि कोई R फ़ंक्शन केवल एक बार कॉल किया जाता है और मानों के वेक्टर को पास किया जा सकता है, तो मैं एक दृष्टिकोण पर विचार करता हूं। *applyफ़ंक्शंस बार-बार आर फ़ंक्शंस कहते हैं, जो उन्हें लूप बनाता है। अच्छे प्रदर्शन के बारे में sapply(m, mean): संभवतः सी-कोड ऑफ lapplyमेथड केवल एक बार प्रेषण करता है और फिर विधि को बार-बार कहता है? mean.defaultबहुत अनुकूलित है।
रोलैंड

4
उत्कृष्ट प्रश्न, और अंतर्निहित कोड की जांच के लिए धन्यवाद। मैं देख रहा था कि इसे हाल ही में बदल दिया गया है, लेकिन इसके संस्करण 2.13.0 से आर रिलीज़ नोट्स में इसके बारे में कुछ भी नहीं है।
इलिर

1
प्रदर्शन किस सीमा तक प्लेटफ़ॉर्म और सी-कंपाइलर और लिंकर झंडे दोनों पर निर्भर करता है?
एसएमसीआई

3
@DavidArenburg वास्तव में, मुझे नहीं लगता कि यह अच्छी तरह से परिभाषित है। कम से कम मैं एक विहित संदर्भ नहीं जानता। भाषा की परिभाषा में "वेक्टराइज्ड" संचालन का उल्लेख है, लेकिन वेक्टरीकरण को परिभाषित नहीं करता है।
रोलैंड

3
बहुत संबंधित: क्या सिंटिक शुगर की तुलना में आर का लागू परिवार है? (और, इन जवाबों की तरह, यह भी एक अच्छा पढ़ा।)
ग्रेगर थॉमस

जवाबों:


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/ आदि और सीधा आर-लूप के बीच मुख्य अंतर है जहां लूपिंग किया जाता है। रोलाण्ड नोट के रूप में, C और R दोनों छोरों को प्रत्येक पुनरावृत्ति में एक R फ़ंक्शन का मूल्यांकन करने की आवश्यकता होती है जो कि सबसे महंगा है। वास्तव में तेजी से सी फ़ंक्शन वे हैं जो सी में सब कुछ करते हैं, इसलिए, मुझे लगता है, यह होना चाहिए कि "सदिश" क्या है?

एक उदाहरण जहां हम "सूची" तत्वों में से प्रत्येक में माध्य पाते हैं:

( EDIT 11 मई 16) : मेरा मानना ​​है कि "मीन" खोजने के साथ उदाहरण आर फ़ंक्शन के मूल्यांकन के बीच के अंतर के लिए एक अच्छा सेटअप नहीं है। एक सरल sum(x) / length(x)और (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);
}

और आर साइड में:

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

10
एक मैट्रिक्स को data.frame परिवर्तित करने की लागत के बारे में महान बिंदु, और बेंचमार्क प्रदान करने के लिए धन्यवाद।
जोशुआ उलरिच

यह एक बहुत अच्छा जवाब है, हालांकि मैं आपके all_Cऔर C_and_Rकार्यों को संकलित करने में सक्षम नहीं था । मुझे compiler::cmpfunएक पुराने आर संस्करण के दस्तावेज में भी मिला, जिसमें एक वास्तविक आर forलूप शामिल है, मुझे संदेह है कि बर्न्स उस पुराने संस्करण का उल्लेख कर रहे थे , जो तब से वेक्टर किया गया था और यह मेरे प्रश्न का वास्तविक उत्तर है .. ..
डेविड ऐरनबर्ग

@DavidArenburg: बेंचमार्किंग la1से ?compiler::cmpfunलगता है, फिर भी, के साथ एक ही दक्षता उपज के लिए सभी लेकिनall_C कार्यों के । मुझे लगता है, यह -indeed- परिभाषा की बात आती है; "वेक्टराइज्ड" का अर्थ है कोई भी फ़ंक्शन जो केवल स्केलर्स को स्वीकार नहीं करता है, कोई भी फ़ंक्शन जिसमें C कोड है, कोई भी फ़ंक्शन जो केवल C में कंप्यूटेशन का उपयोग करता है?
अलेक्सिस_लज़

1
मुझे लगता है कि R में सभी फ़ंक्शन सी कोड हैं, बस इसलिए कि R में सब कुछ एक फ़ंक्शन है (जिसे किसी भाषा में लिखा जाना था)। इसलिए मूल रूप से, अगर मैं इसे सही समझ रहा हूं, तो आप कह रहे हैं कि lapplyयह केवल इसलिए सदिश नहीं किया गया है क्योंकि यह प्रत्येक पुनरावृत्ति में एक आर फ़ंक्शन का मूल्यांकन कर रहा है जो कि इसके सी कोड है?
डेविड ऐरनबर्ग

5
@ डेविडविरेनबर्ग: अगर मुझे किसी तरह "वैश्वीकरण" को परिभाषित करना चाहिए, तो मुझे लगता है, मैं एक भाषाई दृष्टिकोण चुनूंगा; यानी एक ऐसा फ़ंक्शन जो स्वीकार करता है और जानता है कि एक "वेक्टर" को कैसे संभालना है, चाहे वह तेज हो, धीमा हो, सी में लिखा हो, आर में या कुछ और। आर में, वैश्वीकरण का महत्व यह है कि कई फ़ंक्शन सी में लिखे गए हैं और वेक्टर्स को संभालते हैं जबकि अन्य भाषाओं में उपयोगकर्ता, आमतौर पर, इनपुट टू-लो पर माध्य पाते हैं। यह गति, दक्षता, सुरक्षा और मजबूती के साथ, परोक्ष रूप से, संबंधित करने के लिए वैश्वीकरण बनाता है।
alexis_laz

65

मेरे लिए, वैश्वीकरण मुख्य रूप से आपके कोड को लिखने और समझने में आसान बनाने के बारे में है।

एक वेक्टर किए गए फ़ंक्शन का लक्ष्य एक फॉर लूप से जुड़े पुस्तक-रखने को खत्म करना है। उदाहरण के लिए, इसके बजाय:

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

इससे यह देखना आसान हो जाता है कि क्या समान है (इनपुट डेटा) और क्या अलग है (जिस फ़ंक्शन को आप लागू कर रहे हैं)।

सदिशीकरण का एक माध्यमिक लाभ यह है कि आर में के बजाय फॉर-लूप को अक्सर C में लिखा जाता है। इसमें पर्याप्त प्रदर्शन लाभ होते हैं, लेकिन मुझे नहीं लगता कि यह सदिशीकरण की महत्वपूर्ण संपत्ति है। वेक्टराइज़ेशन मूल रूप से आपके मस्तिष्क को बचाने के बारे में है, कंप्यूटर के काम को बचाने के लिए नहीं।


5
मुझे नहीं लगता कि सी और आर forछोरों के बीच एक सार्थक प्रदर्शन अंतर है । ठीक है, संकलक द्वारा एक सी लूप अनुकूलित किया जा सकता है, लेकिन प्रदर्शन के लिए मुख्य बिंदु यह है कि क्या लूप की सामग्री कुशल है। और स्पष्ट रूप से संकलित कोड आमतौर पर व्याख्या किए गए कोड से तेज है। लेकिन शायद यही आपका कहना था।
रोलैंड

3
@ रोल हां, यह फॉर-लूप प्रति से ही नहीं है, यह इसके चारों ओर सामान है (फ़ंक्शन कॉल की लागत, जगह में संशोधन करने की क्षमता, ...)।
हैडली

10
@ डैविडअरेनबर्ग "नीडल कंसिस्टेंसी, छोटे दिमागों की
हॉजोब्लिन है

6
नहीं, मुझे नहीं लगता कि प्रदर्शन आपके कोड को वेक्टर करने का मुख्य बिंदु है। किसी भी तेजी से नहीं है, भले ही एक लूप को दुबले में बदलना फायदेमंद है। सपने देखने का मुख्य बिंदु यह है कि इससे डेटा हेरफेर को व्यक्त करना आसान हो जाता है (और यह बहुत अच्छा है कि यह भी तेज है)।
हैडली

12
@DavidArenburg ऐसा इसलिए है क्योंकि आप एक अनुभवी आर उपयोगकर्ता हैं। अधिकांश नए उपयोगकर्ता लूप्स को अधिक स्वाभाविक पाते हैं, और उन्हें वेक्टराइज़ करने के लिए प्रोत्साहित करने की आवश्यकता होती है। मेरे लिए, ColMeans जैसे फ़ंक्शन का उपयोग करना वैश्वीकरण के बारे में आवश्यक नहीं है, यह तेज़ कोड के पुन: उपयोग के बारे में है जो किसी ने पहले ही लिखा है
हैडली

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एक समय में एक ही निर्देश के तहत, के बजाय।

हम सिंगल इंस्ट्रक्शंस मल्टीपल डेटा का दोहन करना चाहते हैं और डेटा लेवल समानता का प्रदर्शन करते हैं , यानी एक बार में 4 चीजें लोड करते हैं, समय पर 4 चीजें जोड़ते हैं, एक बार में 4 चीजें स्टोर करते हैं, उदाहरण के लिए। और यह कोड सदिश है

ध्यान दें कि यह कोड समांतरिकरण से भिन्न है - जहां कई संगणनाएँ समवर्ती रूप से की जाती हैं।

यह बहुत अच्छा होगा यदि कंपाइलर कोड के ऐसे ब्लॉकों की पहचान करता है और स्वचालित रूप से उन्हें वेक्टर करता है, जो एक मुश्किल काम है। कंप्यूटर विज्ञान में स्वचालित कोड सदिशीकरण एक चुनौतीपूर्ण शोध विषय है। लेकिन समय के साथ, कंपाइलरों ने इसे बेहतर बना दिया है। आप यहां ऑटो वेक्टराइजेशन क्षमताओं की जांच कर सकते GNU-gcc हैं । इसी तरह LLVM-clang यहाँ के लिए । और तुम भी के खिलाफ तुलना में पिछले कड़ी में कुछ मानक पा सकते हैं gccऔर ICC(इंटेल सी ++ संकलक)।

gcc(मैं पर v4.9) उदाहरण के लिए कोड को स्वचालित रूप से सदिश नहीं करता है-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

निरंतरता खोए बिना इस हिस्से को सुरक्षित रूप से छोड़ दिया जा सकता है।

कंपाइलरों को हमेशा वेक्टर की पर्याप्त जानकारी नहीं होगी। हम समानांतर प्रोग्रामिंग के लिए ओपनएमपी विनिर्देश का उपयोग कर सकते हैं , जो कोड को वेक्टर करने के लिए संकलक को निर्देश देने के लिए एक सीएमडी संकलक निर्देश भी प्रदान करता है । यह सुनिश्चित करना आवश्यक है कि कोई मेमोरी ओवरलैप, दौड़ की स्थिति आदि नहीं हैं। जब कोड को मैन्युअल रूप से तैयार किया जाता है, अन्यथा यह गलत परिणाम देगा।

#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 (दोनों होमब्रे, मैकओएस 10.12.3 के माध्यम से स्थापित) के साथ परीक्षण किया गया था, दोनों ओपनएम 4.0 का समर्थन करते हैं।


इस अर्थ में, भले ही ऐरे प्रोग्रामिंग पर विकिपीडिया पृष्ठ में उल्लेख किया गया है कि संपूर्ण सरणियों पर काम करने वाली भाषाओं को आमतौर पर वेक्टरकृत संचालन के रूप में कहा जाता है , यह वास्तव में लूप छिपाना आईएमओ है (जब तक कि यह वास्तव में सदिश नहीं है)।

आर के मामले में, यहां तक ​​कि rowSums()या colSums()सी में कोड वेक्टर कोड IIUC का शोषण नहीं करते हैं ; यह सी। में एक लूप है lapply()। के मामले में apply(), यह आर में है। ये सभी लूप छुपा रहे हैं

संक्षेप में, एक आर फ़ंक्शन को लपेटकर:

सिर्फ एक लेखन के लिए लूप में C! = अपने कोड vectorising।
सिर्फ एक लेखन के लिए लूप में R! = अपने कोड vectorising।

इंटेल मठ कर्नेल लाइब्रेरी (एमकेएल) उदाहरण के लिए कार्यों के सदिश रूपों को लागू करता है।

HTH


संदर्भ:

  1. जेम्स रेइंडर्स, इंटेल द्वारा बात (यह जवाब ज्यादातर इस उत्कृष्ट बात को संक्षेप में प्रस्तुत करने का एक प्रयास है)

35

तो कुछ सामान्य उत्तर में महान जवाब / टिप्पणियों को समेटने और कुछ पृष्ठभूमि प्रदान करने के लिए: आर में 4 प्रकार के लूप हैं ( न कि सदिश से सदिश क्रम में )

  1. आर forलूप जिसे बार-बार प्रत्येक पुनरावृत्तियों में आर फ़ंक्शन कहते हैं ( वेक्टर नहीं किया गया )
  2. सी लूप जो बार-बार प्रत्येक पुनरावृत्तियों में आर फ़ंक्शन को कॉल करता है ( वेक्टर नहीं किया गया है )
  3. सी लूप जो केवल एक बार आर फ़ंक्शन को कहते हैं ( कुछ हद तक वेक्टरकृत )
  4. एक सादा लूप जो किसी भी आर फंक्शन को कॉल नहीं करता है और इसे स्वयं संकलित फ़ंक्शन ( वेक्टरकृत ) का उपयोग करता है

तो *applyपरिवार दूसरा प्रकार है। सिवाय applyजो पहले प्रकार का अधिक है

आप इसे इसके स्रोत कोड में टिप्पणी से समझ सकते हैं

/ * .इंटरनल (lapply (X, FUN)) * /

/ * यह एक विशेष है। शाश्वत, इस तरह के अनावश्यक तर्क हैं। इसे
क्लोजर रैपर से बुलाया जाता है, इसलिए X और FUN वादे हैं। FUN का उपयोग करने के लिए unqualuated किया जाना चाहिए जैसे bquote। * /

इसका मतलब है कि lapplys कोड R से एक अनवैलिडेटेड फंक्शन को स्वीकार करता है और बाद में C कोड के अंदर ही इसका मूल्यांकन करता है। यह मूल रूप से lapplys .Internalकॉल के बीच का अंतर है

.Internal(lapply(X, FUN))

जिसमें ए है FUN तर्क है जो एक आर फ़ंक्शन रखता है

और जिस colMeans .Internalकॉल का कोई तर्क नहीं हैFUN

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

colMeans, इसके विपरीत बिल्कुलlapply जानता है कि इसे किस फ़ंक्शन का उपयोग करने की आवश्यकता है, इस प्रकार यह सी कोड के भीतर आंतरिक रूप से माध्य की गणना करता है।

आप सी कोड के भीतर प्रत्येक पुनरावृत्ति में आर फ़ंक्शन की मूल्यांकन प्रक्रिया को स्पष्ट रूप से देख सकते हैं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);
   }

चीजों को योग करने के लिए, lapplyइसे सदिश नहीं किया जाता है , हालांकि सादे आर forलूप पर इसके दो संभावित फायदे हैं

  1. एक लूप में पहुंचना और असाइन करना C में तेज होता है (यानी lapplyकिसी फ़ंक्शन में) हालांकि अंतर बड़ा लगता है, हम, फिर भी, माइक्रोसेकंड स्तर पर रहते हैं और महंगी चीज प्रत्येक पुनरावृत्ति में आर फ़ंक्शन का मूल्यांकन है। एक सरल उदाहरण:

    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. जैसा कि @ रोलैंड द्वारा उल्लेख किया गया है, यह एक आर लूप के बजाय संकलित सी लूप चलाता है


हालांकि आपके कोड को वेक्टर करते समय, कुछ चीजें हैं जिन्हें आपको ध्यान में रखना चाहिए।

  1. अपने डेटा सेट (आइए यह कॉल तो df) वर्ग की है data.frame, कुछ vectorized कार्यों (जैसे colMeans, colSums, rowSums, आदि) बस क्योंकि यह कि वे किस तरह डिजाइन किए गए थे, एक मैट्रिक्स के लिए यह पहली कन्वर्ट करने के लिए होगा। इसका मतलब है कि एक बड़े के लिए dfयह एक बहुत बड़ा ओवरहेड बना सकता है। हालांकि lapplyऐसा नहीं करना होगा क्योंकि यह वास्तविक वैक्टर को बाहर निकालता है df(जैसा data.frameकि वैक्टर की एक सूची है) और इस प्रकार, यदि आपके पास इतने कॉलम नहीं हैं, लेकिन कई पंक्तियां हैं, तो lapply(df, mean)कभी-कभी इससे बेहतर विकल्प हो सकता है colMeans(df)
  2. याद रखने वाली एक और बात यह है कि आर में विभिन्न प्रकार के शानदार प्रकार हैं, जैसे कि .Primitive, और जेनेरिक ( S3, S4) कुछ अतिरिक्त जानकारी के लिए यहां देखें। जेनेरिक फ़ंक्शन को एक विधि प्रेषण करना पड़ता है जो कभी-कभी एक महंगा ऑपरेशन होता है। उदाहरण के लिए, meanसामान्य S3कार्य है जबकि sumहै Primitive। इस प्रकार ऊपर सूचीबद्ध कारणों lapply(df, sum)की तुलना में कुछ समय बहुत कुशल हो सकता colSumsहै

1
बहुत सामंजस्यपूर्ण सारांश। बस कुछ नोट: (1) सी जानता है कि "data.frame" को कैसे संभालना है, क्योंकि वे विशेषताओं के साथ "सूची" हैं; यह है कि colMeansआदि कि केवल matrices को संभालने के लिए बनाया गया है। (२) मैं आपकी तीसरी श्रेणी से थोड़ा भ्रमित हूँ; मैं नहीं बता सकता कि क्या -एक्सैक्लटी- का जिक्र कर रहे हैं। (3) चूंकि आप विशेष रूप से संदर्भित कर रहे हैं lapply, मेरा मानना ​​है कि यह "[<-"आर और सी के बीच अंतर नहीं करता है; वे दोनों एक "सूची" (एक SEXP) पूर्व-आवंटित करते हैं और इसे प्रत्येक पुनरावृत्ति ( SET_VECTOR_ELTC) में भरते हैं , जब तक कि मैं अपनी बात याद नहीं कर रहा हूं।
alexis_laz

2
मैं इस बारे do.callमें आपकी बात प्राप्त करता हूं कि यह C environmen में एक फ़ंक्शन कॉल बनाता है और बस इसका मूल्यांकन करता है; हालांकि मुझे इसे पाटने या वैश्वीकरण से तुलना करने का कठिन समय है क्योंकि यह एक अलग बात करता है। आप वास्तव में, सी और आर के बीच अंतर तक पहुँचने और असाइन करने के बारे में सही हैं, हालांकि दोनों माइक्रोसेकंड स्तर पर रहते हैं और परिणाम को फिर से बहुत प्रभावित नहीं करते हैं, क्योंकि महंगा पुनरावृत्ति आर फ़ंक्शन कॉल है (तुलना करें R_loopऔर R_lapplyमेरे जवाब में) )। (मैं आपके पोस्ट को एक बेंचमार्क के साथ संपादित करूँगा; मुझे आशा है कि आप अभी भी, बुरा नहीं
मानेंगे

2
मैं असहमत होने की कोशिश नहीं कर रहा हूं --- और मैं उलझन में हूँ, ईमानदारी से, जो आप असहमत हैं। मेरी पहले की टिप्पणी को बेहतर तरीके से कहा जा सकता था। मैं प्रयोग की जा रही शब्दावली को परिष्कृत करने की कोशिश कर रहा हूं क्योंकि शब्द "वैश्वीकरण" की दो परिभाषाएं हैं जो अक्सर भ्रमित होती हैं। मुझे नहीं लगता कि यह तर्कपूर्ण है। बर्न्स और आप इसे केवल कार्यान्वयन के अर्थ में उपयोग करना चाहते हैं, लेकिन हैडली और कई आर-कोर सदस्य ( Vectorize()उदाहरण के रूप में) यूआई अर्थ में भी इसका उपयोग करते हैं। मुझे लगता है कि इस धागे में बहुत असहमति दो अलग-लेकिन-संबंधित अवधारणाओं के लिए एक शब्द का उपयोग करने के कारण होती है।
ग्रिगोर थॉमस

3
@DavidArenburg और क्या यह यूआई अर्थ में वैश्वीकरण नहीं है, इस बात की परवाह किए बिना कि आर या सी में लूप है या नहीं?
ग्रेगर थॉमस

2
@ डेविडविरेनबर्ग, ग्रेगोर, मुझे लगता है कि भ्रम "कोड सदिशीकरण" और "सदिश कार्यों" के बीच है। आर में, उपयोग बाद की ओर झुका हुआ लगता है। "कोड सदिशीकरण" एक ही निर्देश में लंबाई 'के' के एक वेक्टर पर संचालन का वर्णन करता है। एक fn लपेटकर। "सदिश फ़ंक्शंस" में लगभग लूप कोड परिणाम होते हैं (हां, इसका कोई मतलब नहीं है और यह भ्रमित करने वाला है, मैं सहमत हूं, बेहतर होगा लूप छिपाना या वेक्टर आई / पी फ़ंक्शन ) और कोड सदिशीकरण के साथ कुछ भी करने की आवश्यकता नहीं है । आर में, लागू एक वेक्टराइज़्ड फ़ंक्शन होगा , लेकिन यह आपके कोड को वेक्टर नहीं करता है, बल्कि वैक्टर पर काम करता है।
अरुण
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.