feat: Add performance benchmarks for various modules and expose funda… - #23
Conversation
…mental submodules.
Summary of ChangesHello @tekawade, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the project's performance analysis capabilities by integrating Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request adds a comprehensive set of performance benchmarks for various modules using the criterion framework. It also exposes some submodules in algs4-fundamentals to make them accessible to the benchmarks. The changes are a great addition for monitoring performance.
My review focuses on improving the accuracy and clarity of the new benchmark code. I've identified several places where criterion::black_box should be used to prevent the compiler from optimizing away the code being measured. I also found some unused code and opportunities to make the benchmark setup more efficient. In one case, the structure of a benchmark was measuring setup cost along with the operation, for which I've suggested a fix.
| group.bench_with_input(BenchmarkId::new("kmp_search", size), &text, |b, txt| { | ||
| b.iter(|| { | ||
| let kmp = KMP::new(black_box(pattern)); | ||
| kmp.search(black_box(txt)); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The KMP::new() call is inside the b.iter loop, which means you are benchmarking the DFA construction on every iteration, not just the search. For a more accurate benchmark of the search performance, the KMP instance should be created outside the loop. Additionally, the result of kmp.search should be wrapped in black_box to prevent the compiler from optimizing it away.
group.bench_with_input(BenchmarkId::new("kmp_search", size), &text, |b, txt| {
let kmp = KMP::new(pattern);
b.iter(|| {
black_box(kmp.search(black_box(txt)));
});
});| let mut rng = thread_rng(); | ||
| let mut data: Vec<i32> = (0..*size as i32).collect(); | ||
| data.shuffle(&mut rng); |
| use algs4_fundamentals::union_find::quick_find_uf::QuickFindUF; | ||
| use algs4_fundamentals::union_find::quick_union_uf::QuickUnionUF; | ||
| use algs4_fundamentals::union_find::weighted_quick_union_uf::WeightedQuickUnionUF; | ||
| use algs4_fundamentals::union_find::weighted_quick_union_uf::WeightedQuickUnionUF as UF; // UF trait might not be public or exist as named |
| let mut group = c.benchmark_group("union_find"); | ||
|
|
||
| for size in [1000, 5000].iter() { | ||
| let ops = size * 2; |
| for i in 0..size - 1 { | ||
| uf.union(i, i + 1); | ||
| } | ||
| uf.connected(0, size - 1) |
There was a problem hiding this comment.
| let mut p = pts.clone(); | ||
| GrahamScan::new(black_box(&mut p)); |
There was a problem hiding this comment.
The p vector does not need to be mutable. More importantly, the result of GrahamScan::new is not used and could be optimized away by the compiler. You should wrap the function call in black_box to ensure the benchmark is accurate.
| let mut p = pts.clone(); | |
| GrahamScan::new(black_box(&mut p)); | |
| let p = pts.clone(); | |
| black_box(GrahamScan::new(&p)); |
| b.iter(|| { | ||
| let mut st = BST::new(); | ||
| for &key in &keys { | ||
| st.put(key, key); | ||
| } | ||
| for &key in &keys { | ||
| st.get(&key); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The benchmark may not be accurate because the compiler could optimize away the put and get calls since their results are not used. To prevent this and ensure you're measuring the real performance, you should use criterion::black_box on the inputs to put and the output of get. This advice applies to all other benchmarks in this file as well.
| b.iter(|| { | |
| let mut st = BST::new(); | |
| for &key in &keys { | |
| st.put(key, key); | |
| } | |
| for &key in &keys { | |
| st.get(&key); | |
| } | |
| }); | |
| b.iter(|| { | |
| let mut st = BST::new(); | |
| for &key in &keys { | |
| st.put(black_box(key), black_box(key)); | |
| } | |
| for &key in &keys { | |
| black_box(st.get(&key)); | |
| } | |
| }); |
| group.bench_with_input(BenchmarkId::new("lsd", size), &data, |b, d| { | ||
| b.iter(|| { | ||
| let mut arr = d.clone(); | ||
| lsd::sort(&mut arr, 10); |
There was a problem hiding this comment.
The sort operation might be optimized away by the compiler because the result (the modified arr) is not used. To ensure the benchmark is accurate, you should wrap the mutable slice in black_box, just as you did for the integer sorts. This also applies to the msd and quick3string benchmarks in this file.
| lsd::sort(&mut arr, 10); | |
| lsd::sort(black_box(&mut arr), 10); |
| let text: String = (0..*size) | ||
| .map(|_| { | ||
| thread_rng() | ||
| .sample_iter(&Alphanumeric) | ||
| .take(1) | ||
| .map(char::from) | ||
| .collect::<String>() | ||
| }) | ||
| .collect(); |
There was a problem hiding this comment.
The text generation is inefficient. It creates a new String for each character and then collects them. This can be done more efficiently by collecting directly from the iterator, which avoids intermediate String allocations.
let text: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(*size)
.map(char::from)
.collect();There was a problem hiding this comment.
Pull Request Overview
This PR adds comprehensive performance benchmarks across multiple algorithm modules and exposes fundamental submodules for direct access. The benchmarks enable performance testing of various data structures and algorithms using the Criterion framework.
Key Changes:
- Adds benchmark suites for 6 modules: fundamentals, sorting, searching, strings, geometry, and advanced
- Exposes union_find and collections submodules in the fundamentals module for granular access
- Updates README with comprehensive benchmark documentation and usage examples
Reviewed Changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/fundamentals/benches/fundamentals_benchmarks.rs | Implements benchmarks for collections (bags, queues, stacks) and union-find algorithms |
| modules/fundamentals/Cargo.toml | Adds criterion and rand dev dependencies with benchmark harness configuration |
| modules/fundamentals/src/union_find/mod.rs | Changes module visibility from private to public for direct submodule access |
| modules/fundamentals/src/collections/mod.rs | Changes module visibility from private to public for direct submodule access |
| modules/sorting/benches/sorting_benchmarks.rs | Implements benchmarks for comparison-based and string sorting algorithms |
| modules/sorting/Cargo.toml | Adds criterion and rand dev dependencies with benchmark harness configuration |
| modules/searching/benches/searching_benchmarks.rs | Implements benchmarks for symbol table implementations (BST, hash tables, tries) |
| modules/searching/Cargo.toml | Adds criterion and rand dev dependencies with benchmark harness configuration |
| modules/strings/benches/strings_benchmarks.rs | Implements benchmarks for KMP pattern matching algorithm |
| modules/strings/Cargo.toml | Adds criterion and rand dev dependencies with benchmark harness configuration |
| modules/geometry/benches/geometry_benchmarks.rs | Implements benchmarks for Graham scan convex hull algorithm |
| modules/geometry/Cargo.toml | Adds criterion and rand dev dependencies with benchmark harness configuration |
| modules/advanced/benches/advanced_benchmarks.rs | Implements benchmarks for Fenwick tree operations |
| modules/advanced/Cargo.toml | Adds criterion and rand dev dependencies with benchmark harness configuration |
| README.md | Updates benchmarking section with commands for all modules and description of benchmarks |
| Cargo.toml | Sets minimum Rust version requirement to 1.82.0 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| use algs4_fundamentals::union_find::quick_find_uf::QuickFindUF; | ||
| use algs4_fundamentals::union_find::quick_union_uf::QuickUnionUF; | ||
| use algs4_fundamentals::union_find::weighted_quick_union_uf::WeightedQuickUnionUF; | ||
| use algs4_fundamentals::union_find::weighted_quick_union_uf::WeightedQuickUnionUF as UF; // UF trait might not be public or exist as named |
There was a problem hiding this comment.
The import alias UF is defined but never used in the code. Remove this unused import to clean up the code.
| use algs4_fundamentals::union_find::weighted_quick_union_uf::WeightedQuickUnionUF as UF; // UF trait might not be public or exist as named |
| let mut group = c.benchmark_group("union_find"); | ||
|
|
||
| for size in [1000, 5000].iter() { | ||
| let ops = size * 2; |
There was a problem hiding this comment.
The variable ops is defined but never used. Remove this unused variable.
| let ops = size * 2; |
| let mut rng = thread_rng(); | ||
| let mut data: Vec<i32> = (0..*size as i32).collect(); | ||
| data.shuffle(&mut rng); |
There was a problem hiding this comment.
The variables rng and data are created outside the benchmark closure but never used. These should be removed as they don't contribute to the benchmark and may confuse readers about what is being measured.
| let mut rng = thread_rng(); | |
| let mut data: Vec<i32> = (0..*size as i32).collect(); | |
| data.shuffle(&mut rng); |
| let text: String = (0..*size) | ||
| .map(|_| { | ||
| thread_rng() | ||
| .sample_iter(&Alphanumeric) | ||
| .take(1) | ||
| .map(char::from) | ||
| .collect::<String>() | ||
| }) |
There was a problem hiding this comment.
The random character generation is inefficient. Creating a new RNG for each character and collecting into an intermediate String is unnecessary. Consider using: thread_rng().sample_iter(&Alphanumeric).take(*size).map(char::from).collect::<String>()
| let text: String = (0..*size) | |
| .map(|_| { | |
| thread_rng() | |
| .sample_iter(&Alphanumeric) | |
| .take(1) | |
| .map(char::from) | |
| .collect::<String>() | |
| }) | |
| let text: String = thread_rng() | |
| .sample_iter(&Alphanumeric) | |
| .take(*size) | |
| .map(char::from) |
| let mut keys: Vec<i32> = (0..*size as i32).collect(); | ||
| keys.shuffle(&mut rng); | ||
|
|
||
| group.bench_with_input(BenchmarkId::new("bst_put_get", size), size, |b, &size| { |
There was a problem hiding this comment.
The size parameter is passed to the closure but never used. The keys vector from the outer scope is used instead. Either remove the unused parameter by using |b, _| or restructure to pass &keys as the input.
| BenchmarkId::new("red_black_bst_put_get", size), | ||
| size, | ||
| |b, &size| { |
There was a problem hiding this comment.
The size parameter is passed to the closure but never used. The keys vector from the outer scope is used instead. Either remove the unused parameter by using |b, _| or restructure to pass &keys as the input.
| BenchmarkId::new("linear_probing_hash_st_put_get", size), | ||
| size, | ||
| |b, &size| { |
There was a problem hiding this comment.
The size parameter is passed to the closure but never used. The keys vector from the outer scope is used instead. Either remove the unused parameter by using |b, _| or restructure to pass &keys as the input.
| group.bench_with_input( | ||
| BenchmarkId::new("separate_chaining_hash_st_put_get", size), | ||
| size, | ||
| |b, &size| { |
There was a problem hiding this comment.
The size parameter is passed to the closure but never used. The keys vector from the outer scope is used instead. Either remove the unused parameter by using |b, _| or restructure to pass &keys as the input.
| |b, &size| { | |
| |b, _| { |
| size, | ||
| |b, &_size| { | ||
| b.iter(|| { | ||
| let mut st = TrieST::new(); | ||
| for (i, key) in keys.iter().enumerate() { | ||
| st.put(key, i); | ||
| } | ||
| for key in &keys { |
There was a problem hiding this comment.
[nitpick] The _size parameter is intentionally ignored with underscore prefix, but size is still passed as the input parameter. Since the keys vector from the outer scope is used, consider passing &keys as the input or simply passing size and using it consistently.
| size, | |
| |b, &_size| { | |
| b.iter(|| { | |
| let mut st = TrieST::new(); | |
| for (i, key) in keys.iter().enumerate() { | |
| st.put(key, i); | |
| } | |
| for key in &keys { | |
| &keys, | |
| |b, keys| { | |
| b.iter(|| { | |
| let mut st = TrieST::new(); | |
| for (i, key) in keys.iter().enumerate() { | |
| st.put(key, i); | |
| } | |
| for key in keys { |
…mental submodules.