Performance

library(S7)

The dispatch performance should be roughly on par with S3 and S4, though as this is implemented in a package there is some overhead due to .Call vs .Primitive.

Text <- new_class("Text", parent = class_character)
Number <- new_class("Number", parent = class_double)

x <- Text("hi")
y <- Number(1)

foo_S7 <- new_generic("foo_S7", "x")
method(foo_S7, Text) <- function(x, ...) paste0(x, "-foo")

foo_S3 <- function(x, ...) {
  UseMethod("foo_S3")
}

foo_S3.Text <- function(x, ...) {
  paste0(x, "-foo")
}

library(methods)
setOldClass(c("Number", "numeric", "S7_object"))
setOldClass(c("Text", "character", "S7_object"))

setGeneric("foo_S4", function(x, ...) standardGeneric("foo_S4"))
#> [1] "foo_S4"
setMethod("foo_S4", c("Text"), function(x, ...) paste0(x, "-foo"))

# Measure performance of single dispatch
bench::mark(foo_S7(x), foo_S3(x), foo_S4(x))
#> # A tibble: 3 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 foo_S7(x)    6.93µs   8.48µs   110465.        0B     55.3
#> 2 foo_S3(x)    2.37µs   2.79µs   325772.        0B     32.6
#> 3 foo_S4(x)    2.62µs   3.15µs   302269.        0B     60.5

bar_S7 <- new_generic("bar_S7", c("x", "y"))
method(bar_S7, list(Text, Number)) <- function(x, y, ...) paste0(x, "-", y, "-bar")

setGeneric("bar_S4", function(x, y, ...) standardGeneric("bar_S4"))
#> [1] "bar_S4"
setMethod("bar_S4", c("Text", "Number"), function(x, y, ...) paste0(x, "-", y, "-bar"))

# Measure performance of double dispatch
bench::mark(bar_S7(x, y), bar_S4(x, y))
#> # A tibble: 2 × 6
#>   expression        min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>   <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 bar_S7(x, y)  13.39µs  15.39µs    61639.        0B     55.5
#> 2 bar_S4(x, y)   7.28µs   8.48µs   114432.        0B     34.3

A potential optimization is caching based on the class names, but lookup should be fast without this.

The following benchmark generates a class hierarchy of different levels and lengths of class names and compares the time to dispatch on the first class in the hierarchy vs the time to dispatch on the last class.

We find that even in very extreme cases (e.g. 100 deep hierarchy 100 of character class names) the overhead is reasonable, and for more reasonable cases (e.g. 10 deep hierarchy of 15 character class names) the overhead is basically negligible.

library(S7)

gen_character <- function (n, min = 5, max = 25, values = c(letters, LETTERS, 0:9)) {
  lengths <- sample(min:max, replace = TRUE, size = n)
  values <- sample(values, sum(lengths), replace = TRUE)
  starts <- c(1, cumsum(lengths)[-n] + 1)
  ends <- cumsum(lengths)
  mapply(function(start, end) paste0(values[start:end], collapse=""), starts, ends)
}

bench::press(
  num_classes = c(3, 5, 10, 50, 100),
  class_nchar = c(15, 100),
  {
    # Construct a class hierarchy with that number of classes
    Text <- new_class("Text", parent = class_character)
    parent <- Text
    classes <- gen_character(num_classes, min = class_nchar, max = class_nchar)
    env <- new.env()
    for (x in classes) {
      assign(x, new_class(x, parent = parent), env)
      parent <- get(x, env)
    }

    # Get the last defined class
    cls <- parent

    # Construct an object of that class
    x <- do.call(cls, list("hi"))

    # Define a generic and a method for the last class (best case scenario)
    foo_S7 <- new_generic("foo_S7", "x")
    method(foo_S7, cls) <- function(x, ...) paste0(x, "-foo")

    # Define a generic and a method for the first class (worst case scenario)
    foo2_S7 <- new_generic("foo2_S7", "x")
    method(foo2_S7, S7_object) <- function(x, ...) paste0(x, "-foo")

    bench::mark(
      best = foo_S7(x),
      worst = foo2_S7(x)
    )
  }
)
#> # A tibble: 20 × 8
#>    expression num_classes class_nchar      min   median `itr/sec` mem_alloc `gc/sec`
#>    <bch:expr>       <dbl>       <dbl> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#>  1 best                 3          15   7.16µs   8.68µs   109065.        0B    65.5 
#>  2 worst                3          15   7.36µs   8.79µs   108046.        0B    64.9 
#>  3 best                 5          15   7.11µs   8.69µs   108661.        0B    65.2 
#>  4 worst                5          15   7.59µs   9.41µs   100786.        0B    50.4 
#>  5 best                10          15   7.25µs   8.84µs   106579.        0B    64.0 
#>  6 worst               10          15   7.66µs   9.35µs   100681.        0B    60.4 
#>  7 best                50          15   7.62µs   9.37µs   100828.        0B    60.5 
#>  8 worst               50          15   9.41µs   11.2µs    84702.        0B    50.9 
#>  9 best               100          15   8.12µs   9.34µs    96692.        0B    19.3 
#> 10 worst              100          15  11.78µs  13.03µs    75457.        0B    15.1 
#> 11 best                 3         100   7.11µs    8.3µs   117650.        0B    11.8 
#> 12 worst                3         100    7.2µs   8.58µs   101478.        0B    20.3 
#> 13 best                 5         100   7.08µs   8.72µs    95204.        0B    19.0 
#> 14 worst                5         100   7.78µs   9.35µs   104029.        0B    10.4 
#> 15 best                10         100   7.02µs   8.34µs   116174.        0B    23.2 
#> 16 worst               10         100   7.99µs   9.37µs   102608.        0B    20.5 
#> 17 best                50         100   7.68µs   8.89µs   110305.        0B    22.1 
#> 18 worst               50         100  12.44µs  13.72µs    66985.        0B     6.70
#> 19 best               100         100   8.28µs   9.99µs    89666.        0B     8.97
#> 20 worst              100         100   18.3µs  19.72µs    49780.        0B     9.96

