Skip to contents

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)    9.53µs  10.62µs    91326.    21.3KB     27.4
#> 2 foo_S3(x)    2.65µs   2.95µs   312500.        0B     31.3
#> 3 foo_S4(x)    2.85µs   3.25µs   289437.        0B     28.9

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)  15.48µs  17.06µs    57419.        0B     34.5
#> 2 bar_S4(x, y)   7.47µs   8.34µs   116485.        0B     23.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   9.57µs   10.8µs    90656.        0B    36.3 
#>  2 worst                3          15   9.65µs   10.8µs    91084.        0B    36.4 
#>  3 best                 5          15   9.39µs   10.6µs    89955.        0B    36.0 
#>  4 worst                5          15    9.7µs   10.7µs    91774.        0B    36.7 
#>  5 best                10          15   9.64µs   10.7µs    92330.        0B    36.9 
#>  6 worst               10          15   9.96µs   11.1µs    88852.        0B    35.6 
#>  7 best                50          15  10.22µs   11.4µs    86157.        0B    34.5 
#>  8 worst               50          15  11.81µs   12.9µs    75917.        0B    30.4 
#>  9 best               100          15  10.81µs   12.1µs    80546.        0B    40.3 
#> 10 worst              100          15  14.55µs   15.8µs    62036.        0B    24.8 
#> 11 best                 3         100   9.35µs   10.6µs    91977.        0B    36.8 
#> 12 worst                3         100   9.65µs   10.8µs    89793.        0B    35.9 
#> 13 best                 5         100   9.51µs   10.6µs    91664.        0B    36.7 
#> 14 worst                5         100  10.02µs   11.1µs    87876.        0B    35.2 
#> 15 best                10         100   9.56µs   10.6µs    89354.        0B    35.8 
#> 16 worst               10         100  10.58µs   11.7µs    83218.        0B    33.3 
#> 17 best                50         100   10.2µs   11.5µs    85222.        0B    34.1 
#> 18 worst               50         100  15.54µs   16.8µs    58390.        0B    29.2 
#> 19 best               100         100  10.63µs   11.7µs    78180.        0B    15.6 
#> 20 worst              100         100  20.44µs   21.6µs    44937.        0B     8.99

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   11.3µs   12.3µs    79666.        0B    23.9 
#>  2 worst                3          15   11.6µs   12.6µs    78010.        0B    15.6 
#>  3 best                 5          15   10.8µs   11.9µs    81623.        0B    24.5 
#>  4 worst                5          15   11.7µs   12.6µs    78138.        0B    23.4 
#>  5 best                10          15   11.3µs   12.3µs    80127.        0B    24.0 
#>  6 worst               10          15   12.1µs   13.1µs    74976.        0B    15.0 
#>  7 best                50          15   12.3µs   13.4µs    73604.        0B    22.1 
#>  8 worst               50          15   16.1µs   17.2µs    57354.        0B    17.2 
#>  9 best               100          15   13.5µs   14.7µs    66621.        0B    20.0 
#> 10 worst              100          15   20.7µs   21.9µs    45031.        0B    13.5 
#> 11 best                 3         100   11.3µs   12.4µs    79317.        0B    23.8 
#> 12 worst                3         100   12.1µs   13.2µs    73721.        0B    22.1 
#> 13 best                 5         100   11.6µs   12.8µs    75969.        0B    22.8 
#> 14 worst                5         100   12.8µs   14.1µs    67052.        0B    20.1 
#> 15 best                10         100   11.5µs   12.6µs    77085.        0B    23.1 
#> 16 worst               10         100   13.5µs   14.7µs    66383.        0B    13.3 
#> 17 best                50         100   12.3µs   13.6µs    71675.        0B    21.5 
#> 18 worst               50         100   22.3µs   23.6µs    41590.        0B    12.5 
#> 19 best               100         100   13.8µs     15µs    65014.        0B    19.5 
#> 20 worst              100         100   33.6µs   34.9µs    28214.        0B     8.47