ครอบครัวของ R ใช้มากกว่าน้ำตาลประโยคหรือไม่?


152

... เกี่ยวกับเวลาดำเนินการและ / หรือหน่วยความจำ

หากนี่ไม่เป็นจริงให้พิสูจน์ด้วยโค้ดขนาดสั้น โปรดทราบว่าการเร่งความเร็วโดย vectorization จะไม่นับรวม speedup ต้องมาจากapply( tapply, sapply, ... ) ตัวเอง

คำตอบ:


152

applyฟังก์ชั่นในการวิจัยไม่ให้ประสิทธิภาพที่เพิ่มขึ้นมากกว่าฟังก์ชั่นการวนลูปอื่น ๆ (เช่นfor) ข้อยกเว้นประการหนึ่งสำหรับสิ่งนี้คือlapplyสิ่งที่สามารถทำได้เร็วกว่าเล็กน้อยเนื่องจากทำงานในรหัส C มากกว่าใน R (ดูคำถามนี้สำหรับตัวอย่างของสิ่งนี้ )

แต่โดยทั่วไปกฎก็คือว่าคุณควรจะใช้ฟังก์ชั่นใช้เพื่อความชัดเจนไม่ได้สำหรับผลการดำเนินงาน

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

แก้ไข:

เพียงเพื่อเน้นสิ่งนี้ด้วยตัวอย่างเล็ก ๆ น้อย ๆ ที่คำนวณลำดับฟีโบนักชีซ้ำ ๆ สิ่งนี้สามารถเรียกใช้หลายครั้งเพื่อให้ได้การวัดที่แม่นยำ แต่ประเด็นก็คือไม่มีวิธีใดที่มีประสิทธิภาพแตกต่างกันอย่างมีนัยสำคัญ:

> fibo <- function(n) {
+   if ( n < 2 ) n
+   else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
   user  system elapsed 
   7.48    0.00    7.52 
> system.time(sapply(0:26, fibo))
   user  system elapsed 
   7.50    0.00    7.54 
> system.time(lapply(0:26, fibo))
   user  system elapsed 
   7.48    0.04    7.54 
> library(plyr)
> system.time(ldply(0:26, fibo))
   user  system elapsed 
   7.52    0.00    7.58 

แก้ไข 2:

เกี่ยวกับการใช้งานของแพคเกจคู่ขนานสำหรับ R (เช่น rpvm, rmpi หิมะ) เหล่านี้โดยทั่วไปให้applyฟังก์ชั่นครอบครัว (แม้foreachแพคเกจจะเทียบเท่าเป็นหลักแม้จะมีชื่อ) นี่คือตัวอย่างง่ายๆของsapplyฟังก์ชั่นในsnow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

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

parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)

มันทำให้รู้สึกว่าapplyฟังก์ชั่นที่ควรจะใช้สำหรับการดำเนินการคู่ขนานตั้งแต่พวกเขาไม่มีผลข้างเคียง เมื่อคุณเปลี่ยนค่าตัวแปรภายในforลูปมันจะถูกตั้งค่าทั่วโลก ในทางกลับกันapplyฟังก์ชั่นทั้งหมดสามารถใช้ได้อย่างปลอดภัยในแบบคู่ขนานเนื่องจากการเปลี่ยนแปลงเป็นแบบเฉพาะกับการเรียกใช้ฟังก์ชัน (เว้นแต่คุณจะพยายามใช้assignหรือ<<-ในกรณีนี้คุณสามารถแนะนำผลข้างเคียง) จำเป็นอย่างยิ่งที่จะต้องพูดอย่างระมัดระวังเกี่ยวกับตัวแปรท้องถิ่นและระดับโลกโดยเฉพาะอย่างยิ่งเมื่อต้องรับมือกับการประมวลผลแบบขนาน

แก้ไข:

นี่คือตัวอย่างเล็ก ๆ น้อย ๆ ที่แสดงให้เห็นถึงความแตกต่างระหว่างforและ*applyเท่าที่มีผลข้างเคียงที่เกี่ยวข้อง:

> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
 [1]  1  2  3  4  5  6  7  8  9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
 [1]  6 12 18 24 30 36 42 48 54 60

หมายเหตุวิธีการdfในสภาพแวดล้อมของผู้ปกครองที่มีการเปลี่ยนแปลงโดยแต่ไม่for*apply


