[英]Why is the time complexity of this loop non-linear?
為什么這個循環的時間復雜度是非線性的,為什么它如此慢? ~38s for N=50k,
環路需要~38s for N=50k,
~570s for N=200k
~38s for N=50k,
環路~38s for N=50k,
需要~38s for N=50k,
。 有更快的方法嗎? Rprof()
似乎表明寫入內存非常慢。
df <- data.frame(replicate(5, runif(200000)))
df[,1:3] <- round(df[,1:3])
Rprof(line.profiling = TRUE); timer <- proc.time()
x <- df; N <- nrow(df); i <- 1
ind <- df[1:(N-1),1:3] == df[2:N,1:3];
rind <- which(apply(ind,1,all))
N <- length(rind)
while(i <= N)
{
x$X4[rind[i]+1] <- x$X4[rind[i]+1] + x$X4[rind[i]]
x$X5[rind[i]+1] <- x$X4[rind[i]+1] * x$X3[rind[i]+1]
x$X5[rind[i]+1] <- trunc(x$X5[rind[i]+1]*10^8)/10^8
x$X1[rind[i]] <- NA
i <- i + 1
};x <- na.omit(x)
proc.time() - timer; Rprof(NULL)
summaryRprof(lines = "show")
該算法的目的是迭代數據幀並組合在某些元素上匹配的相鄰行。 也就是說,它會刪除其中一行,並將該行的某些值添加到另一行。 結果數據幀應該少n行,其中n是原始數據幀中匹配的相鄰行的數量。 每次組合一對行時,源數據幀和新數據幀的索引不同步1,因為從新幀中刪除/省略了一行,所以i
跟蹤源數據上的位置幀和q
跟蹤新數據幀上的位置。
由於@joran的評論,上面的代碼更新了。 ~5.5s for N=50k
,性能大致提高到~5.5s for N=50k
~88s for N=200k
,性能提高到~88s for N=200k
。 然而,時間復雜性仍然是非線性的,我無法理解。 我需要在N = 100萬或更多時運行它,所以它仍然不是很快的速度。
只有X4
列更新取決於之前的值,因此循環可以大部分是“矢量化”(通過一些優化,避免在每次迭代中添加1到rind
)
rind1 <- rind + 1L
for (i in seq_len(N))
x$X4[rind1[i]] <- x$X4[rind1[i]] + x$X4[rind[i]]
x$X5[rind1] <- x$X4[rind1] * x$X3[rind1]
x$X5[rind1] <- trunc(x$X5[rind1] * 10^8) / 10^8
x$X1[rind] <- NA
na.omit(x)
X4
是一個數值,通過將其更新為向量而不是data.frame的列,可以使更新更有效
X4 <- x$X4
for (i in seq_len(N))
X4[rind1[i]] <- X4[rind1[i]] + X4[rind[i]]
x$X4 <- X4
為了比較,我們有
f0 <- function(nrow) {
set.seed(123)
df <- data.frame(replicate(5, runif(nrow)))
df[,1:3] <- round(df[,1:3])
x <- df; N <- nrow(df); i <- 1
ind <- df[1:(N-1),1:3] == df[2:N,1:3];
rind <- which(apply(ind,1,all))
N <- length(rind)
while(i <= N)
{
x$X4[rind[i]+1] <- x$X4[rind[i]+1] + x$X4[rind[i]]
x$X5[rind[i]+1] <- x$X4[rind[i]+1] * x$X3[rind[i]+1]
x$X5[rind[i]+1] <- trunc(x$X5[rind[i]+1]*10^8)/10^8
x$X1[rind[i]] <- NA
i <- i + 1
}
na.omit(x)
}
f1a <- function(nrow) {
set.seed(123)
df <- data.frame(replicate(5, runif(nrow)))
df[,1:3] <- round(df[,1:3])
x <- df; N <- nrow(df)
ind <- df[1:(N-1),1:3] == df[2:N,1:3];
rind <- which(apply(ind,1,all))
rind1 <- rind + 1L
for (i in seq_along(rind))
x$X4[rind1[i]] <- x$X4[rind1[i]] + x$X4[rind[i]]
x$X5[rind1] <- x$X4[rind1] * x$X3[rind1]
x$X5[rind1] <- trunc(x$X5[rind1] * 10^8) / 10^8
x$X1[rind] <- NA
na.omit(x)
}
f4a <- function(nrow) {
set.seed(123)
df <- data.frame(replicate(5, runif(nrow)))
df[,1:3] <- round(df[,1:3])
x <- df; N <- nrow(df)
ind <- df[1:(N-1),1:3] == df[2:N,1:3];
rind <- which(apply(ind,1,all))
rind1 <- rind + 1L
X4 <- x$X4
for (i in seq_along(rind))
X4[rind1[i]] <- X4[rind1[i]] + X4[rind[i]]
x$X4 <- X4
x$X1[rind] <- NA
x$X5[rind1] <- X4[rind1] * x$X3[rind1]
x$X5[rind1] <- trunc(x$X5[rind1] * 10^8) / 10^8
na.omit(x)
}
結果是一樣的
> identical(f0(1000), f1a(1000))
[1] TRUE
> identical(f0(1000), f4a(1000))
[1] TRUE
加速很快(使用library(microbenchmark)
)
> microbenchmark(f0(10000), f1a(10000), f4a(10000), times=10)
Unit: milliseconds
expr min lq mean median uq max neval
f0(10000) 346.35906 354.37637 361.15188 363.71627 366.74944 373.88275 10
f1a(10000) 124.71766 126.43532 127.99166 127.39257 129.51927 133.01573 10
f4a(10000) 41.70401 42.48141 42.90487 43.00584 43.32059 43.83757 10
在編譯R並啟用內存分析時可以看到差異的原因 -
> tracemem(x)
[1] "<0x39d93a8>"
> tracemem(x$X4)
[1] "<0x6586e40>"
> x$X4[1] <- 1
tracemem[0x39d93a8 -> 0x39d9410]:
tracemem[0x6586e40 -> 0x670d870]:
tracemem[0x39d9410 -> 0x39d9478]:
tracemem[0x39d9478 -> 0x39d94e0]: $<-.data.frame $<-
tracemem[0x39d94e0 -> 0x39d9548]: $<-.data.frame $<-
>
每行表示一個內存副本,因此更新數據幀中的單元格會產生5個外部結構副本或矢量本身。 相反,可以在沒有任何副本的情況下更新矢量。
> tracemem(X4)
[1] "<0xdd44460>"
> X4[1] = 1
tracemem[0xdd44460 -> 0x9d26c10]:
> X4[1] = 2
>
(第一個分配是昂貴的,因為它代表data.frame列的重復;后續更新到X4
,只有X4
引用正在更新的向量,並且向量不需要重復)。
data.frame實現似乎確實非線性擴展
> microbenchmark(f1a(100), f1a(1000), f1a(10000), f1a(100000), times=10)
Unit: milliseconds
expr min lq mean median uq
f1a(100) 2.372266 2.479458 2.551568 2.524818 2.640244
f1a(1000) 10.831288 11.100009 11.210483 11.194863 11.432533
f1a(10000) 130.011104 138.686445 139.556787 141.138329 141.522686
f1a(1e+05) 4092.439956 4117.818817 4145.809235 4143.634663 4172.282888
max neval
2.727221 10
11.581644 10
147.993499 10
4216.129732 10
原因在於上面tracemem輸出的第二行顯而易見 - 更新行會觸發整個列的副本。 因此,算法級表的行數更新時間行數列中,大約二次。
f4a()
似乎線性縮放
> microbenchmark(f4a(100), f4a(1000), f4a(10000), f4a(100000), f4a(1e6), times=10)
Unit: milliseconds
expr min lq mean median uq
f4a(100) 1.741458 1.756095 1.827886 1.773887 1.929943
f4a(1000) 5.286016 5.517491 5.558091 5.569514 5.671840
f4a(10000) 42.906895 43.025385 43.880020 43.928631 44.633684
f4a(1e+05) 467.698285 478.919843 539.696364 552.896109 576.707913
f4a(1e+06) 5385.029968 5521.645185 5614.960871 5573.475270 5794.307470
max neval
2.003700 10
5.764022 10
44.983002 10
644.927832 10
5823.868167 10
人們可以嘗試並且聰明地對矢量化循環,但現在是否有必要?
函數的數據處理部分的調整版本使用負索引(例如, -nrow(df)
)從數據框中刪除行, rowSums()
而不是apply()
和unname()
以便子集操作不要攜帶未使用的名字:
g0 <- function(df) {
ind <- df[-nrow(df), 1:3] == df[-1, 1:3]
rind <- unname(which(rowSums(ind) == ncol(ind)))
rind1 <- rind + 1L
X4 <- df$X4
for (i in seq_along(rind))
X4[rind1[i]] <- X4[rind1[i]] + X4[rind[i]]
df$X4 <- X4
df$X1[rind] <- NA
df$X5[rind1] <- trunc(df$X4[rind1] * df$X3[rind1] * 10^8) / 10^8
na.omit(df)
}
與@Khashaa建議的data.table解決方案相比
g1 <- function(df) {
x <- setDT(df)[, r:=rleid(X1, X2, X3),]
x <- x[, .(X1=X1[.N], X2=X2[.N], X3=X3[.N], X4=sum(X4), X5=X5[.N]), by=r]
x <- x[, X5:= trunc(X3 * X4 * 10^8)/10^8]
x
}
基本R版本隨着時間的推移表現良好
> n_row <- 200000
> set.seed(123)
> df <- data.frame(replicate(5, runif(n_row)))
> df[,1:3] <- round(df[,1:3])
> system.time(g0res <- g0(df))
user system elapsed
0.247 0.000 0.247
> system.time(g1res <- g1(df))
user system elapsed
0.551 0.000 0.551
(f4a中的預調整版本大約需要760毫秒,因此速度超過兩倍)。
data.table實現的結果不正確
> head(g0res)
X1 X2 X3 X4 X5
1 0 1 1 0.4708851 0.8631978
2 1 1 0 0.8977670 0.8311355
3 0 1 0 0.7615472 0.6002179
4 1 1 1 0.6478515 0.5616587
5 1 0 0 0.5329256 0.5805195
6 0 1 1 0.8526255 0.4913130
> head(g1res)
r X1 X2 X3 X4 X5
1: 1 0 1 1 0.4708851 0.4708851
2: 2 1 1 0 0.8977670 0.0000000
3: 3 0 1 0 0.7615472 0.0000000
4: 4 1 1 1 0.6478515 0.6478515
5: 5 1 0 0 0.5329256 0.0000000
6: 6 0 1 1 0.8526255 0.8526255
而且我還不夠data.table向導(幾乎不是data.table用戶)來了解正確的配方是什么。
編譯(僅從for循環中受益?)將速度提高約20%
> g0c <- compiler::cmpfun(g0)
> microbenchmark(g0(df), g0c(df), times=10)
Unit: milliseconds
expr min lq mean median uq max neval
g0(df) 250.0750 262.941 276.1549 276.8848 281.1966 321.3778 10
g0c(df) 214.3132 219.940 228.0784 230.2098 235.4579 242.6636 10
下面只是重寫@Martin Morgan的答案,利用data.table
的快速子集。 它比data.frame
方法快約3倍。
library(data.table)
library(matrixStats) # for efficient rowAlls function
g01 <- function(df) {
setDT(df)
ind <- df[-nrow(df), 1:3, with=FALSE] == df[-1, 1:3, with=FALSE]
rind <- which(rowAlls(ind)) + 1L
X4 <- df$X4
for (i in seq_along(rind))
X4[rind[i]] <- X4[rind[i]] + X4[rind[i] - 1L]
df$X4 <- X4
df$X5[rind] <- trunc(df$X4[rind] * df$X3[rind] * 10^8) / 10^8
df[-rind + 1L,]
}
g01c <- compiler::cmpfun(g01)
n_row <- 1e6
set.seed(123)
df <- data.frame(replicate(5, runif(n_row)))
df[,1:3] <- round(df[,1:3])
# data.frame
system.time(g0(df))
# user system elapsed
# 1.14 0.00 1.14
system.time(g0c(df))
# user system elapsed
# 0.82 0.03 0.86
# data.table
system.time(g01(df))
# user system elapsed
# 0.40 0.02 0.43
system.time(g01c(df))
# user system elapsed
# 0.12 0.03 0.16
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.