Производительность .Primitive и .Internal

Я делал некоторую оптимизацию, удаляя один шаг из процесса...

> library(microbenchmark)
> microbenchmark(paste0("this","and","that"))
Unit: microseconds
                          expr   min    lq    mean median    uq    max neval
 paste0("this", "and", "that") 2.026 2.027 3.50933  2.431 2.837 34.038   100

> microbenchmark(.Internal(paste0(list("this","and","that"),NULL)))
Unit: microseconds
                                                 expr   min    lq    mean median    uq    max neval
 .Internal(paste0(list("this", "and", "that"), NULL)) 1.216 1.621 2.77596  2.026 2.027 43.764   100

пока что так хорошо...

но после того, как я заметил, что list определяется как

function (...)  .Primitive("list")

Я попытался далее "упростить"

> microbenchmark(.Internal(paste0(.Primitive("list")("this","and","that"),NULL)))
Unit: microseconds
                                                               expr   min    lq    mean median    uq    max neval
 .Internal(paste0(.Primitive("list")("this", "and", "that"), NULL)) 3.241 3.242 4.66433  3.647 3.648 80.638   100

и время увеличивается!

Я предполагаю, что обработка строки "list" является источником проблемы и что она обрабатывается по-разному в пределах фактического вызова функции list

но как?

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


Изменить в ответ на комментарий Джоша О'Брайена:

Я не уверен, что это говорит о его идее, но

library(compiler)
ff <- compile(function(...){.Internal(paste0(.Primitive("list")("this","and","that"),NULL))})
ff2 <- compile(function(...){.Internal(paste0(list("this","and","that"),NULL))})
microbenchmark(eval(ff),eval(ff2),times=10000)
> microbenchmark(eval(ff2),eval(ff),times=10000)
Unit: microseconds
      expr   min    lq     mean median    uq     max neval
 eval(ff2) 1.621 2.026 2.356761  2.026 2.431 144.257 10000
  eval(ff) 1.621 2.026 2.455913  2.026 2.431  89.148 10000

и глядя на график, созданный с помощью microbenchmark (просто оберните его plot(), чтобы увидеть его сами), запуская его несколько раз, похоже, что они имеют статистически идентичную производительность, несмотря на то, что значение "max", похожее на ff2, хуже худшего. Не знаю, что с этим делать, но, возможно, это поможет кому-то. Итак, все это в основном говорит о том, что они скомпилируются в идентичный код. Означает ли это, что его комментарий является ответом?

Ответы

Ответ 1

Причина .Internal(paste0(.Primitive("list")("this","and","that"),NULL)) медленнее, по-видимому, из-за того, что догадался Джош О'Брайен. Вызов .Primitive("list") напрямую вызывает некоторые дополнительные накладные расходы.

Вы можете увидеть эффекты с помощью простого примера:

require(compiler)
pl <- cmpfun({.Primitive("list")})
microbenchmark(list(), .Primitive("list")(), pl())
# Unit: nanoseconds
#                  expr  min     lq median     uq   max neval
#                list()   63   98.0  112.0  140.5   529   100
#  .Primitive("list")() 4243 4391.5 4486.5 4606.0 16077   100
#                  pl()   79  135.5  148.0  175.5 39108   100

Тем не менее, вы не сможете улучшить скорость .Primitive и .Internal из подсказки R. Они являются точками входа в код C.

И нет причин попробовать и заменить вызов .Primitive на .Internal. Это рекурсивно, так как .Internal сам является примитивным.

> .Internal
function (call)  .Primitive(".Internal")

Вы получите ту же медлительность, если попытаетесь вызвать .Internal "напрямую"... и аналогичную "ускорение", если вы скомпилируете "прямой" вызов.

Internal. <- function() .Internal(paste0(list("this","and","that"),NULL))
Primitive. <- function() .Primitive(".Internal")(paste0("this","and","that"),NULL)
cPrimitive. <- cmpfun({Primitive.})
microbenchmark(Internal., Primitive., cPrimitive., times=1e4)
# Unit: nanoseconds
#         expr min lq median uq  max neval
#    Internal.  26 27     27 28 1057 10000
#   Primitive.  28 32     32 33 2526 10000
#  cPrimitive.  26 27     27 27 1706 10000

Ответ 2

У интерпретатора R есть жестко запрограммированные оптимизации для общих функций, и это идет глубже, чем байтовая компиляция:

> list2 <- list
> list3 <- cmpfun(list2)
> microbenchmark(
+   list(1,2),
+   list2(1,2),
+   list3(1,2)
+ )
Unit: nanoseconds
        expr min    lq    mean median    uq   max neval
  list(1, 2) 576 620.5  654.53  640.0 675.5   941   100
 list2(1, 2) 619 702.0 1123.43  728.0 761.0 39045   100
 list3(1, 2) 617 683.0  735.83  715.5 759.0  1964   100

Вот как выглядят SEXP. Обратите внимание на метаданные в "списке"

> .Internal(inspect(quote(list(1,2))))
@23b0ed0 06 LANGSXP g0c0 [NAM(2)] 
  @1ed8f48 01 SYMSXP g1c0 [MARK,LCK,gp=0x4000] "list" (has value)
  @2c7adf8 14 REALSXP g0c1 [] (len=1, tl=0) 1
  @2c7adc8 14 REALSXP g0c1 [] (len=1, tl=0) 2

list2 отсутствуют некоторые метаданные:

> list2 <- list
> .Internal(inspect(quote(list2(1,2))))
@23b1578 06 LANGSXP g0c0 [NAM(2)] 
  @23b0a70 01 SYMSXP g0c0 [] "list2"
  @2c7ad08 14 REALSXP g0c1 [] (len=1, tl=0) 1
  @2c7acd8 14 REALSXP g0c1 [] (len=1, tl=0) 2

.Primitive("list") является более сложным выражением:

> .Internal(inspect(quote(.Primitive("list")(1,2))))
@297e748 06 LANGSXP g0c0 [NAM(2)] 
  @297d9a0 06 LANGSXP g0c0 [] 
    @1ec4530 01 SYMSXP g1c0 [MARK,LCK,gp=0x4000] ".Primitive" (has value)
    @2c7a888 16 STRSXP g0c1 [] (len=1, tl=0)
      @1ed5588 09 CHARSXP g1c1 [MARK,gp=0x61] [ASCII] [cached] "list"
  @2c7a858 14 REALSXP g0c1 [] (len=1, tl=0) 1
  @2c7a828 14 REALSXP g0c1 [] (len=1, tl=0) 2