В R, почему сумма настолько медленная по сравнению с другими, например cumsum?

Я пытаюсь реализовать функцию, которая должна быть невероятно быстрой, главным образом потому, что она много раз работает с огромными кадрами данных.

R всегда оставлял меня в замешательстве относительно того, почему это иногда немного медленно, и почему это в другое время смехотворно медленно. (К сожалению, это не так быстро.)

Независимо от того, я всегда предполагал, что, когда это возможно, все может происходить намного быстрее, когда они каким-то образом применяются, применяя, применяя или применяя, вместо того, чтобы вводить в цикл. Недавно я столкнулся с примером, который заставляет меня думать, что под капотом происходит что-то еще, что, если бы я это понял, может помочь мне с оптимизацией в будущем.

Вот несколько расчетов, которые я запускал на своей относительно мощной машине Ubuntu Linux:

system.time(sapply(1:1e5, sum))
user  system elapsed
35.130   0.000  35.128


system.time(sapply(1:1e5, cumsum))
user  system elapsed
0.110   0.000   0.108

Да, вы правильно читаете эти цифры: cumsum, который создает массив суммарной суммы, на порядок быстрее, чем просто предоставление мне простой суммы. (Было бы здорово, если бы кто-то еще мог проверить эти результаты на своей машине!)

Я не вижу, как это возможно, за исключением случаев, когда реализации сильно различаются. Предполагая, что они сильно отличаются друг от друга, я хотел бы знать, каким образом, чтобы я мог следить за определенными функциями, которых можно избежать при поиске скорости. (Для основных функций я не знаю, как увидеть их источник. Стандартные средства просто вводить имя функции без каких-либо круглых скобок не работают для основных функций.)

Большое спасибо!

Ответы

Ответ 1

Следуя более или менее инструкциям для использования operf, я создал файл с единственной строкой sapply(1:1e5, sum) и запустил

$ operf ~/bin/R-3-1-branch/bin/R -f sum.R
$ opreport -l ~/bin/R-3-1-branch/lib/libR.so |less

производство

CPU: Intel Sandy Bridge microarchitecture, speed 2.401e+06 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (No unit mask) count 100000
samples  %        image name               symbol name
835882   93.0929  libR.so                  RunGenCollect
27731     3.0884  libR.so                  SortNodes
9323      1.0383  libR.so                  AgeNodeAndChildren
2038      0.2270  libR.so                  CheckFinalizers
1593      0.1774  libR.so                  Rf_allocVector3
1222      0.1361  libR.so                  duplicate1
...

и т.д.. Большую часть времени тратится на сборщик мусора (RunGenCollect - запускает сборщик мусора поколения). Поэтому я побежал

$ R -d gdb R
(gdb) run
> sapply(1:1e5, sum)
^C
(gdb) break RunGenCollect
(gdb) continue
Continuing.

Breakpoint 1, RunGenCollect (size_needed=50000) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:1504
1504        bad_sexp_type_seen = 0;
(gdb) where

который произвел

#0  RunGenCollect (size_needed=50000) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:1504
#1  0x00007ffff789d354 in R_gc_internal (size_needed=50000) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:2825
#2  0x00007ffff789e99b in Rf_allocVector3 (type=13, length=100000, allocator=0x0) at /home/mtmorgan/src/R-3-1-branch/src/main/memory.c:2563
#3  0x00007ffff788e1a5 in Rf_allocVector (type=13, length=100000) at /home/mtmorgan/src/R-3-1-branch/src/include/Rinlinedfuns.h:189
#4  0x00007ffff7831787 in duplicate1 (s=0x7ffff3b0b010, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:335
#5  0x00007ffff783371a in duplicate_child (s=0x7ffff3b0b010, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:199
#6  0x00007ffff783357a in duplicate_list (s=0x2c98b30, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:261
#7  0x00007ffff7830fc2 in duplicate1 (s=0x2c98b30, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:308
#8  0x00007ffff783371a in duplicate_child (s=0x2c98b30, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:199
#9  0x00007ffff783357a in duplicate_list (s=0x2c98a88, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:261
#10 0x00007ffff7830fc2 in duplicate1 (s=0x2c98a88, deep=TRUE) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:308
#11 0x00007ffff7830c7f in Rf_duplicate (s=0x2c98a88) at /home/mtmorgan/src/R-3-1-branch/src/main/duplicate.c:132
#12 0x00007ffff79257f4 in do_summary (call=0x2c98a88, op=0x6259a0, args=0x303cf88, env=0x2c97f48) at /home/mtmorgan/src/R-3-1-branch/src/main/summary.c:462
...

