diff --git a/NAMESPACE b/NAMESPACE index 54fb81a..c3eeb37 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,5 @@ S3method("$<-",savvy_blazr__sealed) S3method("[[<-",savvy_blazr__sealed) -S3method(print,Person__bundle) -export(int_times_int) -export(to_upper) +export(sum_with_threads) useDynLib(blazr, .registration = TRUE) diff --git a/R/000-wrappers.R b/R/000-wrappers.R index 1cad530..3c12ace 100644 --- a/R/000-wrappers.R +++ b/R/000-wrappers.R @@ -37,67 +37,16 @@ NULL stop(class, " cannot be modified", call. = FALSE) } -#' Convert Input To Upper-Case +#' Calculate the sum of a vector of integers using multiple threads. #' -#' @param x A character vector. -#' @returns A character vector with upper case version of the input. -#' @export -`to_upper` <- function(`x`) { - .Call(savvy_to_upper__impl, `x`) -} - -#' Multiply Input By Another Input +#' @param x A vector of integers to sum over. +#' @param n The number of threads used to compute this calculation (int). +#' +#' @return The sum of all elements of the input vector. #' -#' @param x An integer vector. -#' @param y An integer to multiply. -#' @returns An integer vector with values multiplied by `y`. #' @export -`int_times_int` <- function(`x`, `y`) { - .Call(savvy_int_times_int__impl, `x`, `y`) +`sum_with_threads` <- function(`x`, `n`) { + .Call(savvy_sum_with_threads__impl, `x`, `n`) } -### wrapper functions for Person - -`Person_set_name` <- function(self) { - function(`name`) { - invisible(.Call(savvy_Person_set_name__impl, `self`, `name`)) - } -} - -`Person_name` <- function(self) { - function() { - .Call(savvy_Person_name__impl, `self`) - } -} - -`.savvy_wrap_Person` <- function(ptr) { - e <- new.env(parent = emptyenv()) - e$.ptr <- ptr - e$`set_name` <- `Person_set_name`(ptr) - e$`name` <- `Person_name`(ptr) - - class(e) <- c("Person", "savvy_blazr__sealed") - e -} - - -`Person` <- new.env(parent = emptyenv()) - -### associated functions for Person - -`Person`$`new` <- function() { - .savvy_wrap_Person(.Call(savvy_Person_new__impl)) -} - -`Person`$`associated_function` <- function() { - .Call(savvy_Person_associated_function__impl) -} - - -class(`Person`) <- c("Person__bundle", "savvy_blazr__sealed") - -#' @export -`print.Person__bundle` <- function(x, ...) { - cat('Person') -} diff --git a/README.Rmd b/README.Rmd index 32224cf..b1ea87c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -36,10 +36,53 @@ pak::pak("r-staceans/blazr") ## Example -This is just a dummy example: +Here's a simple example, computing the sum of a vector of integers, using multiple threads: ```{r example} library(blazr) -blazr::to_upper("hello, world") +create_int_vector <- function(n) { + set.seed(42) + sample.int(100, n, replace = TRUE) +} + +n <- 1e8 + +x <- create_int_vector(n) + +blazr::sum_with_threads( + x, + n = 2L +) +``` + +## Benchmarking + +When running this sum against very large numbers, we can see the performance benefits of using multiple threads: + +```{r benchmark} +library(bench) +library(ggplot2) + +results <- bench::press( + size = c(1e7, 1e8, 1e9), + { + x = create_int_vector(n) + bench::mark( + single_thread = { + blazr::sum_with_threads( + x, + n = 1L + ) + }, + multi_thread = { + blazr::sum_with_threads( + x, + n = 4L + ) + } + ) + }) + +ggplot2::autoplot(results) ``` diff --git a/README.md b/README.md index 3c430a0..f81905f 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,64 @@ pak::pak("r-staceans/blazr") ## Example -This is just a dummy example: +Here’s a simple example, computing the sum of a vector of integers, +using multiple threads: ``` r library(blazr) -blazr::to_upper("hello, world") -#> [1] "HELLO, WORLD" +create_int_vector <- function(n) { + set.seed(42) + sample.int(100, n, replace = TRUE) +} + +n <- 1e8 + +x <- create_int_vector(n) + +blazr::sum_with_threads( + x, + n = 2L +) +#> [1] 754832969 ``` + +## Benchmarking + +When running this sum against very large numbers, we can see the +performance benefits of using multiple threads: + +``` r +library(bench) +library(ggplot2) + +results <- bench::press( + size = c(1e7, 1e8, 1e9), + { + x = create_int_vector(n) + bench::mark( + single_thread = { + blazr::sum_with_threads( + x, + n = 1L + ) + }, + multi_thread = { + blazr::sum_with_threads( + x, + n = 4L + ) + } + ) + }) +#> Running with: +#> size +#> 1 10000000 +#> 2 100000000 +#> 3 1000000000 + +ggplot2::autoplot(results) +#> Loading required namespace: tidyr +``` + + diff --git a/man/blazr-package.Rd b/man/blazr-package.Rd index d20cc1e..48f2e16 100644 --- a/man/blazr-package.Rd +++ b/man/blazr-package.Rd @@ -6,6 +6,8 @@ \alias{blazr-package} \title{blazr: What the Package Does (One Line, Title Case)} \description{ +\if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} + What the package does (one paragraph). } \seealso{ diff --git a/man/figures/README-benchmark-1.png b/man/figures/README-benchmark-1.png new file mode 100644 index 0000000..fd3b1bb Binary files /dev/null and b/man/figures/README-benchmark-1.png differ diff --git a/man/int_times_int.Rd b/man/int_times_int.Rd deleted file mode 100644 index 565baee..0000000 --- a/man/int_times_int.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/000-wrappers.R -\name{int_times_int} -\alias{int_times_int} -\title{Multiply Input By Another Input} -\usage{ -int_times_int(x, y) -} -\arguments{ -\item{x}{An integer vector.} - -\item{y}{An integer to multiply.} -} -\value{ -An integer vector with values multiplied by \code{y}. -} -\description{ -Multiply Input By Another Input -} diff --git a/man/sum_with_threads.Rd b/man/sum_with_threads.Rd new file mode 100644 index 0000000..06fbfbc --- /dev/null +++ b/man/sum_with_threads.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{sum_with_threads} +\alias{sum_with_threads} +\title{Calculate the sum of a vector of integers using multiple threads.} +\usage{ +sum_with_threads(x, n) +} +\arguments{ +\item{x}{A vector of integers to sum over.} + +\item{n}{The number of threads used to compute this calculation (int).} +} +\value{ +The sum of all elements of the input vector. +} +\description{ +Calculate the sum of a vector of integers using multiple threads. +} diff --git a/man/to_upper.Rd b/man/to_upper.Rd deleted file mode 100644 index e2d9321..0000000 --- a/man/to_upper.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/000-wrappers.R -\name{to_upper} -\alias{to_upper} -\title{Convert Input To Upper-Case} -\usage{ -to_upper(x) -} -\arguments{ -\item{x}{A character vector.} -} -\value{ -A character vector with upper case version of the input. -} -\description{ -Convert Input To Upper-Case -} diff --git a/src/init.c b/src/init.c index ef313b6..2e0293f 100644 --- a/src/init.c +++ b/src/init.c @@ -34,44 +34,14 @@ SEXP handle_result(SEXP res_) { return (SEXP)res; } -SEXP savvy_to_upper__impl(SEXP c_arg__x) { - SEXP res = savvy_to_upper__ffi(c_arg__x); - return handle_result(res); -} - -SEXP savvy_int_times_int__impl(SEXP c_arg__x, SEXP c_arg__y) { - SEXP res = savvy_int_times_int__ffi(c_arg__x, c_arg__y); - return handle_result(res); -} - -SEXP savvy_Person_new__impl(void) { - SEXP res = savvy_Person_new__ffi(); - return handle_result(res); -} - -SEXP savvy_Person_set_name__impl(SEXP self__, SEXP c_arg__name) { - SEXP res = savvy_Person_set_name__ffi(self__, c_arg__name); - return handle_result(res); -} - -SEXP savvy_Person_name__impl(SEXP self__) { - SEXP res = savvy_Person_name__ffi(self__); - return handle_result(res); -} - -SEXP savvy_Person_associated_function__impl(void) { - SEXP res = savvy_Person_associated_function__ffi(); +SEXP savvy_sum_with_threads__impl(SEXP c_arg__x, SEXP c_arg__n) { + SEXP res = savvy_sum_with_threads__ffi(c_arg__x, c_arg__n); return handle_result(res); } static const R_CallMethodDef CallEntries[] = { - {"savvy_to_upper__impl", (DL_FUNC) &savvy_to_upper__impl, 1}, - {"savvy_int_times_int__impl", (DL_FUNC) &savvy_int_times_int__impl, 2}, - {"savvy_Person_new__impl", (DL_FUNC) &savvy_Person_new__impl, 0}, - {"savvy_Person_set_name__impl", (DL_FUNC) &savvy_Person_set_name__impl, 2}, - {"savvy_Person_name__impl", (DL_FUNC) &savvy_Person_name__impl, 1}, - {"savvy_Person_associated_function__impl", (DL_FUNC) &savvy_Person_associated_function__impl, 0}, + {"savvy_sum_with_threads__impl", (DL_FUNC) &savvy_sum_with_threads__impl, 2}, {NULL, NULL, 0} }; diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 96fe1ab..d08356c 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1,6 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "blazr" +version = "0.1.0" +dependencies = [ + "savvy", +] [[package]] name = "cc" @@ -11,13 +18,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "blazr" -version = "0.1.0" -dependencies = [ - "savvy", -] - [[package]] name = "once_cell" version = "1.20.2" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 2986339..2b6984b 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -9,6 +9,9 @@ crate-type = ["staticlib", "lib"] [dependencies] savvy = "*" +[features] +savvy-test = [] + [profile.release] # By default, on release build, savvy terminates the R session when a panic # occurs. This is the right behavior in that a panic means such a fatal event diff --git a/src/rust/api.h b/src/rust/api.h index c5f3fd1..e3ea717 100644 --- a/src/rust/api.h +++ b/src/rust/api.h @@ -1,8 +1 @@ -SEXP savvy_to_upper__ffi(SEXP c_arg__x); -SEXP savvy_int_times_int__ffi(SEXP c_arg__x, SEXP c_arg__y); - -// methods and associated functions for Person -SEXP savvy_Person_new__ffi(void); -SEXP savvy_Person_set_name__ffi(SEXP self__, SEXP c_arg__name); -SEXP savvy_Person_name__ffi(SEXP self__); -SEXP savvy_Person_associated_function__ffi(void); \ No newline at end of file +SEXP savvy_sum_with_threads__ffi(SEXP c_arg__x, SEXP c_arg__n); diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 9b94e3e..280897a 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -1,117 +1,106 @@ -// Example functions +use savvy::{savvy, IntegerSexp, Sexp}; +use std::thread; -use savvy::savvy; - -use savvy::{IntegerSexp, OwnedIntegerSexp, OwnedStringSexp, StringSexp}; - -use savvy::NotAvailableValue; - -/// Convert Input To Upper-Case +/// Calculate the sum of a vector of integers using multiple threads. +/// +/// @param x A vector of integers to sum over. +/// @param n The number of threads used to compute this calculation (int). +/// +/// @return The sum of all elements of the input vector. /// -/// @param x A character vector. -/// @returns A character vector with upper case version of the input. /// @export #[savvy] -fn to_upper(x: StringSexp) -> savvy::Result { - let mut out = OwnedStringSexp::new(x.len())?; +fn sum_with_threads(x: IntegerSexp, n: i32) -> savvy::Result { + let x_rust = x.to_vec(); + let n_usize: usize = n as usize; - for (i, e) in x.iter().enumerate() { - if e.is_na() { - out.set_na(i)?; - continue; - } + let out = sum_with_threads_impl(x_rust, n_usize); + out.try_into() +} - let e_upper = e.to_uppercase(); - out.set_elt(i, &e_upper)?; +fn sum_with_threads_impl(x: Vec, n: usize) -> i32 { + if x.is_empty() { + eprintln!("Input vector is empty. Returning 0."); + return 0; } - Ok(out.into()) -} + let n = n.min(x.len()); + let chunk_size = (x.len() + n - 1) / n; -/// Multiply Input By Another Input -/// -/// @param x An integer vector. -/// @param y An integer to multiply. -/// @returns An integer vector with values multiplied by `y`. -/// @export -#[savvy] -fn int_times_int(x: IntegerSexp, y: i32) -> savvy::Result { - let mut out = OwnedIntegerSexp::new(x.len())?; - - for (i, e) in x.iter().enumerate() { - if e.is_na() { - out.set_na(i)?; - } else { - out[i] = e * y; - } + let mut handles = Vec::new(); + for i in 0..n { + let chunk = x[i * chunk_size..((i + 1) * chunk_size).min(x.len())].to_vec(); + handles.push(thread::spawn(move || chunk.iter().sum::())); } - Ok(out.into()) -} + let mut total_sum = 0; + for handle in handles { + total_sum += handle.join().expect("Thread panicked"); + } -#[savvy] -struct Person { - pub name: String, + total_sum } -/// A person with a name -/// -/// @export -#[savvy] -impl Person { - fn new() -> Self { - Self { - name: "".to_string(), - } - } +#[cfg(test)] +mod tests { + use crate::sum_with_threads_impl; - fn set_name(&mut self, name: &str) -> savvy::Result<()> { - self.name = name.to_string(); - Ok(()) + #[test] + fn test_single_thread() { + let numbers = vec![1, 2, 3, 4, 5]; + let n = 1; + assert_eq!(sum_with_threads_impl(numbers, n), 15); } - fn name(&self) -> savvy::Result { - let mut out = OwnedStringSexp::new(1)?; - out.set_elt(0, &self.name)?; - Ok(out.into()) + #[test] + fn test_multiple_threads() { + let x = vec![1, 2, 3, 4]; + let num_threads = 2; + + let result = sum_with_threads_impl(x, num_threads); + assert_eq!(result, 10); } - fn associated_function() -> savvy::Result { - let mut out = OwnedStringSexp::new(1)?; - out.set_elt(0, "associated_function")?; - Ok(out.into()) + #[test] + fn test_more_threads_than_elements() { + let numbers = vec![1, 2, 3, 4, 5]; + let n = 10; + assert_eq!(sum_with_threads_impl(numbers, n), 15); } -} -// This test is run by `cargo test`. You can put tests that don't need a real -// R session here. -#[cfg(test)] -mod test1 { #[test] - fn test_person() { - let mut p = super::Person::new(); - p.set_name("foo").expect("set_name() must succeed"); - assert_eq!(&p.name, "foo"); + fn test_empty_vector() { + let numbers: Vec = vec![]; + let n = 4; + assert_eq!(sum_with_threads_impl(numbers, n), 0); } -} -// Tests marked under `#[cfg(feature = "savvy-test")]` are run by `savvy-cli test`, which -// executes the Rust code on a real R session so that you can use R things for -// testing. -#[cfg(feature = "savvy-test")] -mod test1 { - // The return type must be `savvy::Result<()>` #[test] - fn test_to_upper() -> savvy::Result<()> { - // You can create a non-owned version of input by `.as_read_only()` - let x = savvy::OwnedStringSexp::try_from_slice(["foo", "bar"])?.as_read_only(); + fn test_large_numbers() { + let numbers = vec![1_000_000, 2_000_000, 3_000_000]; + let n = 3; + assert_eq!(sum_with_threads_impl(numbers, n), 6_000_000); + } - let result = super::to_upper(x)?; + #[test] + fn test_negative_numbers() { + let numbers = vec![-1, -2, -3, -4, -5]; + let n = 2; + assert_eq!(sum_with_threads_impl(numbers, n), -15); + } - // This function compares an SEXP with the result of R code specified in - // the second argument. - savvy::assert_eq_r_code(result, r#"c("FOO", "BAR")"#); + #[test] + fn test_mixed_numbers() { + let numbers = vec![-1, 2, -3, 4, -5, 6]; + let n = 3; + assert_eq!(sum_with_threads_impl(numbers, n), 3); + } - Ok(()) + #[test] + fn test_large_vector() { + let numbers: Vec = (1..=1_000).collect(); + let n = 4; + let expected_sum: i32 = (1..=1_000).sum(); + assert_eq!(sum_with_threads_impl(numbers, n), expected_sum); } } diff --git a/tests/testthat/test-add.R b/tests/testthat/test-add.R deleted file mode 100644 index ac8727f..0000000 --- a/tests/testthat/test-add.R +++ /dev/null @@ -1,3 +0,0 @@ -test_that("addition works", { - expect_equal(add(2, 2), 4) -}) diff --git a/tests/testthat/test-sum_with_threads.R b/tests/testthat/test-sum_with_threads.R new file mode 100644 index 0000000..922192d --- /dev/null +++ b/tests/testthat/test-sum_with_threads.R @@ -0,0 +1,6 @@ +test_that("sums as expected", { + vector <- 1:10 + + out <- blazr::sum_with_threads(1:10, 5L) + expect_equal(out, sum(vector)) +})