Là gia đình áp dụng R hơn đường cú pháp?


152

... Về thời gian thực hiện và / hoặc bộ nhớ.

Nếu điều này không đúng, hãy chứng minh bằng đoạn mã. Lưu ý rằng tăng tốc bằng cách vector hóa không được tính. Việc tăng tốc phải đến từ apply( tapply, sapply, ...) của chính nó.

Câu trả lời:


152

Các applyhàm trong R không cung cấp hiệu suất được cải thiện so với các hàm lặp khác (ví dụ for). Một ngoại lệ cho điều này là lapplycó thể nhanh hơn một chút vì nó hoạt động nhiều hơn trong mã C so với R (xem câu hỏi này để biết ví dụ về điều này ).

Nhưng nói chung, quy tắc là bạn nên sử dụng một hàm áp dụng cho rõ ràng, không phải cho hiệu suất .

Tôi sẽ thêm vào điều này rằng các hàm áp dụng không có tác dụng phụ , đó là một điểm khác biệt quan trọng khi nói đến lập trình chức năng với R. Điều này có thể được ghi đè bằng cách sử dụng assignhoặc <<-, nhưng điều đó có thể rất nguy hiểm. Tác dụng phụ cũng làm cho chương trình khó hiểu hơn vì trạng thái của biến phụ thuộc vào lịch sử.

Biên tập:

Chỉ cần nhấn mạnh điều này với một ví dụ tầm thường tính toán đệ quy chuỗi Fibonacci; điều này có thể được chạy nhiều lần để có được số đo chính xác, nhưng vấn đề là không có phương pháp nào có hiệu suất khác nhau đáng kể:

> 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 

Chỉnh sửa 2:

Về việc sử dụng các gói song song cho R (ví dụ: rpvm, rmpi, snow), chúng thường cung cấp các applychức năng gia đình (ngay cả foreachgói về cơ bản là tương đương, mặc dù tên). Đây là một ví dụ đơn giản về sapplyhàm trong snow:

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

Ví dụ này sử dụng cụm socket, không cần cài đặt thêm phần mềm nào; nếu không, bạn sẽ cần một cái gì đó như PVM hoặc MPI (xem trang phân cụm của Tierney ). snowcó các chức năng áp dụng sau:

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

Nó có ý nghĩa rằng các applychức năng nên được sử dụng để thực hiện song song vì chúng không có tác dụng phụ . Khi bạn thay đổi một giá trị biến trong một forvòng lặp, nó được đặt trên toàn cầu. Mặt khác, tất cả các applychức năng có thể được sử dụng song song một cách an toàn vì các thay đổi là cục bộ đối với lệnh gọi chức năng (trừ khi bạn cố gắng sử dụng assignhoặc <<-, trong trường hợp đó bạn có thể đưa ra các tác dụng phụ). Không cần phải nói, điều quan trọng là phải cẩn thận về các biến cục bộ và biến toàn cục, đặc biệt là khi xử lý thực thi song song.

Biên tập:

Đây là một ví dụ tầm thường để chứng minh sự khác biệt giữa for*applycho đến khi có liên quan đến tác dụng phụ:

> 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

Lưu ý làm thế nào dftrong môi trường cha mẹ bị thay đổi bởi fornhưng không *apply.


30
Hầu hết các gói đa lõi cho R cũng thực hiện song song thông qua applyhọ các hàm. Do đó, các chương trình cấu trúc để họ sử dụng áp dụng cho phép chúng được song song hóa với chi phí biên rất nhỏ.
Sharpie

Sharpie - cảm ơn bạn vì điều đó! Bất kỳ ý tưởng cho một ví dụ cho thấy rằng (trên windows XP)?
Tal Galili

5
Tôi sẽ đề nghị nhìn vào snowfallgói và thử các ví dụ trong họa tiết của họ. snowfallxây dựng trên đầu snowgói và trừu tượng hóa các chi tiết song song thậm chí còn khiến việc thực hiện các applyhàm song song trở nên đơn giản .
Sharpie

1
@Sharpie nhưng lưu ý rằng foreachđã trở nên có sẵn và dường như được hỏi nhiều về SO.
Ari B. Friedman