а соответствующая строка - строка 462

(gdb) up 12
#12 0x00007ffff79257f4 in do_summary (call=0x2c98a88, op=0x6259a0, args=0x303cf88, env=0x2c97f48) at /home/mtmorgan/src/R-3-1-branch/src/main/summary.c:462
462     PROTECT(call2 = duplicate(call));
(gdb) list
457     return ans;
458     }
459 
460     /* match to foo(..., na.rm=FALSE) */
461     PROTECT(args = fixup_NaRm(args));
462     PROTECT(call2 = duplicate(call));
463     SETCDR(call2, args);
464 
465     if (DispatchGroup("Summary", call2, op, args, env, &ans)) {
466     UNPROTECT(2);

Вызов дублируется

(gdb) call Rf_PrintValue(call)
FUN(1:100000[[5339L]], ...)

для каждой итерации цикла, вызывая сбор мусора. Аналогичный код не выполняется для cumsum. Это было в течение длительного времени и по причинам, которые не являются на 100% очевидными.

$ svn annotate ~/src/R-3-1-branch/src/main/summary.c |less
...
 42643     ripley     /* match to foo(..., na.rm=FALSE) */
 42643     ripley     PROTECT(args = fixup_NaRm(args));
 42643     ripley     PROTECT(call2 = duplicate(call));
 42643     ripley     SETCDR(call2, args)
...
$ svn log -r42643
------------------------------------------------------------------------
r42643 | ripley | 2007-08-25 23:09:50 -0700 (Sat, 25 Aug 2007) | 1 line

make the rest of the group generics primitive
------------------------------------------------------------------------

Было бы интересно рассмотреть это в списке рассылки R-devel. Дело не в том, что sum особенно медленное, а скорее, что вызовы сборщика мусора достигают времени выполнения.

Hmm, при отражении оказывается, что

sapply(1:1e5, function(x) sum(x))

работает в том же самом шаре, что и cumsum. Я думаю, потому что duplicate в строке 462 в исходной версии делает копию элементов 1e5 при подготовке к выбору i-го элемента для суммирования. Напротив, в function(x) sum(x) вектор уже был подмножеством, поэтому дублирование - только i-го элемента. Дублирование исходного вектора также объясняет, почему элементы 1e5 намного медленнее, чем 1e4, и почему as.list(1:1e5) относительно совершенен (только элемент списка фактически дублируется, а может быть, и не тот). Дублирование во время вызова sum имеет отношение к тому факту, что оно относится к общей группе (S3) Summary, см. ?"group generic".

Ответ 2

Только что присоединившись к этой вещи, видимо, у меня недостаточно репутации, чтобы комментировать мартовскую почту, но я опубликовал патч здесь, чтобы решить эту проблему. На самом деле два патча. Первый позволяет избежать дублирования почти в каждом случае. Второй, гораздо более простой патч, только мелкие дубликаты, так что парный список дублируется, но не большой вектор в вызове. Другой способ исправить это - сделать эквивалент lapply (1:1e5, function (x) sum (x)), т.е. Иметь только символ в вызове. Однако это будет мешать попытке do_lapply оптимизации, пропуская некоторую оценку с каждой итерацией.

Обновление: второй патч был применен к R-devel.

Ответ 3

Это странный пример, так как вы каждый раз передаете один номер sum. Возможно, что sum не оптимизирован для векторов длины 1 и cumsum. Но более логичное сравнение - это что-то вроде

> system.time(sapply(1:1e4, function(x) sum(1:x)))
   user  system elapsed 
  0.126   0.019   0.155 
> system.time(sapply(1:1e4, function(x) cumsum(1:x)))
   user  system elapsed 
  1.601   0.158   1.824 

Я немного отклонил диапазон, чтобы избежать переполнения целых чисел.

И это интересно, как они похожи, когда вы просто несколько элементов

> system.time(sapply(1:1e5, function(x) sum(c(1,x))))
   user  system elapsed 
  0.196   0.001   0.204 
> system.time(sapply(1:1e5, function(x) cumsum(c(1,x))))
   user  system elapsed 
  0.170   0.005   0.188 

Так ясно, что что-то происходит со случаем length(x)==1