From 233c29d61c16cec752173a5034bc26e4774b947f Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Tue, 16 Jan 2024 00:58:00 -0500 Subject: [PATCH 01/11] Add `from-factors` approach --- .../.approaches/from-factors/content.md | 101 ++++++++++++++++++ .../.approaches/from-factors/snippet.txt | 8 ++ 2 files changed, 109 insertions(+) create mode 100644 exercises/practice/sum-of-multiples/.approaches/from-factors/content.md create mode 100644 exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md new file mode 100644 index 000000000..2450f69b6 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md @@ -0,0 +1,101 @@ +# Calculating sum from factors + +```rust +use std::collections::BTreeSet; + +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + factors + .iter() + .filter(|&&factor| factor != 0) + .flat_map(|&factor| multiples(limit, factor)) + .collect::>() + .into_iter() + .sum() +} + +fn multiples(limit: u32, factor: u32) -> impl Iterator { + (factor..) + .step_by(factor as usize) + .take_while(move |&n| n < limit) +} +``` + +This approach implements the exact steps outlined in the exercise description: + +1. For each non-zero factor, find all multiples of that factor that are less than the `limit` +2. Gather the multiples into a [`BTreeSet`][btreeset] (which automatically removes duplicates) +3. Calculate the sum of all unique multiples + +In order to compute the list of multiples for a factor, we create a [`RangeFrom`][rangefrom] +starting with the factor, then use [`step_by`][iterator-step_by] with the same factor. +Because the range has no upper bound, we then use [`take_while`][iterator-take_while] to +stop the iteration when we reach the `limit`. + +To combine the multiples of all factors, we iterate the list of factors and use [`flat_map`][iterator-flat_map] +on each factor's multiples. `flat_map` is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; +it maps each factor into its multiples, then flattens them all in a single sequence. + +To gather the multiples into a [`BTreeSet`][btreeset] which removes duplicates automatically, +we use [`collect`][iterator-collect]. This powerful method can gather the values in an iterator +into many different type of sequences; because of this, the compiler cannot infer the exact output +type we're seeking. To help the compiler, we specify the desired output type using the so-called +[turbofish] syntax: `::<>`. (Note that although we need to specify that the output needs to be +a `BTreeSet`, we do not need to specify the set's _type_ - this can be inferred by the compiler. +To let the compiler infer the set's type, we use the placeholder type specification: `_`.) + +Finally, calculating the sum of the remaining unique multiples in the set is easy: we can simply call [`sum`][iterator-sum]. + +## Nightly Rust and `partition_dedup` + +Collecting the multiples in a [`BTreeSet`][btreeset] works, but might be expensive if we insert many multiples in it. +If we're ready to use a Nightly Rust compiler, we can take advantage of a method that is not yet stabilized: [`partition_dedup`][slice-partition_dedup]. +This method removes consecutive duplicates from a slice. If the slice was sorted to begin with, then all duplicates are removed. +Furthermore, it works in-place by moving duplicates to the end of the slice, thus not allocating new memory. + +If we use [`partition_dedup`][slice-partition_dedup], our code becomes: + +```rust +#![feature(slice_partition_dedup)] + +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + let mut multiples: Vec<_> = factors + .iter() + .filter(|&&factor| factor != 0) + .flat_map(|&factor| multiples(limit, factor)) + .collect(); + + multiples.sort(); + let (unique_multiples, _) = multiples.partition_dedup(); + unique_multiples.iter().sum() +} +``` + +Here, instead of using a [`BTreeSet`][btreeset], we simply collect the multiples into a [`Vec`][vec]. This is +more efficient since `Vec` will allocate much less memory. Then, in order to have all duplicates removed +by [`partition_dedup`], we first [`sort`][slice-sort] the multiples. (For an explanation of why we use [`sort`][slice-sort] +and not [`sort_unstable`][slice-sort_unstable], see the documentation of [`sort`].) + +Running the tests for this variant requires the use of a Nightly Rust toolchain. To install up: + +```sh +rustup toolchain install nightly +``` + +Then, run the tests using: + +```sh +cargo +nightly test +``` + +[btreeset]: https://doc.rust-lang.org/std/collections/struct.BTreeSet.html +[rangefrom]: https://doc.rust-lang.org/std/ops/struct.RangeFrom.html +[iterator-step_by]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.step_by +[iterator-take_while]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.take_while +[iterator-map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map +[iterator-flatten]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flatten +[iterator-collect]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect +[turbofish]: https://matematikaadit.github.io/posts/rust-turbofish.html +[iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum +[slice-partition_dedup]: https://doc.rust-lang.org/std/primitive.slice.html#method.partition_dedup +[vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html +[slice-sort]: https://doc.rust-lang.org/std/primitive.slice.html?search=vec#method.sort diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt b/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt new file mode 100644 index 000000000..6a18bf84b --- /dev/null +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt @@ -0,0 +1,8 @@ +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + factors.iter() + .filter(|&&factor| factor > 0) + .flat_map(|&factor| multiples(limit, factor)) + .collect::>() + .into_iter() + .sum() +} \ No newline at end of file From 84fb84e91799799cb64fe705f4bc590b7a163f83 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Tue, 16 Jan 2024 09:04:03 -0500 Subject: [PATCH 02/11] Fixes for `from-factors` approach --- .../sum-of-multiples/.approaches/from-factors/content.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md index 2450f69b6..f9288bca6 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md @@ -72,10 +72,10 @@ pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { Here, instead of using a [`BTreeSet`][btreeset], we simply collect the multiples into a [`Vec`][vec]. This is more efficient since `Vec` will allocate much less memory. Then, in order to have all duplicates removed -by [`partition_dedup`], we first [`sort`][slice-sort] the multiples. (For an explanation of why we use [`sort`][slice-sort] -and not [`sort_unstable`][slice-sort_unstable], see the documentation of [`sort`].) +by [`partition_dedup`][slice-partition_dedup], we first [`sort`][slice-sort] the multiples. (For an explanation +of why we use [`sort`][slice-sort] and not [`sort_unstable`][slice-sort_unstable], see the documentation of [`sort`][slice-sort].) -Running the tests for this variant requires the use of a Nightly Rust toolchain. To install up: +Running the tests for this variant requires the use of a Nightly Rust toolchain. To install one: ```sh rustup toolchain install nightly @@ -91,6 +91,7 @@ cargo +nightly test [rangefrom]: https://doc.rust-lang.org/std/ops/struct.RangeFrom.html [iterator-step_by]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.step_by [iterator-take_while]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.take_while +[iterator-flat_map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map [iterator-map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map [iterator-flatten]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flatten [iterator-collect]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect @@ -99,3 +100,4 @@ cargo +nightly test [slice-partition_dedup]: https://doc.rust-lang.org/std/primitive.slice.html#method.partition_dedup [vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html [slice-sort]: https://doc.rust-lang.org/std/primitive.slice.html?search=vec#method.sort +[slice-sort_unstable]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort_unstable From 4d1c863c2ec46b5ec649e992fbaeb82c21636442 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Tue, 16 Jan 2024 09:47:36 -0500 Subject: [PATCH 03/11] Add `from-range` approach --- .../.approaches/from-range/content.md | 33 +++++++++++++++++++ .../.approaches/from-range/snippet.txt | 5 +++ 2 files changed, 38 insertions(+) create mode 100644 exercises/practice/sum-of-multiples/.approaches/from-range/content.md create mode 100644 exercises/practice/sum-of-multiples/.approaches/from-range/snippet.txt diff --git a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md new file mode 100644 index 000000000..1d672f09c --- /dev/null +++ b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md @@ -0,0 +1,33 @@ +# Calculating sum by iterating the whole range + +```rust +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + (1..limit) + .filter(|&n| factors.iter().any(|&factor| factor != 0 && n % factor == 0)) + .sum() +} +``` + +Instead of implementing the steps in the exercise description, this approach uses another angle: + +1. Iterate all numbers between 1 (inclusive) and `limit` (exclusive) +2. Keep only numbers which have at least one factor in `factors` +3. Calculate the sum of all numbers kept + +After creating our range, we use [`filter`][iterator-filter] to keep only matching multiples. +To detect the multiples, we scan the `factors` and use [`any`][iterator-any] to check if at least +one is a factor of the number we're checking. ([`any`][iterator-any] is short-circuiting: it stops +as soon as it finds one compatible factor.) + +Finally, once we have the multiples, we can compute the sum easily using [`sum`][iterator-sum]. + +Although this approach requires scanning the entire range of possible numbers that are multiples, +it has a few advantages over the technique proposed in the exercise description: + +- It is concise and simple to implement and understand +- It has stable complexity, with a worst case of `O(limit * factors.len())` +- It does not require allocation of any additional memory + +[iterator-filter]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter +[iterator-any]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.any +[iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum diff --git a/exercises/practice/sum-of-multiples/.approaches/from-range/snippet.txt b/exercises/practice/sum-of-multiples/.approaches/from-range/snippet.txt new file mode 100644 index 000000000..239db6b23 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.approaches/from-range/snippet.txt @@ -0,0 +1,5 @@ +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + (1..limit) + .filter(|&n| factors.iter().any(|&factor| factor != 0 && n % factor == 0)) + .sum() +} \ No newline at end of file From 34d1bf689f18336d4b63350354529cfc6bb3a044 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Tue, 16 Jan 2024 21:59:57 -0500 Subject: [PATCH 04/11] Complete approaches for `sum-of-multiples` --- .../sum-of-multiples/.approaches/config.json | 27 ++++++++ .../.approaches/from-range/content.md | 2 +- .../.approaches/introduction.md | 65 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 exercises/practice/sum-of-multiples/.approaches/config.json create mode 100644 exercises/practice/sum-of-multiples/.approaches/introduction.md diff --git a/exercises/practice/sum-of-multiples/.approaches/config.json b/exercises/practice/sum-of-multiples/.approaches/config.json new file mode 100644 index 000000000..4812f03b7 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.approaches/config.json @@ -0,0 +1,27 @@ +{ + "introduction": { + "authors": [ + "clechasseur" + ] + }, + "approaches": [ + { + "uuid": "c0e599bf-a0b4-4eb3-af73-ab6c5b04dec8", + "slug": "from-factors", + "title": "Calculating sum from factors", + "blurb": "Calculate the sum by scanning the factors and computing their multiples.", + "authors": [ + "clechasseur" + ] + }, + { + "uuid": "305246ad-c36a-48ae-8047-cd00a4e7a3e4", + "slug": "from-range", + "title": "Calculating sum by iterating the whole range", + "blurb": "Calculate the sum by scanning the whole range and identifying any multiple via factors.", + "authors": [ + "clechasseur" + ] + } + ] + } diff --git a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md index 1d672f09c..b1184f4fd 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md @@ -11,7 +11,7 @@ pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { Instead of implementing the steps in the exercise description, this approach uses another angle: 1. Iterate all numbers between 1 (inclusive) and `limit` (exclusive) -2. Keep only numbers which have at least one factor in `factors` +2. Keep only numbers which have at least one factor in `factors` (automatically avoiding any duplicates) 3. Calculate the sum of all numbers kept After creating our range, we use [`filter`][iterator-filter] to keep only matching multiples. diff --git a/exercises/practice/sum-of-multiples/.approaches/introduction.md b/exercises/practice/sum-of-multiples/.approaches/introduction.md new file mode 100644 index 000000000..130340d18 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.approaches/introduction.md @@ -0,0 +1,65 @@ +# Introduction + +There are a couple of different approaches available to solve Sum of Multiples. +One is to follow the algorithm [outlined in the exercise description][approach-from-factors], +but there are other ways, including [scanning the entire range][approach-from-range]. + +## General guidance + +The key to solving Sum of Multiples is to find the unique multiples of all provided factors. +To determine if `f` is a factor of a given number `n`, we can use the [remainder operator][rem]. +It is also possible to find the multiples by simple addition, starting from the factor. + +## Approach: Calculating sum from factors + +```rust +use std::collections::BTreeSet; + +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + factors + .iter() + .filter(|&&factor| factor != 0) + .flat_map(|&factor| multiples(limit, factor)) + .collect::>() + .into_iter() + .sum() +} + +fn multiples(limit: u32, factor: u32) -> impl Iterator { + (factor..) + .step_by(factor as usize) + .take_while(move |&n| n < limit) +} +``` + +For more information, check the [Sum from factors approach][approach-from-factors]. + +## Approach: Calculating sum by iterating the whole range + +```rust +pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { + (1..limit) + .filter(|&n| factors.iter().any(|&factor| factor != 0 && n % factor == 0)) + .sum() +} +``` + +For more information, check the [Sum by iterating the whole range approach][approach-from-range]. + +## Which approach to use? + +- Computing the sum from factors can be efficient if we have a small number of factors and/or if they are large + compared to the limit, because this will result in a small number of multiples to deduplicate. However, as the + number of factors increases or if they are small, the number of multiples grows quickly and this approach + can result in a lot of work to deduplicate them. This approach's complexity is also difficult to pinpoint because + it is dependent on both the number and the size of the factors, making its performance vary wildly. +- Computing the sum by iterating the whole range is less efficient for large ranges when the number of factors is + small and/or when they are large. However, this approach has the advantage of having stable complexity that is + only dependent on the limit and the number of factors. It also avoids any additional memory allocation. + +For more information, check the [Performance article][article-performance]. + +[approach-from-factors]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-factors +[approach-from-range]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-range +[rem]: https://doc.rust-lang.org/core/ops/trait.Rem.html +[article-performance]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/articles/performance From 3860b6d7fd1b01a2098139da7c5ea2df6459c397 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Tue, 16 Jan 2024 23:16:05 -0500 Subject: [PATCH 05/11] Add performance article for `sum-of-multiples` --- .../sum-of-multiples/.articles/config.json | 13 +++++ .../code/sum_of_multiples_bench.rs | 50 +++++++++++++++++ .../.articles/performance/content.md | 54 +++++++++++++++++++ .../.articles/performance/snippet.md | 13 +++++ 4 files changed, 130 insertions(+) create mode 100644 exercises/practice/sum-of-multiples/.articles/config.json create mode 100644 exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs create mode 100644 exercises/practice/sum-of-multiples/.articles/performance/content.md create mode 100644 exercises/practice/sum-of-multiples/.articles/performance/snippet.md diff --git a/exercises/practice/sum-of-multiples/.articles/config.json b/exercises/practice/sum-of-multiples/.articles/config.json new file mode 100644 index 000000000..406e44ea2 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.articles/config.json @@ -0,0 +1,13 @@ +{ + "articles": [ + { + "uuid": "5b2277e9-d224-4db2-b967-f7c1617ecb50", + "slug": "performance", + "title": "Performance deep dive", + "blurb": "Deep dive to find out the most performant approach for computing the sum of multiples.", + "authors": [ + "clechasseur" + ] + } + ] +} diff --git a/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs b/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs new file mode 100644 index 000000000..6e2ad4fe5 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs @@ -0,0 +1,50 @@ +use std::collections::BTreeSet; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 { + factors + .iter() + .filter(|&&factor| factor != 0) + .flat_map(|&factor| multiples(limit, factor)) + .collect::>() + .into_iter() + .sum() +} + +fn multiples(limit: u32, factor: u32) -> impl Iterator { + (factor..) + .step_by(factor as usize) + .take_while(move |&n| n < limit) +} + +pub fn sum_of_multiples_from_range(limit: u32, factors: &[u32]) -> u32 { + (1..limit) + .filter(|&n| factors.iter().any(|&f| f != 0 && n % f == 0)) + .sum() +} + +fn from_factors_benchmark(c: &mut Criterion) { + c.bench_function("from_factors: much_larger_factors", |b| { + b.iter(|| sum_of_multiples_from_factors(black_box(10000), black_box(&[43, 47]))) + }); + c.bench_function( + "from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3", + |b| { + b.iter(|| sum_of_multiples_from_factors(black_box(10000), black_box(&[2, 3, 5, 7, 11]))) + }, + ); +} + +fn from_range_benchmark(c: &mut Criterion) { + c.bench_function("from_range: much_larger_factors", |b| { + b.iter(|| sum_of_multiples_from_range(black_box(10000), black_box(&[43, 47]))) + }); + c.bench_function( + "from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3", + |b| b.iter(|| sum_of_multiples_from_range(black_box(10000), black_box(&[2, 3, 5, 7, 11]))), + ); +} + +criterion_group!(benches, from_factors_benchmark, from_range_benchmark); +criterion_main!(benches); diff --git a/exercises/practice/sum-of-multiples/.articles/performance/content.md b/exercises/practice/sum-of-multiples/.articles/performance/content.md new file mode 100644 index 000000000..306e0fcfd --- /dev/null +++ b/exercises/practice/sum-of-multiples/.articles/performance/content.md @@ -0,0 +1,54 @@ +# Performance + +This article compares the different [approaches] that can be used to solve the Sum of Multiples exercise in Rust: + +1. [Calculating sum from factors][approach-from-factors] +2. [Calculating sum by iterating the whole range][approach-from-range] + +## Benchmarks + +To benchmark the approaches, we wrote a [benchmark] using [`criterion`][crate-criterion]. +The benchmark compares the two approaches by running two of the tests in the exercise's test suite. +Both tests use a large limit (10,000) but differ in the number and size of factors: + +``` +much_larger_factors: + limit = 10_000 + factors = [43, 47] + +solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3: + limit = 10_000 + factors = [2, 3, 5, 7, 11] +``` + +Results from running the benchmark (the average time is the middle one): + +``` +from_factors: much_larger_factors + time: [7.7217 µs 7.7348 µs 7.7479 µs] + +from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 + time: [214.45 µs 215.77 µs 217.17 µs] + +from_range: much_larger_factors + time: [48.398 µs 48.600 µs 48.846 µs] + +from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 + time: [62.479 µs 62.788 µs 63.129 µs] +``` + +As mentioned on the [approaches] page, computing the sum from the factors is very efficient with +a low number of factors. However, as the number of factors increase (especially when they are small), +performance degrades quickly. + +Computing the sum by iterating the whole range, however, is more stable, but is much less efficient +for the "low number of factors" scenario. + +Which approach is the best? Although it depends on the test conditions, it seems that the [second approach][approach-from-range] +offers better performance guarantees when the input cannot be controlled in advance. + +[approaches]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches +[approach-from-factors]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-factors +[approach-from-range]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-range +[benchmark]: https://github.com/exercism/rust/blob/main/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs +[crate-criterion]: https://crates.io/crates/criterion diff --git a/exercises/practice/sum-of-multiples/.articles/performance/snippet.md b/exercises/practice/sum-of-multiples/.articles/performance/snippet.md new file mode 100644 index 000000000..741fbb5bc --- /dev/null +++ b/exercises/practice/sum-of-multiples/.articles/performance/snippet.md @@ -0,0 +1,13 @@ +``` +from_factors: much_larger_factors + time: [7.7217 µs 7.7348 µs 7.7479 µs] + +from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 + time: [214.45 µs 215.77 µs 217.17 µs] + +from_range: much_larger_factors + time: [48.398 µs 48.600 µs 48.846 µs] + +from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 + time: [62.479 µs 62.788 µs 63.129 µs] +``` \ No newline at end of file From 9abaf2556570a1b24a2f07f6e23147d5da832f51 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Tue, 16 Jan 2024 23:20:56 -0500 Subject: [PATCH 06/11] Reduce snippet size --- .../practice/sum-of-multiples/.articles/performance/snippet.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/exercises/practice/sum-of-multiples/.articles/performance/snippet.md b/exercises/practice/sum-of-multiples/.articles/performance/snippet.md index 741fbb5bc..2a5d2f7e9 100644 --- a/exercises/practice/sum-of-multiples/.articles/performance/snippet.md +++ b/exercises/practice/sum-of-multiples/.articles/performance/snippet.md @@ -1,13 +1,10 @@ ``` from_factors: much_larger_factors time: [7.7217 µs 7.7348 µs 7.7479 µs] - from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 time: [214.45 µs 215.77 µs 217.17 µs] - from_range: much_larger_factors time: [48.398 µs 48.600 µs 48.846 µs] - from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 time: [62.479 µs 62.788 µs 63.129 µs] ``` \ No newline at end of file From 7e3a8bb1513b424c0f0073bd664ef8ab55f6777d Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Fri, 19 Jan 2024 19:33:44 -0500 Subject: [PATCH 07/11] Changes after review --- .../.approaches/from-factors/content.md | 98 ++++--------------- .../.approaches/from-factors/snippet.txt | 16 +-- .../.approaches/from-range/content.md | 7 +- .../.approaches/introduction.md | 34 +++---- 4 files changed, 42 insertions(+), 113 deletions(-) diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md index f9288bca6..431d0476e 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md @@ -1,103 +1,43 @@ # Calculating sum from factors ```rust -use std::collections::BTreeSet; - -pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { - factors +pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 { + let mut multiples: Vec<_> = factors .iter() .filter(|&&factor| factor != 0) - .flat_map(|&factor| multiples(limit, factor)) - .collect::>() - .into_iter() - .sum() -} - -fn multiples(limit: u32, factor: u32) -> impl Iterator { - (factor..) - .step_by(factor as usize) - .take_while(move |&n| n < limit) + .flat_map(|&factor| (factor..limit).step_by(factor as usize)) + .collect(); + multiples.sort(); + multiples.dedup(); + multiples.iter().sum() } ``` This approach implements the exact steps outlined in the exercise description: 1. For each non-zero factor, find all multiples of that factor that are less than the `limit` -2. Gather the multiples into a [`BTreeSet`][btreeset] (which automatically removes duplicates) +2. Collect all multiples in a [`Vec`][vec] +3. Remove duplicate multiples 3. Calculate the sum of all unique multiples -In order to compute the list of multiples for a factor, we create a [`RangeFrom`][rangefrom] -starting with the factor, then use [`step_by`][iterator-step_by] with the same factor. -Because the range has no upper bound, we then use [`take_while`][iterator-take_while] to -stop the iteration when we reach the `limit`. +In order to compute the list of multiples for a factor, we create a [`Range`][range] from the factor (inclusive) to the `limit` (exclusive), then use [`step_by`][iterator-step_by] with the same factor. -To combine the multiples of all factors, we iterate the list of factors and use [`flat_map`][iterator-flat_map] -on each factor's multiples. `flat_map` is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; -it maps each factor into its multiples, then flattens them all in a single sequence. +To combine the multiples of all factors, we iterate the list of factors and use [`flat_map`][iterator-flat_map] on each factor's multiples. +[`flat_map`][iteratpr-flat_map] is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; it maps each factor into its multiples, then flattens them all in a single sequence. -To gather the multiples into a [`BTreeSet`][btreeset] which removes duplicates automatically, -we use [`collect`][iterator-collect]. This powerful method can gather the values in an iterator -into many different type of sequences; because of this, the compiler cannot infer the exact output -type we're seeking. To help the compiler, we specify the desired output type using the so-called -[turbofish] syntax: `::<>`. (Note that although we need to specify that the output needs to be -a `BTreeSet`, we do not need to specify the set's _type_ - this can be inferred by the compiler. -To let the compiler infer the set's type, we use the placeholder type specification: `_`.) +Since we need to have unique multiples to compute the proper sum, we [`collect`][iterator-collect] the multiples into a [`Vec`][vec], which allows us to then [`sort`][slice-sort] them and use [`dedup`][vec-dedup] to remove the duplicates. +[`collect`][iterator-collect] is a powerful function that can collect the data in a sequence and store it in any kind of collection - however, because of this, the compiler is not able to infer the type of collection you want as the output. +To solve this problem, we type the `multiples` variable explicitly. Finally, calculating the sum of the remaining unique multiples in the set is easy: we can simply call [`sum`][iterator-sum]. -## Nightly Rust and `partition_dedup` - -Collecting the multiples in a [`BTreeSet`][btreeset] works, but might be expensive if we insert many multiples in it. -If we're ready to use a Nightly Rust compiler, we can take advantage of a method that is not yet stabilized: [`partition_dedup`][slice-partition_dedup]. -This method removes consecutive duplicates from a slice. If the slice was sorted to begin with, then all duplicates are removed. -Furthermore, it works in-place by moving duplicates to the end of the slice, thus not allocating new memory. - -If we use [`partition_dedup`][slice-partition_dedup], our code becomes: - -```rust -#![feature(slice_partition_dedup)] - -pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { - let mut multiples: Vec<_> = factors - .iter() - .filter(|&&factor| factor != 0) - .flat_map(|&factor| multiples(limit, factor)) - .collect(); - - multiples.sort(); - let (unique_multiples, _) = multiples.partition_dedup(); - unique_multiples.iter().sum() -} -``` - -Here, instead of using a [`BTreeSet`][btreeset], we simply collect the multiples into a [`Vec`][vec]. This is -more efficient since `Vec` will allocate much less memory. Then, in order to have all duplicates removed -by [`partition_dedup`][slice-partition_dedup], we first [`sort`][slice-sort] the multiples. (For an explanation -of why we use [`sort`][slice-sort] and not [`sort_unstable`][slice-sort_unstable], see the documentation of [`sort`][slice-sort].) - -Running the tests for this variant requires the use of a Nightly Rust toolchain. To install one: - -```sh -rustup toolchain install nightly -``` - -Then, run the tests using: - -```sh -cargo +nightly test -``` - -[btreeset]: https://doc.rust-lang.org/std/collections/struct.BTreeSet.html -[rangefrom]: https://doc.rust-lang.org/std/ops/struct.RangeFrom.html +[vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html +[range]: https://doc.rust-lang.org/std/ops/struct.Range.html [iterator-step_by]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.step_by -[iterator-take_while]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.take_while [iterator-flat_map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map [iterator-map]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map [iterator-flatten]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flatten [iterator-collect]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect -[turbofish]: https://matematikaadit.github.io/posts/rust-turbofish.html +[slice-sort]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort +[vec-dedup]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.dedup [iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum -[slice-partition_dedup]: https://doc.rust-lang.org/std/primitive.slice.html#method.partition_dedup -[vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html -[slice-sort]: https://doc.rust-lang.org/std/primitive.slice.html?search=vec#method.sort -[slice-sort_unstable]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort_unstable diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt b/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt index 6a18bf84b..511d22574 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/snippet.txt @@ -1,8 +1,8 @@ -pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { - factors.iter() - .filter(|&&factor| factor > 0) - .flat_map(|&factor| multiples(limit, factor)) - .collect::>() - .into_iter() - .sum() -} \ No newline at end of file +pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 { + let mut multiples: Vec<_> = factors.iter() + .filter(|&&factor| factor != 0) + .flat_map(|&factor| (factor..limit).step_by(factor as usize)) + .collect(); + multiples.sort(); + multiples.dedup(); + multiples.iter().sum() diff --git a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md index b1184f4fd..3d7910b92 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md @@ -15,9 +15,8 @@ Instead of implementing the steps in the exercise description, this approach use 3. Calculate the sum of all numbers kept After creating our range, we use [`filter`][iterator-filter] to keep only matching multiples. -To detect the multiples, we scan the `factors` and use [`any`][iterator-any] to check if at least -one is a factor of the number we're checking. ([`any`][iterator-any] is short-circuiting: it stops -as soon as it finds one compatible factor.) +To detect the multiples, we scan the `factors` and use [`any`][iterator-any] to check if at least one is a factor of the number we're checking. +([`any`][iterator-any] is short-circuiting: it stops as soon as it finds one compatible factor.) Finally, once we have the multiples, we can compute the sum easily using [`sum`][iterator-sum]. @@ -25,7 +24,7 @@ Although this approach requires scanning the entire range of possible numbers th it has a few advantages over the technique proposed in the exercise description: - It is concise and simple to implement and understand -- It has stable complexity, with a worst case of `O(limit * factors.len())` +- It has stable complexity, with a worst case of `O(limit * factors.len())`, because there is no need to deduplicate the multiples - It does not require allocation of any additional memory [iterator-filter]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter diff --git a/exercises/practice/sum-of-multiples/.approaches/introduction.md b/exercises/practice/sum-of-multiples/.approaches/introduction.md index 130340d18..ac62e8749 100644 --- a/exercises/practice/sum-of-multiples/.approaches/introduction.md +++ b/exercises/practice/sum-of-multiples/.approaches/introduction.md @@ -13,22 +13,15 @@ It is also possible to find the multiples by simple addition, starting from the ## Approach: Calculating sum from factors ```rust -use std::collections::BTreeSet; - -pub fn sum_of_multiples(limit: u32, factors: &[u32]) -> u32 { - factors +pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 { + let mut multiples: Vec<_> = factors .iter() .filter(|&&factor| factor != 0) - .flat_map(|&factor| multiples(limit, factor)) - .collect::>() - .into_iter() - .sum() -} - -fn multiples(limit: u32, factor: u32) -> impl Iterator { - (factor..) - .step_by(factor as usize) - .take_while(move |&n| n < limit) + .flat_map(|&factor| (factor..limit).step_by(factor as usize)) + .collect(); + multiples.sort(); + multiples.dedup(); + multiples.iter().sum() } ``` @@ -48,14 +41,11 @@ For more information, check the [Sum by iterating the whole range approach][appr ## Which approach to use? -- Computing the sum from factors can be efficient if we have a small number of factors and/or if they are large - compared to the limit, because this will result in a small number of multiples to deduplicate. However, as the - number of factors increases or if they are small, the number of multiples grows quickly and this approach - can result in a lot of work to deduplicate them. This approach's complexity is also difficult to pinpoint because - it is dependent on both the number and the size of the factors, making its performance vary wildly. -- Computing the sum by iterating the whole range is less efficient for large ranges when the number of factors is - small and/or when they are large. However, this approach has the advantage of having stable complexity that is - only dependent on the limit and the number of factors. It also avoids any additional memory allocation. +- Computing the sum from factors can be efficient if we have a small number of factors and/or if they are large compared to the limit, because this will result in a small number of multiples to deduplicate. + However, as the number of multiples grows, this approach can result in a lot of work to deduplicate them. +- Computing the sum by iterating the whole range is less efficient for large ranges when the number of factors is small and/or when they are large. + However, this approach has the advantage of having stable complexity that is only dependent on the limit and the number of factors, since there is no deduplication involved. + It also avoids any additional memory allocation. For more information, check the [Performance article][article-performance]. From 3b37e99bb9f5780af45f7824a8b7a7df21f7b133 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Fri, 19 Jan 2024 19:44:32 -0500 Subject: [PATCH 08/11] Add note about performance of `sort` vs `sort_unstable` --- .../.approaches/from-factors/content.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md index 431d0476e..a2edf0bf0 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md @@ -25,12 +25,24 @@ In order to compute the list of multiples for a factor, we create a [`Range`][ra To combine the multiples of all factors, we iterate the list of factors and use [`flat_map`][iterator-flat_map] on each factor's multiples. [`flat_map`][iteratpr-flat_map] is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; it maps each factor into its multiples, then flattens them all in a single sequence. -Since we need to have unique multiples to compute the proper sum, we [`collect`][iterator-collect] the multiples into a [`Vec`][vec], which allows us to then [`sort`][slice-sort] them and use [`dedup`][vec-dedup] to remove the duplicates. +Since we need to have unique multiples to compute the proper sum, we [`collect`][iterator-collect] the multiples into a [`Vec`][vec], which allows us to then [`sort`][slice-sort][^1] them and use [`dedup`][vec-dedup] to remove the duplicates. [`collect`][iterator-collect] is a powerful function that can collect the data in a sequence and store it in any kind of collection - however, because of this, the compiler is not able to infer the type of collection you want as the output. To solve this problem, we type the `multiples` variable explicitly. Finally, calculating the sum of the remaining unique multiples in the set is easy: we can simply call [`sum`][iterator-sum]. +[^1]: There is another method available to sort a slice: [`sort_unstable`][slice-sort_unstable]. Usually, using [`sort_unstable`][slice-sort_unstable] is recommended if we do not need to keep the ordering of duplicate elements (which is our case). However, [`sort`][slice-sort] has the advantage because of its implementation. From the documentation: + + ``` + Current implementation + + The current algorithm is an adaptive, iterative merge sort inspired by timsort. It is designed to be very fast in cases where the slice is nearly sorted, or consists of two or more sorted sequences concatenated one after another. + ``` + + The last part is key, because this is exactly our use case: we concatenate sequences of _sorted_ multiples. + + Running a benchmark using the two methods shows that in our scenario, [`sort`][slice-sort] is about twice as fast as [`sort_unstable`][slice-sort_unstable]. + [vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html [range]: https://doc.rust-lang.org/std/ops/struct.Range.html [iterator-step_by]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.step_by @@ -41,3 +53,4 @@ Finally, calculating the sum of the remaining unique multiples in the set is eas [slice-sort]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort [vec-dedup]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.dedup [iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum +[slice-sort_unstable]: https://doc.rust-lang.org/std/primitive.slice.html#method.sort_unstable From 437cfb07515d22e529bbcd503e8536bcb4206a41 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Fri, 19 Jan 2024 19:53:34 -0500 Subject: [PATCH 09/11] Remove performance article, rework following review comments --- .../.approaches/from-range/content.md | 7 --- .../.approaches/introduction.md | 4 +- .../sum-of-multiples/.articles/config.json | 13 ----- .../code/sum_of_multiples_bench.rs | 50 ----------------- .../.articles/performance/content.md | 54 ------------------- .../.articles/performance/snippet.md | 10 ---- 6 files changed, 3 insertions(+), 135 deletions(-) delete mode 100644 exercises/practice/sum-of-multiples/.articles/config.json delete mode 100644 exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs delete mode 100644 exercises/practice/sum-of-multiples/.articles/performance/content.md delete mode 100644 exercises/practice/sum-of-multiples/.articles/performance/snippet.md diff --git a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md index 3d7910b92..24afd0a40 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-range/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-range/content.md @@ -20,13 +20,6 @@ To detect the multiples, we scan the `factors` and use [`any`][iterator-any] to Finally, once we have the multiples, we can compute the sum easily using [`sum`][iterator-sum]. -Although this approach requires scanning the entire range of possible numbers that are multiples, -it has a few advantages over the technique proposed in the exercise description: - -- It is concise and simple to implement and understand -- It has stable complexity, with a worst case of `O(limit * factors.len())`, because there is no need to deduplicate the multiples -- It does not require allocation of any additional memory - [iterator-filter]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter [iterator-any]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.any [iterator-sum]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum diff --git a/exercises/practice/sum-of-multiples/.approaches/introduction.md b/exercises/practice/sum-of-multiples/.approaches/introduction.md index ac62e8749..0bdaa90c8 100644 --- a/exercises/practice/sum-of-multiples/.approaches/introduction.md +++ b/exercises/practice/sum-of-multiples/.approaches/introduction.md @@ -47,9 +47,11 @@ For more information, check the [Sum by iterating the whole range approach][appr However, this approach has the advantage of having stable complexity that is only dependent on the limit and the number of factors, since there is no deduplication involved. It also avoids any additional memory allocation. +Without proper benchmarks, the second approach may be preferred since it offers a more stable level of complexity (e.g. its performances varies less when the size of the input changes). +However, if you have some knowledge of the size and shape of the input, then benchmarking might reveal that one approach is better than the other for your specific use case. + For more information, check the [Performance article][article-performance]. [approach-from-factors]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-factors [approach-from-range]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-range [rem]: https://doc.rust-lang.org/core/ops/trait.Rem.html -[article-performance]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/articles/performance diff --git a/exercises/practice/sum-of-multiples/.articles/config.json b/exercises/practice/sum-of-multiples/.articles/config.json deleted file mode 100644 index 406e44ea2..000000000 --- a/exercises/practice/sum-of-multiples/.articles/config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "articles": [ - { - "uuid": "5b2277e9-d224-4db2-b967-f7c1617ecb50", - "slug": "performance", - "title": "Performance deep dive", - "blurb": "Deep dive to find out the most performant approach for computing the sum of multiples.", - "authors": [ - "clechasseur" - ] - } - ] -} diff --git a/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs b/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs deleted file mode 100644 index 6e2ad4fe5..000000000 --- a/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::collections::BTreeSet; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; - -pub fn sum_of_multiples_from_factors(limit: u32, factors: &[u32]) -> u32 { - factors - .iter() - .filter(|&&factor| factor != 0) - .flat_map(|&factor| multiples(limit, factor)) - .collect::>() - .into_iter() - .sum() -} - -fn multiples(limit: u32, factor: u32) -> impl Iterator { - (factor..) - .step_by(factor as usize) - .take_while(move |&n| n < limit) -} - -pub fn sum_of_multiples_from_range(limit: u32, factors: &[u32]) -> u32 { - (1..limit) - .filter(|&n| factors.iter().any(|&f| f != 0 && n % f == 0)) - .sum() -} - -fn from_factors_benchmark(c: &mut Criterion) { - c.bench_function("from_factors: much_larger_factors", |b| { - b.iter(|| sum_of_multiples_from_factors(black_box(10000), black_box(&[43, 47]))) - }); - c.bench_function( - "from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3", - |b| { - b.iter(|| sum_of_multiples_from_factors(black_box(10000), black_box(&[2, 3, 5, 7, 11]))) - }, - ); -} - -fn from_range_benchmark(c: &mut Criterion) { - c.bench_function("from_range: much_larger_factors", |b| { - b.iter(|| sum_of_multiples_from_range(black_box(10000), black_box(&[43, 47]))) - }); - c.bench_function( - "from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3", - |b| b.iter(|| sum_of_multiples_from_range(black_box(10000), black_box(&[2, 3, 5, 7, 11]))), - ); -} - -criterion_group!(benches, from_factors_benchmark, from_range_benchmark); -criterion_main!(benches); diff --git a/exercises/practice/sum-of-multiples/.articles/performance/content.md b/exercises/practice/sum-of-multiples/.articles/performance/content.md deleted file mode 100644 index 306e0fcfd..000000000 --- a/exercises/practice/sum-of-multiples/.articles/performance/content.md +++ /dev/null @@ -1,54 +0,0 @@ -# Performance - -This article compares the different [approaches] that can be used to solve the Sum of Multiples exercise in Rust: - -1. [Calculating sum from factors][approach-from-factors] -2. [Calculating sum by iterating the whole range][approach-from-range] - -## Benchmarks - -To benchmark the approaches, we wrote a [benchmark] using [`criterion`][crate-criterion]. -The benchmark compares the two approaches by running two of the tests in the exercise's test suite. -Both tests use a large limit (10,000) but differ in the number and size of factors: - -``` -much_larger_factors: - limit = 10_000 - factors = [43, 47] - -solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3: - limit = 10_000 - factors = [2, 3, 5, 7, 11] -``` - -Results from running the benchmark (the average time is the middle one): - -``` -from_factors: much_larger_factors - time: [7.7217 µs 7.7348 µs 7.7479 µs] - -from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 - time: [214.45 µs 215.77 µs 217.17 µs] - -from_range: much_larger_factors - time: [48.398 µs 48.600 µs 48.846 µs] - -from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 - time: [62.479 µs 62.788 µs 63.129 µs] -``` - -As mentioned on the [approaches] page, computing the sum from the factors is very efficient with -a low number of factors. However, as the number of factors increase (especially when they are small), -performance degrades quickly. - -Computing the sum by iterating the whole range, however, is more stable, but is much less efficient -for the "low number of factors" scenario. - -Which approach is the best? Although it depends on the test conditions, it seems that the [second approach][approach-from-range] -offers better performance guarantees when the input cannot be controlled in advance. - -[approaches]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches -[approach-from-factors]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-factors -[approach-from-range]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-range -[benchmark]: https://github.com/exercism/rust/blob/main/exercises/practice/sum-of-multiples/.articles/performance/code/sum_of_multiples_bench.rs -[crate-criterion]: https://crates.io/crates/criterion diff --git a/exercises/practice/sum-of-multiples/.articles/performance/snippet.md b/exercises/practice/sum-of-multiples/.articles/performance/snippet.md deleted file mode 100644 index 2a5d2f7e9..000000000 --- a/exercises/practice/sum-of-multiples/.articles/performance/snippet.md +++ /dev/null @@ -1,10 +0,0 @@ -``` -from_factors: much_larger_factors - time: [7.7217 µs 7.7348 µs 7.7479 µs] -from_factors: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 - time: [214.45 µs 215.77 µs 217.17 µs] -from_range: much_larger_factors - time: [48.398 µs 48.600 µs 48.846 µs] -from_range: solutions_using_include_exclude_must_extend_to_cardinality_greater_than_3 - time: [62.479 µs 62.788 µs 63.129 µs] -``` \ No newline at end of file From c5a6a7db37fde640cf08cd356c3e9575f26428d0 Mon Sep 17 00:00:00 2001 From: Charles Lechasseur Date: Fri, 19 Jan 2024 19:55:31 -0500 Subject: [PATCH 10/11] Oops, fix formatting and remove a bad link --- .../sum-of-multiples/.approaches/from-factors/content.md | 8 +++----- .../practice/sum-of-multiples/.approaches/introduction.md | 2 -- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md index a2edf0bf0..2d2d49500 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md @@ -33,11 +33,9 @@ Finally, calculating the sum of the remaining unique multiples in the set is eas [^1]: There is another method available to sort a slice: [`sort_unstable`][slice-sort_unstable]. Usually, using [`sort_unstable`][slice-sort_unstable] is recommended if we do not need to keep the ordering of duplicate elements (which is our case). However, [`sort`][slice-sort] has the advantage because of its implementation. From the documentation: - ``` - Current implementation - - The current algorithm is an adaptive, iterative merge sort inspired by timsort. It is designed to be very fast in cases where the slice is nearly sorted, or consists of two or more sorted sequences concatenated one after another. - ``` + > Current implementation + > + > The current algorithm is an adaptive, iterative merge sort inspired by timsort. It is designed to be very fast in cases where the slice is nearly sorted, or consists of two or more sorted sequences concatenated one after another. The last part is key, because this is exactly our use case: we concatenate sequences of _sorted_ multiples. diff --git a/exercises/practice/sum-of-multiples/.approaches/introduction.md b/exercises/practice/sum-of-multiples/.approaches/introduction.md index 0bdaa90c8..a942e2dd4 100644 --- a/exercises/practice/sum-of-multiples/.approaches/introduction.md +++ b/exercises/practice/sum-of-multiples/.approaches/introduction.md @@ -50,8 +50,6 @@ For more information, check the [Sum by iterating the whole range approach][appr Without proper benchmarks, the second approach may be preferred since it offers a more stable level of complexity (e.g. its performances varies less when the size of the input changes). However, if you have some knowledge of the size and shape of the input, then benchmarking might reveal that one approach is better than the other for your specific use case. -For more information, check the [Performance article][article-performance]. - [approach-from-factors]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-factors [approach-from-range]: https://exercism.org/tracks/rust/exercises/sum-of-multiples/approaches/from-range [rem]: https://doc.rust-lang.org/core/ops/trait.Rem.html From a36c4fa38c67fc7e45a0887e21542591b91e19c7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 21 Jan 2024 11:04:49 +0100 Subject: [PATCH 11/11] fix typo --- .../sum-of-multiples/.approaches/from-factors/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md index 2d2d49500..af71f4433 100644 --- a/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md +++ b/exercises/practice/sum-of-multiples/.approaches/from-factors/content.md @@ -23,7 +23,7 @@ This approach implements the exact steps outlined in the exercise description: In order to compute the list of multiples for a factor, we create a [`Range`][range] from the factor (inclusive) to the `limit` (exclusive), then use [`step_by`][iterator-step_by] with the same factor. To combine the multiples of all factors, we iterate the list of factors and use [`flat_map`][iterator-flat_map] on each factor's multiples. -[`flat_map`][iteratpr-flat_map] is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; it maps each factor into its multiples, then flattens them all in a single sequence. +[`flat_map`][iterator-flat_map] is a combination of [`map`][iterator-map] and [`flatten`][iterator-flatten]; it maps each factor into its multiples, then flattens them all in a single sequence. Since we need to have unique multiples to compute the proper sum, we [`collect`][iterator-collect] the multiples into a [`Vec`][vec], which allows us to then [`sort`][slice-sort][^1] them and use [`dedup`][vec-dedup] to remove the duplicates. [`collect`][iterator-collect] is a powerful function that can collect the data in a sequence and store it in any kind of collection - however, because of this, the compiler is not able to infer the type of collection you want as the output.