Data.table: warum data.table mit einer vektorisierten Spaltenuntermenge schneller ist als eine Listenspaltenuntermenge

Erstellt am 27. März 2019  ·  3Kommentare  ·  Quelle: Rdatatable/data.table

Ich mag dieses data.table-Zeug, sowohl wegen seiner Ausführungsgeschwindigkeit als auch wegen seiner sparsamen Art der Skripterstellung.
Ich benutze es auch auf kleinen Tischen.
Ich unterteile Tabellen regelmäßig auf diese Weise: DT[, .(id1, id5)]
und nicht so: DT[, c("id1", "id5")]

Heute habe ich die Geschwindigkeit der beiden gemessen und war erstaunt über den Geschwindigkeitsunterschied auf kleinen Tischen. Die sparsame Methode ist viel langsamer.

Ist dieser Unterschied gewollt?

Gibt es den Anspruch, den sparsamen Weg zu gehen, um sich in Bezug auf die Ausführungsgeschwindigkeit dem anderen anzunähern?
(Es zählt, wenn ich mehrere kleine Tabellen wiederholt unterteilen muss.)

Ubuntu 18.04
R-Version 3.5.3 (2019-03-11)
data.table 1.12.0
Arbeitsspeicher 32 GB
Intel® Core™ i7-8565U CPU @ 1,80 GHz × 8

library(data.table)
library(microbenchmark)
N  <- 2e8
K  <- 100
set.seed(1)
DT <- data.table(
  id1 = sample(sprintf("id%03d", 1:K), N, TRUE),               # large groups (char)
  id2 = sample(sprintf("id%03d", 1:K), N, TRUE),               # large groups (char)
  id3 = sample(sprintf("id%010d", 1:(N/K)), N, TRUE),       # small groups (char)
  id4 = sample(K,   N, TRUE),                                           # large groups (int)
  id5 = sample(K,   N, TRUE),                                           # large groups (int)
  id6 = sample(N/K, N, TRUE),                                          # small groups (int)
  v1 =  sample(5,   N, TRUE),                                           # int in range [1,5]
  v2 =  sample(5,   N, TRUE),                                           # int in range [1,5]
  v3 =  sample(round(runif(100, max = 100), 4), N, TRUE) # numeric e.g. 23.5749
)

microbenchmark(
  DT[, .(id1, id5)],
  DT[, c("id1", "id5")]
)
Unit: seconds
                  expr      min       lq     mean   median       uq      max neval
     DT[, .(id1, id5)] 1.588367 1.614645 1.929348 1.626847 1.659698 12.33872   100
 DT[, c("id1", "id5")] 1.592154 1.613800 1.937548 1.628082 2.184456 11.74581   100


N  <- 2e5
DT2 <- data.table(
  id1 = sample(sprintf("id%03d", 1:K), N, TRUE),                 # large groups (char)
  id2 = sample(sprintf("id%03d", 1:K), N, TRUE),                 # large groups (char)
  id3 = sample(sprintf("id%010d", 1:(N/K)), N, TRUE),         # small groups (char)
  id4 = sample(K,   N, TRUE),                                             # large groups (int)
  id5 = sample(K,   N, TRUE),                                             # large groups (int)
  id6 = sample(N/K, N, TRUE),                                            # small groups (int)
  v1 =  sample(5,   N, TRUE),                                             # int in range [1,5]
  v2 =  sample(5,   N, TRUE),                                             # int in range [1,5]
  v3 =  sample(round(runif(100, max = 100), 4), N, TRUE)   # numeric e.g. 23.5749
)

microbenchmark(
  DT2[, .(id1, id5)],
  DT2[, c("id1", "id5")]
)
Unit: microseconds
                   expr      min       lq      mean    median        uq      max neval
DT2[, .(id1, id5)] 1405.042 1461.561 1525.5314 1491.7885 1527.8955 2220.860   100
DT2[, c("id1", "id5")]  614.624  640.617  666.2426  659.0175  676.9355  906.966   100

