Преобразование пакета с использованием классов S3 в S4, будет ли падение производительности?

У меня есть пакет R, в котором в настоящее время используется система класса S3, с двумя разными классами и несколькими методами для общих функций S3, таких как plot, logLik и update (для обновления формулы модели). Поскольку мой код стал более сложным со всеми проверками достоверности и структурами if/else из-за того, что наследования или отправки наследования не было на основе двух аргументов в S3, я начал думать о преобразовании моего пакета в S4, Но затем я начал читать о преимуществах и недостатках S3 по сравнению с S4, и я уже не так уверен. Я нашел сообщение блога R-bloggers об эффективности в S3 vs S4, и, поскольку это было 5 лет назад, я тестировал то же самое сейчас:

library(microbenchmark)
setClass("MyClass", representation(x="numeric"))
microbenchmark(structure(list(x=rep(1, 10^7)), class="MyS3Class"),
               new("MyClass", x=rep(1, 10^7)) )
Unit: milliseconds
                                                   expr
 structure(list(x = rep(1, 10^7)), class = "MyS3Class")
                       new("MyClass", x = rep(1, 10^7))
       min       lq   median       uq      max neval
 148.75049 152.3811 155.2263 159.8090 323.5678   100
  75.15198 123.4804 129.6588 131.5031 241.8913   100

Итак, в этом простом примере S4 на самом деле бит быстрее. Затем я прочитал вопрос SO об использовании S3 vs S4, что было в значительной степени в пользу S3. Особенно ответ @joshua-ulrich заставил меня возражать против S4, поскольку он сказал, что

для любого изменения слота требуется полная копия объекта

Это кажется большой проблемой, если я рассмотрю мой случай, когда я обновляю свой объект на каждой итерации при оптимизации логарифмической вероятности моей модели. После некоторого поиска я нашел John Chambers post об этой проблеме, которая, кажется, меняется в R 3.0.0.

Так что, хотя я считаю, что было бы полезно использовать классы S4 для некоторой ясности в моих кодах (например, больше классов, наследующих от основного класса модели), а также для проверок действительности и т.д., теперь мне интересно, стоит ли всю работу с точки зрения производительности? Итак, производительность с точки зрения производительности, существуют ли реальные различия в производительности между S3 и S4? Есть ли еще некоторые проблемы с производительностью, которые я должен рассмотреть? Или можно вообще что-то сказать об этой проблеме?

РЕДАКТИРОВАТЬ: Как предположили @DWin и @g-grothendieck, вышеупомянутый бенчмаркинг не учитывает случай, когда слот существующего объекта изменяется. Итак, вот еще один тест, который более уместен для истинного приложения (функции в примере могут быть функциями get/set для некоторых элементов в модели, которые изменяются при максимизации лог-правдоподобия):

objS3<-structure(list(x=rep(1, 10^3), z=matrix(0,10,10), y=matrix(0,10,10)),
                 class="MyS3Class")
fnS3<-function(obj,a){
  obj$y<-a
  obj
}

setClass("MyClass", representation(x="numeric",z="matrix",y="matrix"))
objS4<-new("MyClass", x=rep(1, 10^3),z=matrix(0,10,10),y=matrix(0,10,10))
fnS4<-function(obj,a){ 
  [email protected]<-a
  obj
}

a<-matrix(1:100,10,10)
microbenchmark(fnS3(objS3,a),fnS4(objS4,a))
Unit: microseconds
           expr    min     lq median     uq    max neval
 fnS3(objS3, a)  6.531  7.464  7.932  9.331 26.591   100
 fnS4(objS4, a) 21.459 22.393 23.325 23.792 73.708   100

Тесты выполняются на R 2.15.2, на 64-битной Windows 7. Итак, здесь S4 явно медленнее.

Ответ 1

  • Прежде всего, вы можете легко иметь S3-методы для классов S4:

    > extract <- function (x, ...) [email protected]
    > setGeneric ("extr4", def=function (x, ...){})
    [1] "extr4"
    > setMethod ("extr4", signature= "MyClass", definition=extract)
    [1] "extr4"
    > `[.MyClass` <- extract
    > `[.MyS3Class` <- function (x, ...) x$x
    > microbenchmark (objS3[], objS4 [], extr4 (objS4), extract (objS4))
    Unit: nanoseconds
               expr   min      lq  median      uq   max neval
            objS3[]  6775  7264.5  7578.5  8312.0 39531   100
            objS4[]  5797  6705.5  7124.0  7404.0 13550   100
       extr4(objS4) 20534 21512.0 22106.0 22664.5 54268   100
     extract(objS4)   908  1188.0  1328.0  1467.0 11804   100
    