30
แพ็คเกจมัลติคอร์ส่วนใหญ่สำหรับ R ยังใช้การทำงานแบบขนานผ่านapplyตระกูลฟังก์ชัน ดังนั้นโปรแกรมการจัดโครงสร้างดังนั้นพวกเขาจึงใช้ใช้ทำให้พวกเขาสามารถขนานในราคาที่น้อยมาก
Sharpie

Sharpie - ขอบคุณสำหรับสิ่งนั้น! มีแนวคิดใดบ้างสำหรับตัวอย่างที่แสดงว่า (บน windows XP)
Tal Galili

5
ฉันขอแนะนำให้ดูsnowfallแพคเกจและลองตัวอย่างในบทความของพวกเขา snowfallสร้างบนsnowบรรจุภัณฑ์และสรุปรายละเอียดของการทำให้เป็นคู่ขนานยิ่งทำให้ง่ายต่อการเรียกใช้applyฟังก์ชั่นแบบขนาน
Sharpie

1
@Sharpie แต่ทราบว่าforeachตั้งแต่นั้นมากลายเป็นใช้ได้และดูเหมือนว่าจะมีการสอบถามมากเกี่ยวกับดังนั้น
Ari B. Friedman

1
@Shane ที่ด้านบนสุดของคำตอบของคุณคุณเชื่อมโยงไปยังคำถามอื่นเพื่อเป็นตัวอย่างของกรณีที่lapply"เร็วขึ้นเล็กน้อย" กว่าforลูป อย่างไรก็ตามที่นั่นฉันไม่เห็นอะไรเลยที่แนะนำ คุณเพียงกล่าวถึงว่าlapplyเร็วกว่าsapplyซึ่งเป็นข้อเท็จจริงที่รู้จักกันดีสำหรับเหตุผลอื่น ๆ ( sapplyพยายามลดความซับซ้อนของเอาต์พุตและด้วยเหตุนี้จึงต้องทำการตรวจสอบขนาดข้อมูลจำนวนมากและการแปลงที่เป็นไปได้) forไม่มีอะไรที่เกี่ยวข้องกับ ฉันพลาดอะไรไปรึเปล่า?
flodel

70

บางครั้งการเร่งความเร็วอาจมีความสำคัญเช่นเมื่อคุณต้องทำรังวนซ้ำเพื่อให้ได้ค่าเฉลี่ยโดยพิจารณาจากการจัดกลุ่มมากกว่าหนึ่งปัจจัย ที่นี่คุณมีสองวิธีที่ให้ผลลัพธ์ที่เหมือนกัน:

set.seed(1)  #for reproducability of the results

# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))

# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions 
#levels() and length() don't have to be called more than once.
  ylev <- levels(y)
  zlev <- levels(z)
  n <- length(ylev)
  p <- length(zlev)

  out <- matrix(NA,ncol=p,nrow=n)
  for(i in 1:n){
      for(j in 1:p){
          out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
      }
  }
  rownames(out) <- ylev
  colnames(out) <- zlev
  return(out)
}

# Used on the generated data
forloop(X,Y,Z)

# The same using tapply
tapply(X,list(Y,Z),mean)

ทั้งคู่ให้ผลลัพธ์เหมือนกันทั้งหมดโดยเป็นเมทริกซ์ขนาด 5 x 10 ที่มีค่าเฉลี่ยและชื่อแถวและคอลัมน์ แต่:

> system.time(forloop(X,Y,Z))
   user  system elapsed 
   0.94    0.02    0.95 

> system.time(tapply(X,list(Y,Z),mean))
   user  system elapsed 
   0.06    0.00    0.06 

ไปแล้ว ฉันชนะอะไร ;-)


อ้า :-) น่ารักจริง ๆ ฉันสงสัยว่าใครจะเจอคำตอบที่ค่อนข้างช้าของฉัน
Joris Meys

1
ฉันมักจะเรียงลำดับโดย "ใช้งาน" :) ไม่แน่ใจว่าจะสรุปคำตอบของคุณอย่างไร บางครั้ง*applyเร็วกว่า แต่ฉันคิดว่าจุดที่สำคัญกว่านั้นคือผลข้างเคียง (อัปเดตคำตอบของฉันพร้อมตัวอย่าง)
เชน

1
ฉันคิดว่าการใช้นั้นเร็วกว่าโดยเฉพาะเมื่อคุณต้องการใช้ฟังก์ชั่นกับชุดย่อยที่แตกต่างกัน หากมีวิธีการแก้ปัญหาการใช้งานที่ชาญฉลาดสำหรับลูปซ้อนกันฉันเดาว่าโซลูชันการสมัครจะเร็วขึ้นเช่นกัน ในกรณีส่วนใหญ่การใช้ไม่ได้รับความเร็วมากฉันเดา แต่ฉันเห็นด้วยกับผลข้างเคียงแน่นอน
Joris Meys

