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.23µs 8.32µs 113741. 18.2KB 22.8
#> 2 foo_S3(x) 2.65µs 2.88µs 322292. 0B 32.2
#> 3 foo_S4(x) 2.77µs 3.14µs 308258. 0B 0
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.88µs 15.32µs 63599. 0B 25.4
#> 2 bar_S4(x, y) 7.78µs 8.72µs 112693. 0B 22.5
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.2µs 8.53µs 114435. 0B 34.3
#> 2 worst 3 15 7.46µs 8.63µs 113854. 0B 34.2
#> 3 best 5 15 7.29µs 8.59µs 113478. 0B 34.1
#> 4 worst 5 15 7.53µs 8.81µs 110937. 0B 33.3
#> 5 best 10 15 7.4µs 8.67µs 112915. 0B 33.9
#> 6 worst 10 15 7.69µs 8.94µs 109449. 0B 21.9
#> 7 best 50 15 7.84µs 9.03µs 108384. 0B 32.5
#> 8 worst 50 15 9.33µs 10.72µs 88701. 0B 26.6
#> 9 best 100 15 8.49µs 9.77µs 100076. 0B 40.0
#> 10 worst 100 15 12.06µs 13.46µs 72663. 0B 21.8
#> 11 best 3 100 7.46µs 8.81µs 110850. 0B 33.3
#> 12 worst 3 100 7.65µs 8.88µs 109957. 0B 44.0
#> 13 best 5 100 7.34µs 8.57µs 114203. 0B 34.3
#> 14 worst 5 100 7.74µs 9µs 107714. 0B 32.3
#> 15 best 10 100 7.34µs 8.64µs 111942. 0B 33.6
#> 16 worst 10 100 8.19µs 9.49µs 102356. 0B 30.7
#> 17 best 50 100 7.74µs 9.15µs 106079. 0B 31.8
#> 18 worst 50 100 12.95µs 14.26µs 68657. 0B 20.6
#> 19 best 100 100 8.56µs 9.82µs 99585. 0B 29.9
#> 20 worst 100 100 19.1µs 20.56µs 47127. 0B 14.1
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 9.04µs 10.5µs 91160. 0B 27.4
#> 2 worst 3 15 9.28µs 10.7µs 89999. 0B 36.0
#> 3 best 5 15 8.96µs 10.5µs 92021. 0B 27.6
#> 4 worst 5 15 9.38µs 11µs 88199. 0B 35.3
#> 5 best 10 15 8.88µs 10.4µs 93770. 0B 37.5
#> 6 worst 10 15 9.73µs 10.5µs 94077. 0B 28.2
#> 7 best 50 15 9.99µs 10.8µs 91551. 0B 27.5
#> 8 worst 50 15 13.07µs 13.9µs 70360. 0B 28.2
#> 9 best 100 15 10.81µs 11.8µs 83405. 0B 33.4
#> 10 worst 100 15 17.78µs 19.1µs 49673. 0B 19.9
#> 11 best 3 100 9.23µs 10.4µs 93274. 0B 28.0
#> 12 worst 3 100 9.98µs 11.1µs 86856. 0B 34.8
#> 13 best 5 100 9.1µs 10.4µs 93018. 0B 37.2
#> 14 worst 5 100 10.06µs 11.2µs 86770. 0B 26.0
#> 15 best 10 100 9.11µs 10.4µs 92886. 0B 37.2
#> 16 worst 10 100 11.35µs 12.6µs 76555. 0B 30.6
#> 17 best 50 100 10.39µs 11.5µs 84333. 0B 33.7
#> 18 worst 50 100 19.22µs 20.5µs 47556. 0B 14.3
#> 19 best 100 100 11.3µs 12.6µs 76652. 0B 30.7
#> 20 worst 100 100 30.77µs 32.5µs 29919. 0B 12.0