--- title: "Performance" vignette: > %\VignetteIndexEntry{Performance} %\VignetteEngine{litedown::vignette} %\VignetteEncoding{UTF-8} --- The sequential Poisson method, and order sampling methods more generally, are simple and consequently not computationally expensive. This makes them suitable for a range of different applications, especially when drawing a sample from a large population. Despite this, there are two optimizations in this package to keep drawing samples fast. ## Calculating inclusion probabilities The first optimization concerns the inclusion probabilities when there are take-all units. As seen in `vignette("take-all")`, the algorithm finds take-all units one at a time without recomputing the inclusion probabilities many times. This is much faster than the naive approach when drawing a large sample, and is on par with the usual algorithm that finds take-all units in batches. ``` {.r} library(sps) # Make a population with 200 take-all units. x <- c(rep(1, 1e6 - 200), rep(1e6, 200)) n <- 1e3 # Naive implementation. ip <- function(x, n, alpha = 0.001) { p <- \(x, n) x * (n / sum(x)) ta_units <- integer(0) pi <- p(x, n) max_ts <- which.max(pi) while (pi[max_ts] > 1 - alpha) { ta_units <- c(ta_units, max_ts) pi <- p(replace(pi, max_ts, 0), n - length(ta_units)) max_ts <- which.max(pi) } replace(pi, ta_units, 1) } cols <- c("expression", "median", "mem_alloc", "n_itr") bench::mark( inclusion_prob(x, n), ip(x, n), sampling::inclusionprobabilities(x, n) )[cols] ``` ``` #> # A tibble: 3 × 4 #> expression median mem_alloc n_itr #> #> 1 inclusion_prob(x, n) 24.18ms 61.1MB 21 #> 2 ip(x, n) 1.41s 3GB 1 #> 3 sampling::inclusionprobabilities(x, n) 70.1ms 134MB 8 ``` ## Partial sorting The second optimization is recognizing that both the computation of inclusion probabilities and the sequential Poisson method can benefit from partial sorting algorithms. In both cases, only the $n$ largest/smallest elements of a vector are needed and, when installed, the `topn()` function from the **kit** package is used to avoid a complete sort. This is a drop-in replacement for `order(x)[1:n]` that can be faster when `n` is much smaller than the length of `x`, and can have a modest impact on performance when drawing a sample from a large population. ``` {.r} options(sps.usekit = TRUE) bench::mark(sps(x, n))[cols] ``` ``` #> # A tibble: 1 × 4 #> expression median mem_alloc n_itr #> #> 1 sps(x, n) 75.5ms 111MB 6 ``` ``` {.r} options(sps.usekit = FALSE) bench::mark(sps(x, n))[cols] ``` ``` #> # A tibble: 1 × 4 #> expression median mem_alloc n_itr #> #> 1 sps(x, n) 153ms 126MB 4 ``` Partial sorting generally speeds up drawing smaller samples as well, but the effect is not as large because the sequential Poisson method is already quick. ## Iteratively drawing a sample Both optimizations above can be seen when iteratively drawing a sequential Poisson sample as creating an iterator involves finding the point at which each unit enters the take-all stratum. Although this is more expensive than drawing a single sample, it quickly pays off because of the small cost to iterating over units in the sample. ``` {.r} bench::mark(s <- sps_iterator(x, n), min_iterations = 10)[cols] ``` ``` #> # A tibble: 1 × 4 #> expression median mem_alloc n_itr #> #> 1 s <- sps_iterator(x, n) 931ms 336MB 10 ``` ``` {.r} bench::mark(s())[cols] ``` ``` #> # A tibble: 1 × 4 #> expression median mem_alloc n_itr #> #> 1 s() 2.82ms 15.2MB 80 ``` ``` {.r} bench::mark(sps(x, n)) ``` ``` #> # A tibble: 1 × 13 #> expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time #> #> 1 sps(x, n) 158ms 159ms 6.13 126MB 7.66 4 5 653ms #> # ℹ 4 more variables: result , memory , time , gc ```