Hilfreichster Kommentar

Sie können die Formatierung Ihres Beitrags korrigieren, indem Sie vor und nach dem Codeabschnitt eine einzelne Zeile mit drei Backticks verwenden:

```
code
```

Es zählt, wenn ich mehrere kleine Tabellen wiederholt unterteilen muss.

Ich denke, das wiederholte Auswählen von Spalten aus kleinen Tabellen ist etwas, das vermieden werden sollte und in den meisten Fällen vermieden werden kann ...? Da j in DT[i, j, by] eine so große Vielfalt an Eingaben unterstützt und optimiert, denke ich, dass es natürlich ist, dass es etwas Overhead beim Parsen gibt.


In Bezug auf andere Möglichkeiten, Ihr Problem anzugehen (und vielleicht passt dies besser zu Stack Overflow, wenn Sie mehr darüber sprechen möchten) ... Je nachdem, was Sie sonst noch mit der Tabelle machen möchten, können Sie einfach die anderen Spalten löschen , DT[, setdiff(names(DT), cols) := NULL] und verwenden Sie DT direkt weiter.

Wenn Sie immer noch lieber die Teilmenge nehmen möchten, ist das Greifen von Spaltenzeigern viel schneller als jede der hier in Betracht gezogenen Optionen, obwohl sich Änderungen am Ergebnis auf diese Weise auf die ursprüngliche Tabelle auswirken:

library(data.table)
library(microbenchmark)
N <- 2e8
K <- 100
set.seed(1)
DT <- data.table(
id1 = sprintf("id%03d", 1:K), # large groups (char)
id2 = sprintf("id%03d", 1:K), # large groups (char)
id3 = sprintf("id%010d", 1:(N/K)), # small groups (char)
id4 = sample(K), # large groups (int)
id5 = sample(K), # large groups (int)
id6 = sample(N/K), # small groups (int)
v1 = sample(5), # int in range [1,5]
v2 = sample(5), # int in range [1,5]
v3 = round(runif(100, max = 100), 4), # numeric e.g. 23.5749
row = seq_len(N)
)

cols = c("id1", "id5")
microbenchmark(times = 3,
  expression = DT[, .(id1, id5)],
  index = DT[, c("id1", "id5")],
  dotdot = DT[, ..cols],
  oddball = setDT(lapply(setNames(cols, cols), function(x) DT[[x]]))[],
  oddball2 = setDT(unclass(DT)[cols])[]
)

Unit: microseconds
       expr         min           lq         mean      median           uq         max neval
 expression 1249753.580 1304355.3415 1417166.9297 1358957.103 1500873.6045 1642790.106     3
      index 1184056.302 1191334.4835 1396372.3483 1198612.665 1502530.3715 1806448.078     3
     dotdot 1084521.234 1240062.2370 1439680.6980 1395603.240 1617260.4300 1838917.620     3
    oddball      92.659     171.8635     568.5317     251.068     806.4680    1361.868     3
   oddball2      66.582     125.9505     150.7337     185.319     192.8095     200.300     3

