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.38µs 8.5µs 111163. 18.2KB 22.2
#> 2 foo_S3(x) 2.67µs 2.98µs 313137. 0B 31.3
#> 3 foo_S4(x) 2.81µs 3.23µs 301735. 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.99µs 15.56µs 62932. 0B 25.2
#> 2 bar_S4(x, y) 8.06µs 8.89µs 110908. 0B 22.2
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.22µs 8.49µs 115068. 0B 34.5
#> 2 worst 3 15 7.52µs 8.73µs 111757. 0B 33.5
#> 3 best 5 15 7.28µs 8.55µs 111295. 0B 33.4
#> 4 worst 5 15 7.61µs 8.85µs 110377. 0B 33.1
#> 5 best 10 15 7.4µs 8.66µs 112584. 0B 33.8
#> 6 worst 10 15 7.76µs 9.04µs 108244. 0B 21.7
#> 7 best 50 15 7.88µs 9.15µs 106749. 0B 32.0
#> 8 worst 50 15 9.42µs 10.72µs 91279. 0B 27.4
#> 9 best 100 15 8.47µs 9.83µs 98437. 0B 39.4
#> 10 worst 100 15 12.02µs 13.42µs 73122. 0B 21.9
#> 11 best 3 100 7.55µs 8.79µs 110936. 0B 33.3
#> 12 worst 3 100 7.79µs 9.1µs 106966. 0B 42.8
#> 13 best 5 100 7.49µs 8.83µs 110495. 0B 33.2
#> 14 worst 5 100 7.89µs 9.2µs 105735. 0B 31.7
#> 15 best 10 100 7.54µs 8.88µs 108897. 0B 32.7
#> 16 worst 10 100 8.19µs 9.54µs 99223. 0B 29.8
#> 17 best 50 100 7.82µs 9.16µs 105228. 0B 31.6
#> 18 worst 50 100 12.82µs 14.25µs 68271. 0B 20.5
#> 19 best 100 100 8.73µs 10.17µs 95641. 0B 28.7
#> 20 worst 100 100 19.15µs 20.67µs 47312. 0B 14.2
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.15µs 10.6µs 90786. 0B 27.2
#> 2 worst 3 15 9.44µs 10.8µs 88974. 0B 35.6
#> 3 best 5 15 9.12µs 10.6µs 90944. 0B 27.3
#> 4 worst 5 15 9.52µs 11µs 87174. 0B 34.9
#> 5 best 10 15 9.04µs 10.6µs 90859. 0B 36.4
#> 6 worst 10 15 9.84µs 10.6µs 93273. 0B 28.0
#> 7 best 50 15 9.75µs 10.7µs 92493. 0B 27.8
#> 8 worst 50 15 13.12µs 13.9µs 71170. 0B 28.5
#> 9 best 100 15 11.08µs 11.9µs 82333. 0B 32.9
#> 10 worst 100 15 17.82µs 19.1µs 51086. 0B 20.4
#> 11 best 3 100 9.4µs 10.5µs 92654. 0B 27.8
#> 12 worst 3 100 10.04µs 11.2µs 86615. 0B 34.7
#> 13 best 5 100 9.18µs 10.4µs 92729. 0B 37.1
#> 14 worst 5 100 10.21µs 11.4µs 85476. 0B 25.7
#> 15 best 10 100 9.14µs 10.4µs 93180. 0B 37.3
#> 16 worst 10 100 11.44µs 12.6µs 77021. 0B 30.8
#> 17 best 50 100 10.53µs 11.7µs 82259. 0B 32.9
#> 18 worst 50 100 19.45µs 20.7µs 47116. 0B 14.1
#> 19 best 100 100 11.45µs 12.7µs 76035. 0B 30.4
#> 20 worst 100 100 30.33µs 32.2µs 30204. 0B 12.1