1
@Shane, ở đầu câu trả lời của bạn, bạn liên kết đến một câu hỏi khác như một ví dụ về trường hợp lapply"nhanh hơn một chút" so với forvòng lặp. Tuy nhiên, ở đó, tôi không thấy bất cứ điều gì gợi ý như vậy. Bạn chỉ đề cập rằng lapplynhanh hơn sapply, đó là một thực tế nổi tiếng vì những lý do khác ( sapplycố gắng đơn giản hóa đầu ra và do đó phải thực hiện nhiều kiểm tra kích thước dữ liệu và chuyển đổi tiềm năng). Không có gì liên quan đến for. Tui bỏ lỡ điều gì vậy?
flodel

70

Đôi khi tăng tốc có thể là đáng kể, như khi bạn phải lồng các vòng lặp để lấy trung bình dựa trên một nhóm nhiều hơn một yếu tố. Ở đây bạn có hai cách tiếp cận cho bạn kết quả chính xác như nhau:

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)

Cả hai đều cho kết quả chính xác như nhau, là một ma trận 5 x 10 với mức trung bình và các hàng và cột được đặt tên. Nhưng :

> 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 

Có bạn đi. Tôi đã giành được gì? ;-)


aah, thật ngọt ngào :-) Tôi thực sự tự hỏi liệu có ai gặp phải câu trả lời khá muộn của tôi không.
Joris Meys

1
Tôi luôn luôn sắp xếp theo "hoạt động". :) Không chắc chắn làm thế nào để khái quát câu trả lời của bạn; đôi khi *applynhanh hơn Nhưng tôi nghĩ rằng điểm quan trọng hơn là tác dụng phụ (cập nhật câu trả lời của tôi với một ví dụ).
Shane

1
Tôi nghĩ rằng việc áp dụng đặc biệt nhanh hơn khi bạn muốn áp dụng một chức năng trên các tập con khác nhau. Nếu có một giải pháp áp dụng thông minh cho một vòng lặp lồng nhau, tôi đoán giải pháp áp dụng cũng sẽ nhanh hơn. Trong hầu hết các trường hợp, áp dụng không đạt được nhiều tốc độ tôi đoán, nhưng tôi hoàn toàn đồng ý về các tác dụng phụ.
Joris Meys

2
Đây là một chủ đề nhỏ, nhưng đối với ví dụ cụ thể này, data.tablethậm chí còn nhanh hơn và tôi nghĩ "dễ dàng hơn". 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
Sự so sánh này là vô lý. tapplylà một chức năng chuyên biệt cho một nhiệm vụ cụ thể, đó là lý do tại sao nó nhanh hơn vòng lặp for. Nó không thể làm những gì một vòng lặp for có thể làm (trong khi thông thường applycó thể). Bạn đang so sánh táo với cam.
eddi

47

... Và như tôi vừa viết ở nơi khác, vapply là bạn của bạn! ... nó giống như sapply, nhưng bạn cũng chỉ định loại giá trị trả về làm cho nó nhanh hơn nhiều.

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 

Cập nhật ngày 1 tháng 1 năm 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

Những phát hiện ban đầu không còn đúng nữa. forcác vòng lặp nhanh hơn trên máy tính Windows 10, 2 lõi của tôi. Tôi đã làm điều này với 5e6các yếu tố - một vòng lặp là 2,9 giây so với 3,1 giây vapply.
Cole

27

Tôi đã viết ở một nơi khác rằng một ví dụ như Shane không thực sự nhấn mạnh sự khác biệt về hiệu suất giữa các loại cú pháp vòng lặp khác nhau bởi vì thời gian được sử dụng hết trong hàm thay vì thực sự nhấn mạnh vào vòng lặp. Hơn nữa, mã không công bằng so sánh một vòng lặp for không có bộ nhớ với các hàm gia đình áp dụng trả về một giá trị. Đây là một ví dụ hơi khác nhau nhấn mạnh điểm này.

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 

Nếu bạn có kế hoạch lưu kết quả thì áp dụng các chức năng gia đình có thể nhiều hơn so với đường cú pháp.

(danh sách đơn giản của z chỉ là 0,2 giây nên tốc độ nhanh hơn rất nhiều. Khởi tạo z trong vòng lặp for khá nhanh vì tôi cho trung bình 5 trên 6 lần chạy cuối cùng để di chuyển ra ngoài hệ thống. hầu như không ảnh hưởng đến mọi thứ)

