Skip to content

Commit

Permalink
Performance improvements - new return types in rust (#57)
Browse files Browse the repository at this point in the history
## Rust 4.0.0 & Python 3.0.1

- **BREAKING CHANGES - Rust**: new return types for significant performance improvements:
  - `MultiFizzBuzz` now lazily returns a rayon IndexedParallelIterator
  - `FizzBuzz` returns a `FizzBuzzAnswer` which can be converted into a `String` or `Cow<str>`
  - `FizzBuzzAnswer` now represents the valid answers to FizzBuzz, not `One(String)`or `Many(Vec>String>)`
- Python implementation updated to work with rust v4.0.0 which brings slight performance improvements (approx 10-20%)

<!-- Generated by sourcery-ai[bot]: start summary -->

## Summary by Sourcery

This pull request introduces significant performance improvements by updating the return types for `FizzBuzz` and `MultiFizzBuzz`. The `FizzBuzzAnswer` enum has been refactored to directly represent valid FizzBuzz answers. The Python implementation has been updated to work with Rust v4.0.0, bringing slight performance improvements. Documentation and tests have been updated accordingly.

* **New Features**:
    - Introduced new return types for `FizzBuzz` and `MultiFizzBuzz` to improve performance, including lazy evaluation with `rayon::iter::IndexedParallelIterator`.
* **Enhancements**:
    - Updated `FizzBuzzAnswer` to represent valid FizzBuzz answers directly, removing the need for `One(String)` and `Many(Vec<String>)` variants.
    - Enhanced the Python implementation to work with Rust v4.0.0, resulting in slight performance improvements (approx 10-20%).
* **Documentation**:
    - Updated documentation to reflect new return types and usage examples for `FizzBuzz` and `MultiFizzBuzz`.
* **Tests**:
    - Added new tests to cover the updated `FizzBuzzAnswer` and `MultiFizzBuzz` implementations, including tests for negative numbers and non-whole numbers.
    - Updated existing tests to use the new return types and ensure compatibility with the new implementation.

<!-- Generated by sourcery-ai[bot]: end summary -->
  • Loading branch information
MusicalNinjaDad authored Jun 9, 2024
1 parent 182e5e5 commit 8f85294
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 116 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# FizzBuzz Changelog

## Rust 4.0.0 & Python 3.0.1

- **BREAKING CHANGES - Rust**: new return types for significant performance improvements:
- `MultiFizzBuzz` now lazily returns a rayon IndexedParallelIterator
- `FizzBuzz` returns a `FizzBuzzAnswer` which can be converted into a `String` or `Cow<str>`
- `FizzBuzzAnswer` now represents the valid answers to FizzBuzz, not `One(String)`or `Many(Vec>String>)`
- Python implementation updated to work with rust v4.0.0 which brings slight performance improvements (approx 10-20%)

## Python 3.0.0

- Return `list[str]` when passed a `list` or `slice`, continue to return `str`when passed a single `int`.
Expand Down
2 changes: 1 addition & 1 deletion __pyversion__
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.0
3.0.1
2 changes: 1 addition & 1 deletion rust/fizzbuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fizzbuzz"
version = "3.0.2"
version = "4.0.0"
edition = "2021"

[lib]
Expand Down
24 changes: 14 additions & 10 deletions rust/fizzbuzz/benches/bench_fizzbuzz.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![allow(dead_code)]
use std::borrow::Cow;

use criterion::{criterion_group, criterion_main, Criterion};
use fizzbuzz::{self, FizzBuzz, MultiFizzBuzz};
use fizzbuzz::{self, FizzBuzz, FizzBuzzAnswer, MultiFizzBuzz};
use rayon::prelude::*;

static TEST_SIZE: isize = 1_000_000;
Expand Down Expand Up @@ -43,27 +45,27 @@ fn vec_pariter() {
}

#[inline]
fn multifizzbuzz_trait() {
fn multifizzbuzz_trait_as_vec_string() {
let inputs: Vec<_> = (1..TEST_SIZE).collect();
let _: Vec<String> = inputs.fizzbuzz().into();
let _: Vec<String> = inputs.fizzbuzz().collect();
}

#[inline]
fn multifizzbuzz_trait_as_string() {
fn multifizzbuzz_trait_as_vec_cow() {
let inputs: Vec<_> = (1..TEST_SIZE).collect();
let _: String = inputs.fizzbuzz().into();
let _: Vec<Cow<str>> = inputs.fizzbuzz().collect();
}

#[inline]
fn multifizzbuzz_trait_from_vec_as_answer() {
let inputs: Vec<_> = (1..TEST_SIZE).collect();
let _ = inputs.fizzbuzz();
let _: Vec<FizzBuzzAnswer> = inputs.fizzbuzz().collect();
}

#[inline]
fn multifizzbuzz_trait_from_range_as_answer() {
let inputs = 1..TEST_SIZE;
let _ = inputs.fizzbuzz();
let _: Vec<FizzBuzzAnswer> = inputs.fizzbuzz().collect();
}

fn criterion_benchmark(c: &mut Criterion) {
Expand All @@ -74,9 +76,11 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("vec_iter", |b| b.iter(|| vec_iter()));
// c.bench_function("vec_intoiter", |b| b.iter(|| vec_intoiter()));
c.bench_function("vec_pariter", |b| b.iter(|| vec_pariter()));
c.bench_function("multifizzbuzz_trait", |b| b.iter(|| multifizzbuzz_trait()));
c.bench_function("multifizzbuzz_trait_as_string", |b| {
b.iter(|| multifizzbuzz_trait_as_string())
c.bench_function("multifizzbuzz_trait_as_vec_string", |b| {
b.iter(|| multifizzbuzz_trait_as_vec_string())
});
c.bench_function("multifizzbuzz_trait_as_vec_cow", |b| {
b.iter(|| multifizzbuzz_trait_as_vec_cow())
});
c.bench_function("multifizzbuzz_trait_from_vec_as_answer", |b| {
b.iter(|| multifizzbuzz_trait_from_vec_as_answer())
Expand Down
13 changes: 7 additions & 6 deletions rust/fizzbuzz/benches/bench_sizes.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
#![allow(dead_code)]
use criterion::{criterion_group, criterion_main, Criterion};
use fizzbuzz::{self, MultiFizzBuzz};
use fizzbuzz::{self, FizzBuzzAnswer, MultiFizzBuzz};
use rayon::iter::ParallelIterator;

#[inline]
fn range_20() {
const TEST_SIZE: i32 = 20;
let _ = (1..TEST_SIZE).fizzbuzz();
let _: Vec<FizzBuzzAnswer> = (1..TEST_SIZE).fizzbuzz().collect();
}

#[inline]
fn range_200_000() {
const TEST_SIZE: i32 = 200_000;
let _ = (1..TEST_SIZE).fizzbuzz();
let _: Vec<FizzBuzzAnswer> = (1..TEST_SIZE).fizzbuzz().collect();
}
#[inline]
fn range_300_000() {
const TEST_SIZE: i32 = 300_000;
let _ = (1..TEST_SIZE).fizzbuzz();
let _: Vec<FizzBuzzAnswer> = (1..TEST_SIZE).fizzbuzz().collect();
}
#[inline]
fn range_1_000_000() {
const TEST_SIZE: i32 = 1_000_000;
let _ = (1..TEST_SIZE).fizzbuzz();
let _: Vec<FizzBuzzAnswer> = (1..TEST_SIZE).fizzbuzz().collect();
}
#[inline]
fn range_10_000_000() {
const TEST_SIZE: i32 = 10_000_000;
let _ = (1..TEST_SIZE).fizzbuzz();
let _: Vec<FizzBuzzAnswer> = (1..TEST_SIZE).fizzbuzz().collect();
}

fn criterion_benchmark(c: &mut Criterion) {
Expand Down
138 changes: 77 additions & 61 deletions rust/fizzbuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,61 @@
//! constructed from `0`,`3`,`5` and support the `%` operator.
//!
//!
//! ## Example usage:
//! ## Example usage for single item:
//!
//! ```
//! use fizzbuzz::FizzBuzz;
//! use std::borrow::Cow;
//!
//! let one: String = 1.fizzbuzz().into();
//! let one: Cow<str> = 1.fizzbuzz().into();
//! let three: String = 3.fizzbuzz().into();
//! assert_eq!(one, "1".to_string());
//! assert_eq!(three, "fizz".to_string());
//! assert_eq!(&one, "1");
//! assert_eq!(three, "fizz");
//! ```
//!
//! ## Example usage for multiple items:
//!
//! ```
//! use fizzbuzz::MultiFizzBuzz;
//! use rayon::iter::ParallelIterator; // required to `.collect()` the results
//!
//! let one_to_five = vec![1,2,3,4,5];
//! let fizzbuzzed: Vec<String> = one_to_five.fizzbuzz().collect();
//! assert_eq!(fizzbuzzed, vec!["1", "2", "fizz", "4", "buzz"]);
//! ```
use std::borrow::Cow;

use rayon::prelude::*;
static BIG_VECTOR: usize = 300_000; // Size from which parallelisation makes sense

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
/// Provides conversion to `String` and `Vec<String>` via `.into()`,
/// ::From() etc.
/// Represents a valid answer to fizzbuzz and provides conversion to `String` and `Cow<&str>` via `.into()`
pub enum FizzBuzzAnswer {
/// Stores a single FizzBuzz value
One(String),
/// Stores a series of FizzBuzz values
Many(Vec<String>),
Fizz,
Buzz,
Fizzbuzz,
Number(String),
}

impl From<FizzBuzzAnswer> for String {
fn from(value: FizzBuzzAnswer) -> Self {
match value {
FizzBuzzAnswer::One(s) => s,
FizzBuzzAnswer::Many(v) => v.join(", "),
impl From<FizzBuzzAnswer> for Cow<'static, str> {
fn from(answer: FizzBuzzAnswer) -> Self {
match answer {
FizzBuzzAnswer::Fizz => "fizz".into(),
FizzBuzzAnswer::Buzz => "buzz".into(),
FizzBuzzAnswer::Fizzbuzz => "fizzbuzz".into(),
FizzBuzzAnswer::Number(n) => n.into(),
}
}
}

impl From<FizzBuzzAnswer> for Vec<String> {
fn from(value: FizzBuzzAnswer) -> Self {
match value {
FizzBuzzAnswer::One(s) => vec![s],
FizzBuzzAnswer::Many(v) => v,
impl From<FizzBuzzAnswer> for String {
fn from(answer: FizzBuzzAnswer) -> Self {
match answer {
FizzBuzzAnswer::Fizz => "fizz".into(),
FizzBuzzAnswer::Buzz => "buzz".into(),
FizzBuzzAnswer::Fizzbuzz => "fizzbuzz".into(),
FizzBuzzAnswer::Number(n) => n,
}
}
}
Expand Down Expand Up @@ -76,91 +93,90 @@ where
fn fizzbuzz(&self) -> FizzBuzzAnswer {
let three = match <Num>::try_from(3_u8) {
Ok(three) => three,
Err(_) => return FizzBuzzAnswer::One(self.to_string()),
Err(_) => return FizzBuzzAnswer::Number(self.to_string()),
};
let five = match <Num>::try_from(5_u8) {
Ok(five) => five,
Err(_) => return FizzBuzzAnswer::One(self.to_string()),
Err(_) => return FizzBuzzAnswer::Number(self.to_string()),
};
let zero = match <Num>::try_from(0_u8) {
Ok(zero) => zero,
Err(_) => return FizzBuzzAnswer::One(self.to_string()),
Err(_) => return FizzBuzzAnswer::Number(self.to_string()),
};
match (self % three == zero, self % five == zero) {
(true, true) => FizzBuzzAnswer::One("fizzbuzz".to_string()),
(true, false) => FizzBuzzAnswer::One("fizz".to_string()),
(false, true) => FizzBuzzAnswer::One("buzz".to_string()),
_ => FizzBuzzAnswer::One(self.to_string()),
(true, true) => FizzBuzzAnswer::Fizzbuzz,
(true, false) => FizzBuzzAnswer::Fizz,
(false, true) => FizzBuzzAnswer::Buzz,
_ => FizzBuzzAnswer::Number(self.to_string()),
}
}
}

/// Used to obtain the correct `FizzBuzzAnswer` for a `Vec` of numbers
///
/// ### Required:
/// - fn fizzbuzz(self) -> FizzBuzzAnswer
/// Used to obtain the correct `FizzBuzzAnswer` for a multiple fizzbuzz-able numbers
pub trait MultiFizzBuzz {
fn fizzbuzz(self) -> FizzBuzzAnswer;
/// Returns an iterator which provides the FizzBuzz values for the elements of the implementing type.
///
/// Note:
/// - This function **consumes** the input
/// - The returned iterator is a `rayon::iter::IndexedParallelIterator`
/// - The Items in the returned iterator will be converted to a requested type
/// (e.g. `FizzBuzzAnswer`, `String`, `Cow<str>`)
fn fizzbuzz<Rtn>(self) -> impl IndexedParallelIterator<Item = Rtn>
where
Rtn: From<FizzBuzzAnswer> + Send;
}

/// Implements the MultiFizzBuzz trait for any type which can be easily converted into a
/// `rayon::iter::IndexedParallelIterator` over Items which implement `fizzbuzz::FizzBuzz`
///
/// Note:
/// - The returned iterator is _lazy_ - no calculations are performed until you use it
/// - Collecting this iterator requires that `rayon::iter::ParallelIterator` is in scope
/// - This implementation will decide whether it is worth the overhead of spawning multiple parallel threads
impl<Iterable, Num> MultiFizzBuzz for Iterable
where
Iterable: rayon::iter::IntoParallelIterator<Item = Num>,
<Iterable as IntoParallelIterator>::Iter: IndexedParallelIterator,
Num: FizzBuzz,
{
fn fizzbuzz(self) -> FizzBuzzAnswer {
fn fizzbuzz<Rtn>(self) -> impl IndexedParallelIterator<Item = Rtn>
where
Rtn: From<FizzBuzzAnswer> + Send,
{
let par_iter = self.into_par_iter();
if par_iter.len() < BIG_VECTOR {
FizzBuzzAnswer::Many(
par_iter
.with_min_len(BIG_VECTOR) //Don't parallelise when small
.map(|n| n.fizzbuzz().into())
.collect(),
)
let min_len = if par_iter.len() < BIG_VECTOR {
BIG_VECTOR //Don't parallelise when small
} else {
FizzBuzzAnswer::Many(par_iter.map(|n| n.fizzbuzz().into()).collect())
}
1
};
par_iter.with_min_len(min_len).map(|n| n.fizzbuzz().into())
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn vec_to_string() {
let input = FizzBuzzAnswer::Many(vec![
"1".to_string(),
"2".to_string(),
"fizz".to_string(),
"4".to_string(),
"buzz".to_string(),
]);
let output: String = input.into();
let expected = "1, 2, fizz, 4, buzz".to_string();
assert_eq!(output, expected)
}
use super::*;

#[test]
fn big_vector_is_well_ordered() {
let input: Vec<_> = (1..BIG_VECTOR + 2).collect();
let output: Vec<_> = input.clone().fizzbuzz().into();
let mut expected: Vec<String> = vec![];
let output: Vec<FizzBuzzAnswer> = input.clone().fizzbuzz().collect();
let mut expected: Vec<FizzBuzzAnswer> = vec![];
for i in input.iter() {
expected.push(i.fizzbuzz().into())
expected.push(i.fizzbuzz())
}
assert_eq!(output, expected);
}

#[test]
fn fizzbuzz_range() {
let input = 1..20;
let mut expected: Vec<String> = vec![];
let mut expected: Vec<FizzBuzzAnswer> = vec![];
for i in 1..20 {
expected.push(i.fizzbuzz().into())
}
let output: Vec<String> = input.fizzbuzz().into();
let output: Vec<FizzBuzzAnswer> = input.fizzbuzz().collect();
assert_eq!(output, expected)
}
}
Loading

0 comments on commit 8f85294

Please sign in to comment.