2
นี่เป็นหัวข้อเล็กน้อย แต่สำหรับตัวอย่างdata.tableนี้จะยิ่งเร็วขึ้นและฉันคิดว่า "ง่ายขึ้น" library(data.table) dt<-data.table(X,Y,Z,key=c("Y,Z")) system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
dnlbrky

12
การเปรียบเทียบนี้ไร้สาระ tapplyเป็นฟังก์ชั่นพิเศษสำหรับงานเฉพาะนั่นคือสาเหตุที่เร็วกว่าลูป มันไม่สามารถทำในสิ่งที่สำหรับห่วงสามารถทำ (ในขณะที่ปกติapplyสามารถ) คุณกำลังเปรียบเทียบแอปเปิ้ลกับส้ม
eddi

47

... และเมื่อฉันเพิ่งเขียนที่อื่นเพื่อนของคุณก็สมัครใจ! ... มันดูไพเราะ แต่คุณยังระบุประเภทค่าตอบแทนซึ่งทำให้เร็วขึ้นมาก

foo <- function(x) x+1
y <- numeric(1e6)

system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
#   user  system elapsed 
#   3.54    0.00    3.53 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#   2.89    0.00    2.91 
system.time(z <- vapply(y, foo, numeric(1)))
#   user  system elapsed 
#   1.35    0.00    1.36 

อัปเดตเมื่อวันที่ 1 มกราคม 2020:

system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
#   user  system elapsed 
#   0.52    0.00    0.53 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#   0.72    0.00    0.72 
system.time(z3 <- vapply(y, foo, numeric(1)))
#   user  system elapsed 
#    0.7     0.0     0.7 
identical(z1, z3)
# [1] TRUE

ผลการวิจัยต้นฉบับดูเหมือนจะไม่เป็นความจริงอีกต่อไป forการวนซ้ำเร็วขึ้นในคอมพิวเตอร์ Windows 10, 2-core ของฉัน ผมทำอย่างนี้กับ5e6องค์ประกอบ - ห่วงเป็น 2.9 วินาทีเทียบกับ 3.1 vapplyวินาที
โคล

27

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

foo <- function(x) {
   x <- x+1
 }
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
#   user  system elapsed 
#  4.967   0.049   7.293 
system.time(z <- sapply(y, foo))
#   user  system elapsed 
#  5.256   0.134   7.965 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#  2.179   0.126   3.301 

หากคุณวางแผนที่จะบันทึกผลแล้วใช้ฟังก์ชั่นในครอบครัวสามารถมากขึ้นกว่าน้ำตาลประโยค

(unlist ที่ง่ายของ z เป็นเพียง 0.2s ดังนั้น lapply เร็วกว่ามากการเริ่มต้น z ใน for for loop ค่อนข้างเร็วเพราะฉันให้ค่าเฉลี่ยของการวิ่ง 5 จาก 6 ครั้งล่าสุดดังนั้นการเคลื่อนที่นอกระบบ แทบจะไม่ส่งผลกระทบต่อสิ่งต่าง ๆ )

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

ยกตัวอย่าง Joris Meys ที่เขาแทนที่แบบดั้งเดิมสำหรับลูปด้วยฟังก์ชั่น R ที่มีประโยชน์เราสามารถใช้มันเพื่อแสดงประสิทธิภาพของการเขียนโค้ดในลักษณะที่เป็นมิตรกับ R มากขึ้นสำหรับการเร่งความเร็วที่คล้ายกันโดยไม่ต้องใช้ฟังก์ชั่นพิเศษ

set.seed(1)  #for reproducability of the results

# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))

# an R way to generate tapply functionality that is fast and 
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m

การทำแบบนี้จะเร็วกว่าforลูปมากและช้ากว่าtapplyฟังก์ชั่นที่ได้รับการปรับปรุงให้ดีขึ้นเล็กน้อย ไม่ใช่เพราะvapplyเร็วกว่ามากforแต่เป็นเพราะการดำเนินการเพียงครั้งเดียวในแต่ละรอบของการวนซ้ำ ในรหัสนี้ทุกอย่างอื่นจะถูกเวกเตอร์ ใน Joris Meys การforวนซ้ำแบบดั้งเดิมมีการดำเนินการจำนวนมาก (7?) เกิดขึ้นในแต่ละการวนซ้ำและมีการตั้งค่าค่อนข้างน้อย โปรดทราบว่าขนาดนี้กะทัดรัดกว่าforรุ่นเท่าใด


