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)    3.69µs   4.14µs   233552.        0B     70.1
#> 2 foo_S3(x)    1.35µs   1.48µs   616660.        0B      0  
#> 3 foo_S4(x)    1.48µs   1.64µs   586204.        0B     58.6

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)   6.89µs   7.63µs   125512.        0B     50.2
#> 2 bar_S4(x, y)   3.94µs    4.3µs   223593.        0B     44.7

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   3.73µs   4.14µs   233715.        0B     46.8
#>  2 worst                3          15   3.85µs   4.26µs   226893.        0B     68.1
#>  3 best                 5          15   3.73µs    4.1µs   235608.        0B     70.7
#>  4 worst                5          15    3.9µs    4.3µs   226792.        0B     68.1
#>  5 best                10          15   3.73µs   4.14µs   234855.        0B     70.5
#>  6 worst               10          15   4.06µs   4.51µs   215358.        0B     64.6
#>  7 best                50          15    4.1µs   4.51µs   215821.        0B     64.8
#>  8 worst               50          15   5.49µs   5.99µs   163775.        0B     49.1
#>  9 best               100          15   4.51µs   5.12µs   189758.        0B     56.9
#> 10 worst              100          15   7.42µs      8µs   123380.        0B     37.0
#> 11 best                 3         100   3.77µs   4.18µs   233487.        0B     93.4
#> 12 worst                3         100   3.98µs   4.55µs   212867.        0B     63.9
#> 13 best                 5         100    3.9µs   4.55µs   207037.        0B     41.4
#> 14 worst                5         100   4.43µs   4.96µs   192327.        0B     57.7
#> 15 best                10         100   3.73µs   4.22µs   223713.        0B     67.1
#> 16 worst               10         100   4.84µs   5.33µs   181142.        0B     54.4
#> 17 best                50         100   4.18µs   4.63µs   207896.        0B     41.6
#> 18 worst               50         100  10.21µs  10.87µs    89545.        0B     26.9
#> 19 best               100         100   4.47µs   5.04µs   186869.        0B     37.4
#> 20 worst              100         100  15.46µs  16.32µs    59365.        0B     17.8

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   4.63µs   5.25µs   178156.        0B    71.3 
#>  2 worst                3          15   4.84µs   5.62µs   162908.        0B    48.9 
#>  3 best                 5          15   4.67µs   5.25µs   178662.        0B    71.5 
#>  4 worst                5          15   5.04µs   5.66µs   168251.        0B    50.5 
#>  5 best                10          15   4.67µs   5.04µs   190060.        0B    76.1 
#>  6 worst               10          15   5.25µs   5.66µs   171466.        0B    51.5 
#>  7 best                50          15   5.29µs    5.7µs   167396.        0B    67.0 
#>  8 worst               50          15   7.71µs   8.32µs   114879.        0B    34.5 
#>  9 best               100          15   6.07µs   6.68µs   141150.        0B    56.5 
#> 10 worst              100          15  11.52µs   12.3µs    78719.        0B    23.6 
#> 11 best                 3         100   4.59µs   5.08µs   187697.        0B    75.1 
#> 12 worst                3         100   5.41µs   6.03µs   154788.        0B    46.5 
#> 13 best                 5         100   4.88µs   5.37µs   178631.        0B    71.5 
#> 14 worst                5         100   5.54µs   6.03µs   160767.        0B    48.2 
#> 15 best                10         100      5µs   5.66µs   160065.        0B    64.1 
#> 16 worst               10         100   7.58µs   8.04µs   118897.        0B    35.7 
#> 17 best                50         100   5.25µs    5.7µs   169331.        0B    50.8 
#> 18 worst               50         100  15.29µs  16.15µs    59015.        0B    23.6 
#> 19 best               100         100   5.95µs   6.44µs   149406.        0B    44.8 
#> 20 worst              100         100  28.99µs  29.97µs    32367.        0B     9.71