And the same benchmark using double-dispatch

bench::press(
  num_classes = c(3, 5, 10, 50, 100),
  class_nchar = c(15, 100),
  {
    # Construct a class hierarchy with that number of classes
    Text <- new_class("Text", parent = class_character)
    parent <- Text
    classes <- gen_character(num_classes, min = class_nchar, max = class_nchar)
    env <- new.env()
    for (x in classes) {
      assign(x, new_class(x, parent = parent), env)
      parent <- get(x, env)
    }

    # Get the last defined class
    cls <- parent

    # Construct an object of that class
    x <- do.call(cls, list("hi"))
    y <- do.call(cls, list("ho"))

    # Define a generic and a method for the last class (best case scenario)
    foo_S7 <- new_generic("foo_S7", c("x", "y"))
    method(foo_S7, list(cls, cls)) <- function(x, y, ...) paste0(x, y, "-foo")

    # Define a generic and a method for the first class (worst case scenario)
    foo2_S7 <- new_generic("foo2_S7", c("x", "y"))
    method(foo2_S7, list(S7_object, S7_object)) <- function(x, y, ...) paste0(x, y, "-foo")

    bench::mark(
      best = foo_S7(x, y),
      worst = foo2_S7(x, y)
    )
  }
)
#> # A tibble: 20 × 8
#>    expression num_classes class_nchar      min   median `itr/sec` mem_alloc `gc/sec`
#>    <bch:expr>       <dbl>       <dbl> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#>  1 best                 3          15    8.7µs   10.2µs    96132.        0B    19.2 
#>  2 worst                3          15   9.03µs   10.4µs    94207.        0B    28.3 
#>  3 best                 5          15   8.77µs   10.2µs    96305.        0B    19.3 
#>  4 worst                5          15   9.13µs   10.5µs    93088.        0B    18.6 
#>  5 best                10          15   8.87µs   10.3µs    94628.        0B    18.9 
#>  6 worst               10          15    9.5µs   10.9µs    88944.        0B    17.8 
#>  7 best                50          15   9.88µs   11.4µs    85059.        0B    17.0 
#>  8 worst               50          15  13.28µs   14.8µs    65849.        0B    13.2 
#>  9 best               100          15  11.13µs   12.5µs    77268.        0B    23.2 
#> 10 worst              100          15  17.64µs   19.1µs    51224.        0B    10.2 
#> 11 best                 3         100   8.75µs   10.2µs    95369.        0B    19.1 
#> 12 worst                3         100    9.4µs   10.8µs    89732.        0B    26.9 
#> 13 best                 5         100   8.88µs   10.3µs    94049.        0B    18.8 
#> 14 worst                5         100   9.61µs   10.9µs    88500.        0B    17.7 
#> 15 best                10         100   9.07µs   10.4µs    92670.        0B    18.5 
#> 16 worst               10         100  11.01µs   12.4µs    78287.        0B    15.7 
#> 17 best                50         100  10.05µs   11.5µs    83664.        0B    16.7 
#> 18 worst               50         100  20.22µs   21.6µs    45445.        0B     9.09
#> 19 best               100         100  11.28µs   12.7µs    75634.        0B    15.1 
#> 20 worst              100         100  29.97µs   31.5µs    31299.        0B     6.26