4
แต่ตัวอย่างของเชนเป็นจริงในการที่ใช้เวลาส่วนใหญ่จะใช้เวลาในการทำงานมักจะไม่ได้อยู่ในวง
hadley

9
พูดเพื่อตัวคุณเอง ... :) ... บางทีเชนอาจเป็นจริงในบางแง่มุม แต่ในแง่เดียวกันการวิเคราะห์นั้นไร้ประโยชน์อย่างมาก ผู้คนจะสนใจเกี่ยวกับความเร็วของกลไกการทำซ้ำเมื่อต้องทำซ้ำหลายครั้งไม่เช่นนั้นปัญหาจะอยู่ที่อื่น มันเป็นจริงของฟังก์ชั่นใด ๆ ถ้าฉันเขียนบาปที่ใช้เวลา 0.001 และอีกคนเขียนหนึ่งที่ใช้ 0.002 ใครสนใจ? ทันทีที่คุณต้องทำกลุ่มพวกเขาคุณสนใจ
จอห์น

2
ใน 12 core 3Ghz intel Xeon, 64 บิตฉันได้รับตัวเลขที่แตกต่างกันมากสำหรับคุณ - ห่วงสำหรับการปรับปรุงดีขึ้นมาก: สำหรับการทดสอบทั้งสามครั้งของคุณฉันได้รับ2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528และ vapply ดียิ่งขึ้น:1.19 0.00 1.19
naught101

2
มันแตกต่างกันไปตามระบบปฏิบัติการและรุ่น R ... และใน CPU ที่เหมาะสม ฉันเพิ่งรันด้วย 2.15.2 บน Mac และsapplyช้ากว่าforและlapplyเร็วกว่าถึง 50 เท่า
John

1
ในตัวอย่างของคุณหมายถึงชุดyที่จะ1:1e6ไม่numeric(1e6)(เวกเตอร์ของเลขศูนย์) การพยายามจัดสรรfoo(0)ให้z[0]ซ้ำแล้วซ้ำอีกไม่ได้แสดงให้เห็นถึงการforใช้งานลูปทั่วไป ข้อความจะปรากฏขึ้นเป็นอย่างอื่น
flodel

3

เมื่อใช้ฟังก์ชันกับชุดย่อยของเวกเตอร์tapplyอาจเร็วกว่าลูปสำหรับลูป ตัวอย่าง:

df <- data.frame(id = rep(letters[1:10], 100000),
                 value = rnorm(1000000))

f1 <- function(x)
  tapply(x$value, x$id, sum)

f2 <- function(x){
  res <- 0
  for(i in seq_along(l <- unique(x$id)))
    res[i] <- sum(x$value[x$id == l[i]])
  names(res) <- l
  res
}            

library(microbenchmark)

> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
   expr      min       lq   median       uq      max neval
 f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656   100
 f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273   100

applyอย่างไรก็ตามในสถานการณ์ส่วนใหญ่ไม่มีการเพิ่มความเร็วใด ๆ และในบางกรณีอาจช้ากว่านี้มาก:

mat <- matrix(rnorm(1000000), nrow=1000)

f3 <- function(x)
  apply(x, 2, sum)

f4 <- function(x){
  res <- 0
  for(i in 1:ncol(x))
    res[i] <- sum(x[,i])
  res
}

> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
    expr      min       lq   median       uq      max neval
 f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975   100
 f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100   100

แต่สำหรับสถานการณ์เหล่านี้เราได้colSumsและrowSums:

f5 <- function(x)
  colSums(x) 

> microbenchmark(f5(mat), times=100)
Unit: milliseconds
    expr      min       lq   median       uq      max neval
 f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909   100

7
มันเป็นสิ่งสำคัญที่จะแจ้งให้ทราบว่า (สำหรับชิ้นเล็ก ๆ ของรหัส) มันเป็นมากขึ้นได้อย่างแม่นยำกว่าmicrobenchmark system.timeหากคุณพยายามเปรียบเทียบsystem.time(f3(mat))และsystem.time(f4(mat))คุณจะได้รับผลลัพธ์ที่แตกต่างกันเกือบทุกครั้ง บางครั้งการทดสอบเกณฑ์มาตรฐานที่เหมาะสมเท่านั้นที่สามารถแสดงฟังก์ชันที่เร็วที่สุด
Michele
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.