Một điều nữa cần lưu ý là có một lý do khác để sử dụng các chức năng gia đình áp dụng độc lập với hiệu suất, sự rõ ràng hoặc thiếu tác dụng phụ của chúng. Một forvòng lặp thường khuyến khích đặt càng nhiều càng tốt trong vòng lặp. Điều này là do mỗi vòng lặp yêu cầu thiết lập các biến để lưu trữ thông tin (trong số các hoạt động có thể khác). Áp dụng các tuyên bố có xu hướng thiên vị theo cách khác. Thông thường, bạn muốn thực hiện nhiều thao tác trên dữ liệu của mình, một số thao tác có thể được vector hóa nhưng một số có thể không thực hiện được. Trong R, không giống như các ngôn ngữ khác, cách tốt nhất là tách các hoạt động đó ra và chạy các hoạt động không được vector hóa trong một câu lệnh áp dụng (hoặc phiên bản được vector hóa của hàm) và các hoạt động được vector hóa như các hoạt động vectơ thực sự. Điều này thường tăng tốc hiệu suất rất nhiều.

Lấy ví dụ về Joris Meys khi anh ta thay thế một vòng lặp truyền thống bằng hàm R tiện dụng, chúng ta có thể sử dụng nó để hiển thị hiệu quả của việc viết mã theo cách thân thiện hơn R để tăng tốc tương tự mà không cần chức năng chuyên biệt.

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

Điều này kết thúc nhanh hơn nhiều so với forvòng lặp và chỉ chậm hơn một chút so với tapplychức năng tối ưu hóa tích hợp. Không phải vì vapplynó nhanh hơn nhiều formà bởi vì nó chỉ thực hiện một thao tác trong mỗi lần lặp của vòng lặp. Trong mã này, mọi thứ khác được vector hóa. Trong forvòng lặp truyền thống của Joris Meys, nhiều thao tác (7?) Đang diễn ra trong mỗi lần lặp và có khá nhiều thiết lập chỉ để thực hiện. Cũng lưu ý rằng nó nhỏ gọn hơn bao nhiêu so với forphiên bản.


4
Nhưng ví dụ Shane là thực tế trong đó hầu hết thời gian thường chi tiêu trong chức năng, không phải trong vòng lặp.
hadley

9
nói cho chính mình ... :) ... Có thể Shane là hiện thực theo một nghĩa nào đó nhưng theo nghĩa tương tự, việc phân tích hoàn toàn vô dụng. Mọi người sẽ quan tâm đến tốc độ của cơ chế lặp khi họ phải thực hiện rất nhiều lần lặp, nếu không thì vấn đề của họ là ở nơi khác. Đó là sự thật của bất kỳ chức năng. Nếu tôi viết một tội lỗi mất 0,001 giây và người khác viết một tội lỗi mất 0,002 thì ai quan tâm chứ ?? Chà, ngay khi bạn phải làm một loạt chúng bạn quan tâm.
Giăng

2
trên intel Xeon 12 nhân 3Ghz, 64 bit, tôi nhận được các số khá khác nhau - vòng lặp for cải thiện đáng kể: đối với ba bài kiểm tra của bạn, tôi nhận được 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, và vapply thậm chí còn tốt hơn:1.19 0.00 1.19
naught 101

2
Nó thay đổi theo phiên bản OS và R ... và theo nghĩa tuyệt đối là CPU. Tôi mới chạy với 2.15.2 trên Mac và sapplychậm hơn 50% so với forlapplynhanh gấp đôi.
John

1
Trong ví dụ của bạn, bạn có nghĩa là đặt ythành 1:1e6, không numeric(1e6)(một vectơ số 0). Đang cố gắng để phân bổ foo(0)để z[0]lặp đi lặp không minh họa cũng là một điển hình forsử dụng vòng lặp. Thông điệp là tại chỗ trên.
flodel

3

Khi áp dụng các hàm trên các tập con của một vectơ, tapplycó thể nhanh hơn vòng lặp for. Thí dụ:

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

applytuy nhiên, trong hầu hết các tình huống không cung cấp bất kỳ sự tăng tốc nào và trong một số trường hợp có thể chậm hơn rất nhiều:

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

Nhưng đối với những tình huống này, chúng tôi đã có colSumsrowSums:

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
Điều quan trọng cần lưu ý là (đối với các đoạn mã nhỏ) microbenchmarknó chính xác hơn nhiều system.time. Nếu bạn cố gắng so sánh system.time(f3(mat))system.time(f4(mat))bạn sẽ nhận được kết quả khác nhau gần như mỗi lần. Đôi khi chỉ có một bài kiểm tra điểm chuẩn thích hợp là có thể hiển thị chức năng nhanh nhất.
Michele
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.