edit: из-за комментария Хэдли, измените эксперимент на plot:

> `plot.MyClass` <- extract
> `plot.MyS3Class` <- function (x, ...) x$x
> microbenchmark (plot (objS3), plot (objS4), extr4 (objS4), extract (objS4))
Unit: nanoseconds
           expr   min      lq median      uq     max neval
    plot(objS3) 28915 30172.0  30591 30975.5 1887824   100
    plot(objS4) 25353 26121.0  26471 26960.0  411508   100
   extr4(objS4) 20395 21372.5  22001 22385.5   31359   100
 extract(objS4)   979  1328.0   1398  1677.0    3982   100

для метода S4 для plot Я получаю:

    plot(objS4) 19835 20428.5 21336.5 22175.0 58876   100

Итак, [ имеет исключительно быстрый механизм отправки (что хорошо, потому что я думаю, что извлечение и соответствующие функции замены относятся к числу наиболее часто называемых методов. Но нет, отправка S4 не медленнее, чем отправка S3.


Здесь метод S3 объекта S4 выполняется так же быстро, как метод S3 на объекте S3. Однако вызов без отправки еще быстрее.

  • есть некоторые вещи, которые работают намного лучше, чем S3, такие как as.matrix или as.data.frame
    По некоторым причинам определение этих значений как S3 означает, что, например, lm (formula, objS4) будет работать из коробки. Это не работает с as.data.frame, определяемым как метод S4.

  • Также гораздо удобнее вызывать debug по методу S3.

  • некоторые другие вещи не будут работать с S3, например. отправляя второй аргумент.

  • Будет ли какое-либо заметное снижение производительности, очевидно, зависит от вашего класса, то есть от того, какие структуры у вас есть, насколько велики объекты и как часто называются методы. Несколько мкс отправки метода не будут иметь значения при расчете мс или четных s. Но μs имеют значение, когда функция называется миллиардами раз.

  • Одна вещь, вызвавшая заметное снижение производительности для некоторых функций, которые часто вызываются ([), - это проверка S4 (справедливое количество проверок, выполненных в validObject)), однако я рад, что это, поэтому я использую его. Внутри я использую функции рабочей лошадки, которые пропускают этот шаг.

  • В случае, если у вас есть большие данные, а call-by-reference поможет вам повысить производительность, вы можете захотеть взглянуть на ссылочные классы. Я до сих пор не работал с ними, поэтому я не могу прокомментировать это.

Ответ 2

Если вас беспокоит производительность, сравните ее. Если вам действительно нужно множественное наследование или множественная отправка, используйте S4. В противном случае используйте S3.

Ответ 3

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

Я признаю, что я никогда не понимал модель программирования S4. Однако то, что говорилось в сообщении Chambers, заключается в том, что @<-, т.е. Назначение слотов, было повторно реализовано как примитив, а не как закрытие, так что ему не потребовалась бы полная копия объекта, когда один компонент был изменен. Таким образом, более раннее состояние дел будет изменено в R 3.0.0 beta. На моей машине (5-летняя MacPro с R.0.0.0 beta) относительная разница была еще больше. Однако я не думал, что это был хороший тест, поскольку он не изменял существующую копию именованного объекта с несколькими слотами.

res <-microbenchmark(structure(list(x=rep(1, 10^7)), class="MyS3Class"),
                new("MyClass", x=rep(1, 10^7)) )
summary(res)[ ,"median"]
#[1] 145.0541 103.4064

Я думаю, вы должны пойти с S4, так как ваша структура мозга более гибкая, чем моя, и есть много очень умных людей, Дуглас Бейтс и Мартин Маэчер, чтобы назвать еще двух, кроме Джона Чамберса, которые использовали методы S4 для пакетов, которые требуют интенсивной обработки. Пакет Matrix и lme4 используют методы S4 для критических функций.