(Ich habe die Randomisierung aus Ihrem Beispiel herausgenommen und im Benchmark # Mal reduziert, weil ich ungeduldig war.)

Ich habe nie eine Möglichkeit gefunden, die Listenteilmenge von R direkt aufzurufen (die nach den unclass oben verwendet wird).

In Bezug auf "Bearbeitungen des Ergebnisses ändern die ursprüngliche Tabelle" meine ich:

myDT = data.table(a = 1:2, b = 3:4)

# standard way
res <- myDT[, "a"]
res[, a := 0]
myDT
#    a b
# 1: 1 3
# 2: 2 4

# oddball, grabbing pointers
res2 <- setDT(unclass(myDT)["a"])
res2[, a := 0]
myDT
#    a b
# 1: 0 3
# 2: 0 4

Alle 3 Kommentare

Sie können die Formatierung Ihres Beitrags korrigieren, indem Sie vor und nach dem Codeabschnitt eine einzelne Zeile mit drei Backticks verwenden:

```
code
```

Es zählt, wenn ich mehrere kleine Tabellen wiederholt unterteilen muss.

Ich denke, das wiederholte Auswählen von Spalten aus kleinen Tabellen ist etwas, das vermieden werden sollte und in den meisten Fällen vermieden werden kann ...? Da j in DT[i, j, by] eine so große Vielfalt an Eingaben unterstützt und optimiert, denke ich, dass es natürlich ist, dass es etwas Overhead beim Parsen gibt.


In Bezug auf andere Möglichkeiten, Ihr Problem anzugehen (und vielleicht passt dies besser zu Stack Overflow, wenn Sie mehr darüber sprechen möchten) ... Je nachdem, was Sie sonst noch mit der Tabelle machen möchten, können Sie einfach die anderen Spalten löschen , DT[, setdiff(names(DT), cols) := NULL] und verwenden Sie DT direkt weiter.

Wenn Sie immer noch lieber die Teilmenge nehmen möchten, ist das Greifen von Spaltenzeigern viel schneller als jede der hier in Betracht gezogenen Optionen, obwohl sich Änderungen am Ergebnis auf diese Weise auf die ursprüngliche Tabelle auswirken:

library(data.table)
library(microbenchmark)
N <- 2e8
K <- 100
set.seed(1)
DT <- data.table(
id1 = sprintf("id%03d", 1:K), # large groups (char)
id2 = sprintf("id%03d", 1:K), # large groups (char)
id3 = sprintf("id%010d", 1:(N/K)), # small groups (char)
id4 = sample(K), # large groups (int)
id5 = sample(K), # large groups (int)
id6 = sample(N/K), # small groups (int)
v1 = sample(5), # int in range [1,5]
v2 = sample(5), # int in range [1,5]
v3 = round(runif(100, max = 100), 4), # numeric e.g. 23.5749
row = seq_len(N)
)

cols = c("id1", "id5")
microbenchmark(times = 3,
  expression = DT[, .(id1, id5)],
  index = DT[, c("id1", "id5")],
  dotdot = DT[, ..cols],
  oddball = setDT(lapply(setNames(cols, cols), function(x) DT[[x]]))[],
  oddball2 = setDT(unclass(DT)[cols])[]
)

Unit: microseconds
       expr         min           lq         mean      median           uq         max neval
 expression 1249753.580 1304355.3415 1417166.9297 1358957.103 1500873.6045 1642790.106     3
      index 1184056.302 1191334.4835 1396372.3483 1198612.665 1502530.3715 1806448.078     3
     dotdot 1084521.234 1240062.2370 1439680.6980 1395603.240 1617260.4300 1838917.620     3
    oddball      92.659     171.8635     568.5317     251.068     806.4680    1361.868     3
   oddball2      66.582     125.9505     150.7337     185.319     192.8095     200.300     3

(Ich habe die Randomisierung aus Ihrem Beispiel herausgenommen und im Benchmark # Mal reduziert, weil ich ungeduldig war.)

Ich habe nie eine Möglichkeit gefunden, die Listenteilmenge von R direkt aufzurufen (die nach den unclass oben verwendet wird).

In Bezug auf "Bearbeitungen des Ergebnisses ändern die ursprüngliche Tabelle" meine ich:

myDT = data.table(a = 1:2, b = 3:4)

# standard way
res <- myDT[, "a"]
res[, a := 0]
myDT
#    a b
# 1: 1 3
# 2: 2 4

# oddball, grabbing pointers
res2 <- setDT(unclass(myDT)["a"])
res2[, a := 0]
myDT
#    a b
# 1: 0 3
# 2: 0 4

Ok, ich habe heute etwas Neues und Schnelles gelernt (die Spinner), und ich habe zur Kenntnis genommen, dass es einen Kompromiss zwischen Geschwindigkeit und sparsamer Codierung gibt. Das Glas ist also halb voll! Danke!

Ich denke, #852 verwandt

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen