{hash}
This vignette provides a
comparison of {r2r}
with the same-purpose CRAN package {hash}
,
which also offers an implementation of hash tables based on R
environments. We first describe the features offered by both packages,
and then perform some benchmark timing comparisons. The package versions
referred to in this vignette are:
library(hash)
library(r2r)
packageVersion("hash")
#> [1] '2.2.6.3'
packageVersion("r2r")
#> [1] '0.1.1'
Both {r2r}
and {hash}
hash tables are built
on top of the R built-in environment
data structure, and
have thus a similar API. In particular, hash table objects have
reference semantics for both packages. {r2r}
hashtable
s are S3 class objects, whereas in
{hash}
the data structure is implemented as an S4
class.
Hash tables provided by r2r
support arbitrary type keys
and values, arbitrary key comparison and hash functions, and have
customizable behaviour (either throw an exception or return a default
value) upon query of a missing key.
In contrast, hash tables in hash
currently support only
string keys, with basic identity comparison (the hashing is performed
automatically by the underlying environment
objects);
values can be arbitrary R objects. Querying missing keys through
non-vectorized [[
-subsetting returns the default value
NULL
, whereas queries through vectorized
[
-subsetting result in an error. On the other hand,
hash
also offers support for inverting hash tables (an
experimental feature at the time of writing).
The table below summarizes the features of the two packages
Feature | r2r | hash |
---|---|---|
Basic data structure | R environment | R environment |
Arbitrary type keys | X | |
Arbitrary type values | X | X |
Arbitrary hash function | X | |
Arbitrary key comparison function | X | |
Throw or return default on missing keys | X | |
Hash table inversion | X |
We will perform our benchmark tests using the CRAN package microbenchmark
.
We start by timing the insertion of:
random key-value pairs (with possible repetitions). In order to perform a meaningful comparison between the two packages, we restrict to string (i.e. length one character) keys. We can generate random keys as follows:
chars <- c(letters, LETTERS, 0:9)
random_keys <- function(n) paste0(
sample(chars, n, replace = TRUE),
sample(chars, n, replace = TRUE),
sample(chars, n, replace = TRUE),
sample(chars, n, replace = TRUE),
sample(chars, n, replace = TRUE)
)
set.seed(840)
keys <- random_keys(N)
values <- rnorm(N)
We test both the non-vectorized ([[<-
) and vectorized
([<-
) operators:
microbenchmark(
`r2r_[[<-` = {
for (i in seq_along(keys))
m_r2r[[ keys[[i]] ]] <- values[[i]]
},
`r2r_[<-` = { m_r2r[keys] <- values },
`hash_[[<-` = {
for (i in seq_along(keys))
m_hash[[ keys[[i]] ]] <- values[[i]]
},
`hash_[<-` = m_hash[keys] <- values,
times = 30,
setup = { m_r2r <- hashmap(); m_hash <- hash() }
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> r2r_[[<- 73.45672 92.70669 123.62721 112.85573 144.90794 236.9923 30
#> r2r_[<- 64.04095 74.03435 109.04551 103.57653 142.90232 173.8177 30
#> hash_[[<- 63.14849 79.72931 102.30746 93.71738 116.13100 186.4778 30
#> hash_[<- 33.48111 41.43365 58.71698 63.78374 68.69394 102.4354 30
As it is seen, r2r
and hash
have comparable
performances at the insertion of key-value pairs, with both vectorized
and non-vectorized insertions, hash
being somewhat more
efficient in both cases.
We now test key query, again both in non-vectorized and vectorized form:
microbenchmark(
`r2r_[[` = { for (key in keys) m_r2r[[ key ]] },
`r2r_[` = { m_r2r[ keys ] },
`hash_[[` = { for (key in keys) m_hash[[ key ]] },
`hash_[` = { m_hash[ keys ] },
times = 30,
setup = {
m_r2r <- hashmap(); m_r2r[keys] <- values
m_hash <- hash(); m_hash[keys] <- values
}
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> r2r_[[ 84.806640 100.262967 124.04996 123.85817 140.46635 192.44535 30
#> r2r_[ 76.286091 86.518208 113.18009 104.74811 132.75563 183.97599 30
#> hash_[[ 9.529629 9.875433 13.17707 10.91300 15.50973 33.30505 30
#> hash_[ 52.462140 60.473160 77.26813 71.97034 89.86606 144.10507 30
For non-vectorized queries, hash
is significantly faster
(by one order of magnitude) than r2r
. This is likely due to
the fact that the [[
method dispatch is handled natively by
R in hash
(i.e. the default [[
method
for environment
s is used ), whereas r2r
suffers the overhead of S3 method dispatch. This is confirmed by the
result for vectorized queries, which is comparable for the two packages;
notice that here a single (rather than N
) S3 method
dispatch occurs in the r2r
timed expression.
As an additional test, we perform the benchmarks for non-vectorized expressions with a new set of keys:
set.seed(841)
new_keys <- random_keys(N)
microbenchmark(
`r2r_[[_bis` = { for (key in new_keys) m_r2r[[ key ]] },
`hash_[[_bis` = { for (key in new_keys) m_hash[[ key ]] },
times = 30,
setup = {
m_r2r <- hashmap(); m_r2r[keys] <- values
m_hash <- hash(); m_hash[keys] <- values
}
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> r2r_[[_bis 61.072977 71.02243 88.33356 79.70052 101.82955 157.15263 30
#> hash_[[_bis 9.483412 10.12755 13.10971 11.14707 14.05105 36.60939 30
The results are similar to the ones already commented. Finally, we
test the performances of the two packages in checking the existence of
keys (notice that here has_key
refers to
r2r::has_key
, whereas has.key
is
hash::has.key
):
set.seed(842)
mixed_keys <- sample(c(keys, new_keys), N)
microbenchmark(
r2r_has_key = { for (key in mixed_keys) has_key(m_r2r, key) },
hash_has_key = { for (key in new_keys) has.key(key, m_hash) },
times = 30,
setup = {
m_r2r <- hashmap(); m_r2r[keys] <- values
m_hash <- hash(); m_hash[keys] <- values
}
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval
#> r2r_has_key 60.44007 68.84383 79.75397 78.06496 87.1900 145.2095 30
#> hash_has_key 178.04685 202.45473 228.72759 222.17070 257.8577 286.1805 30
The results are comparable for the two packages, r2r
being slightly more performant in this particular case.
Finally, we test key deletion. In order to handle name collisions, we
will use delete()
(which refers to
r2r::delete()
) and del()
(which refers to
hash::del()
).
microbenchmark(
r2r_delete = { for (key in keys) delete(m_r2r, key) },
hash_delete = { for (key in keys) del(key, m_hash) },
hash_vectorized_delete = { del(keys, m_hash) },
times = 30,
setup = {
m_r2r <- hashmap(); m_r2r[keys] <- values
m_hash <- hash(); m_hash[keys] <- values
}
)
#> Unit: milliseconds
#> expr min lq mean median uq
#> r2r_delete 103.69075 115.44276 153.120405 154.939656 174.653658
#> hash_delete 60.84627 69.87706 87.000242 87.454847 96.645069
#> hash_vectorized_delete 1.95995 2.14816 2.411817 2.374413 2.589974
#> max neval
#> 251.678265 30
#> 149.739116 30
#> 3.392138 30
The vectorized version of hash
significantly outperforms
the non-vectorized versions (by roughly two orders of magnitude in
speed). Currently, r2r
does not support vectorized key
deletion 1.
The two R packages r2r
and hash
offer hash
table implementations with different advantages and drawbacks.
r2r
focuses on flexibility, and has a richer set of
features. hash
is more minimal, but offers superior
performance in some important tasks. Finally, as a positive note for
both parties, the two packages share a similar API, making it relatively
easy to switch between the two, according to the particular use case
needs.
This is due to complications introduced by the internal
hash collision handling system of r2r
.↩︎