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.99µs 8.12µs 117278. 0B 46.9
#> 2 foo_S3(x) 2.4µs 2.67µs 345301. 0B 34.5
#> 3 foo_S4(x) 2.65µs 2.96µs 328505. 0B 32.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) 12.68µs 13.83µs 70005. 0B 56.0
#> 2 bar_S4(x, y) 7.17µs 8.02µs 122225. 0B 48.9
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 6.97µs 7.75µs 124537. 0B 74.8
#> 2 worst 3 15 7.21µs 7.82µs 124351. 0B 74.7
#> 3 best 5 15 6.95µs 7.63µs 126120. 0B 75.7
#> 4 worst 5 15 7.31µs 7.92µs 123237. 0B 74.0
#> 5 best 10 15 7.19µs 8.16µs 118127. 0B 70.9
#> 6 worst 10 15 7.48µs 8.47µs 114129. 0B 68.5
#> 7 best 50 15 7.56µs 8.46µs 115235. 0B 69.2
#> 8 worst 50 15 9.34µs 10.06µs 97397. 0B 58.5
#> 9 best 100 15 8.01µs 8.96µs 100029. 0B 20.0
#> 10 worst 100 15 11.55µs 12.69µs 77665. 0B 15.5
#> 11 best 3 100 7.08µs 8.09µs 121095. 0B 12.1
#> 12 worst 3 100 7.44µs 8.35µs 117217. 0B 23.4
#> 13 best 5 100 7.13µs 8.07µs 121572. 0B 24.3
#> 14 worst 5 100 7.53µs 8.58µs 113638. 0B 11.4
#> 15 best 10 100 7.07µs 8.07µs 121643. 0B 24.3
#> 16 worst 10 100 8.34µs 9.28µs 105980. 0B 21.2
#> 17 best 50 100 7.6µs 8.57µs 114450. 0B 22.9
#> 18 worst 50 100 12.32µs 13.29µs 73760. 0B 7.38
#> 19 best 100 100 8.33µs 9.42µs 104594. 0B 10.5
#> 20 worst 100 100 17.9µs 19.05µs 51862. 0B 10.4
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.69µs 9.85µs 99292. 0B 29.8
#> 2 worst 3 15 9.1µs 10.22µs 96230. 0B 19.2
#> 3 best 5 15 8.81µs 10.01µs 97934. 0B 19.6
#> 4 worst 5 15 9.19µs 10.21µs 96191. 0B 19.2
#> 5 best 10 15 8.91µs 9.91µs 98703. 0B 19.7
#> 6 worst 10 15 9.7µs 10.74µs 91137. 0B 18.2
#> 7 best 50 15 9.56µs 10.66µs 91248. 0B 18.3
#> 8 worst 50 15 12.76µs 13.95µs 70394. 0B 14.1
#> 9 best 100 15 10.94µs 12.1µs 80708. 0B 16.1
#> 10 worst 100 15 17.67µs 18.98µs 51995. 0B 10.4
#> 11 best 3 100 8.78µs 9.89µs 98203. 0B 29.5
#> 12 worst 3 100 9.29µs 10.4µs 94002. 0B 18.8
#> 13 best 5 100 8.97µs 10.05µs 96987. 0B 19.4
#> 14 worst 5 100 10.18µs 11.24µs 86916. 0B 17.4
#> 15 best 10 100 9.05µs 10.12µs 96566. 0B 29.0
#> 16 worst 10 100 11.3µs 12.34µs 79708. 0B 15.9
#> 17 best 50 100 9.8µs 10.97µs 89085. 0B 17.8
#> 18 worst 50 100 18.09µs 19.2µs 51290. 0B 10.3
#> 19 best 100 100 11.05µs 12.27µs 79786. 0B 16.0
#> 20 worst 100 100 32.23µs 33.39µs 29651. 0B 5.93