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)    7.29µs   8.43µs   110766.    18.2KB     22.2
#> 2 foo_S3(x)    2.59µs   2.83µs   322645.        0B      0  
#> 3 foo_S4(x)    2.77µs   3.09µs   310841.        0B     31.1

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.25µs  14.94µs    64761.        0B     25.9
#> 2 bar_S4(x, y)   7.44µs   8.29µs   117097.        0B     23.4

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.42µs   115287.        0B     34.6
#>  2 worst                3          15   7.42µs   8.53µs   114141.        0B     22.8
#>  3 best                 5          15   7.21µs   8.46µs   114809.        0B     23.0
#>  4 worst                5          15   7.52µs   8.73µs   111477.        0B     33.5
#>  5 best                10          15   7.43µs   8.56µs   113088.        0B     33.9
#>  6 worst               10          15   7.67µs      9µs   107615.        0B     21.5
#>  7 best                50          15   7.88µs   9.14µs   106167.        0B     31.9
#>  8 worst               50          15   9.68µs  11.03µs    88414.        0B     17.7
#>  9 best               100          15   8.61µs   9.86µs    98512.        0B     29.6
#> 10 worst              100          15  12.18µs   13.6µs    71586.        0B     14.3
#> 11 best                 3         100   7.38µs   8.55µs   113547.        0B     34.1
#> 12 worst                3         100   7.67µs   8.79µs   109677.        0B     32.9
#> 13 best                 5         100   7.35µs    8.6µs   112867.        0B     33.9
#> 14 worst                5         100   7.82µs   8.97µs   107973.        0B     32.4
#> 15 best                10         100   7.42µs   8.58µs   112383.        0B     33.7
#> 16 worst               10         100   8.14µs   9.25µs   103716.        0B     31.1
#> 17 best                50         100   7.79µs   8.94µs   107503.        0B     32.3
#> 18 worst               50         100  12.84µs  14.28µs    67315.        0B     20.2
#> 19 best               100         100   8.62µs   9.89µs    97485.        0B     29.3
#> 20 worst              100         100  19.36µs  20.92µs    46449.        0B     13.9

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.91µs  10.42µs    92289.        0B     27.7
#>  2 worst                3          15    9.4µs  10.83µs    87986.        0B     35.2
#>  3 best                 5          15   9.02µs  10.46µs    91916.        0B     27.6
#>  4 worst                5          15   9.56µs  10.75µs    89443.        0B     26.8
#>  5 best                10          15   9.21µs  10.74µs    87830.        0B     26.4
#>  6 worst               10          15  10.08µs  11.69µs    82037.        0B     24.6
#>  7 best                50          15   9.79µs  10.69µs    89367.        0B     35.8
#>  8 worst               50          15  13.54µs  14.24µs    68681.        0B     20.6
#>  9 best               100          15  11.22µs  11.98µs    81213.        0B     32.5
#> 10 worst              100          15  18.33µs  19.23µs    50684.        0B     20.3
#> 11 best                 3         100   9.16µs   9.98µs    97043.        0B     29.1
#> 12 worst                3         100  10.04µs  11.11µs    86291.        0B     34.5
#> 13 best                 5         100   9.05µs  10.06µs    95445.        0B     28.6
#> 14 worst                5         100  10.19µs  11.39µs    84105.        0B     25.2
#> 15 best                10         100   9.19µs  10.35µs    92399.        0B     27.7
#> 16 worst               10         100  11.44µs  12.62µs    76222.        0B     22.9
#> 17 best                50         100  10.37µs  11.59µs    81907.        0B     32.8
#> 18 worst               50         100  19.68µs  20.87µs    46553.        0B     14.0
#> 19 best               100         100  11.63µs  12.79µs    75226.        0B     30.1
#> 20 worst              100         100  30.66µs  32.18µs    29587.        0B     11.8