samgeo.codes

Day 1

Day 1! A great place to start.

I previously wrote a post discussing some basic templates for getting started with Advent of Code in Python and JavaScript. I think that's a solid place to start, but I like to use something a little bit different for Python.

In particular, fileinput reads input files line-by-line which can occasionally make it awkward to parse certain inputs. You know what? Let's try day 1 using the template I suggest and then see if we can make some basic improvements.

In case you forgot, the template looks like this:

import fileinput

lines = [line.strip() for line in fileinput.input()]

To store my solutions, I use a directory named 2022. Since I am going to produce solutions using both Python and Rust, I have the subdirectories py, rust and input. To get started, I'm going to save my Python template under 2022/py/day01/sol.py and my input under 2022/input/01/input.txt. I am also going to save the example input under 2022/input/01/example.txt.

So far, my directory structure looks like this:

.
โ””โ”€โ”€ 2022/
    โ”œโ”€โ”€ input/
    โ”‚   โ””โ”€โ”€ 01/
    โ”‚       โ”œโ”€โ”€ input.txt
    โ”‚       โ””โ”€โ”€ example.txt
    โ”œโ”€โ”€ py/
    โ”‚   โ””โ”€โ”€ day01/
    โ”‚       โ””โ”€โ”€ sol.py
    โ””โ”€โ”€ rust

Problem statement

The example input looks like this:

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000

Each contiguous group of lines represents the food carried by a single elf. Each line within each group represents the calories contained within a single food item.

For part one, we want to find the elf carrying the most total calories.

Link to the prompt if you want some festive winteriness.

Let's go!

If we go ahead and print out our lines:

import fileinput

lines = [line.strip() for line in fileinput.input()]
print(lines)

When we run our solution against the example, this is what we get:

$ python py/day01/sol.py input/01/example.txt
['1000', '2000', '3000', '', '4000', '', '5000', '6000', '', '7000', '8000', '9000', '', '10000']

It might be easier to rebuild the input and then re-split to divide our input into one section per elf, then we can process each of those sections. We'll define a helper function to determine the total calories that a single elf is carrying.

Generator Expressions

Many people are familiar with list comprehensions. Generator expressions behave very similarly to list comprehensions but have lazy semantics: they don't compute elements until they are asked for. We will see some great parallels with Rust's iterators later in the post. Unlike iterators in Rust, generator expressions have some runtime overhead because Python is an interpreted language.

In general, I like to use generator expressions instead of list comprehensions when feasible. For most applications, list comprehensions are just fine (and might even be faster), but when working with potentially large collections, it can be prohibitively expensive to materialize the entire collection in-memory.

Some may see this as an unnecessary optimization, but I like to get into habits that lead to reasonably fast code.

We'll use a few Python built-ins for our solution:

  • int: constructs an integer from a number or a string
  • sum: sums the items of an iterable, note that generator expressions produce iterables
  • max: finds the largest item in an iterable; again note that generator expressions produce iterables

With that all in mind, let's see what our solution to part one looks like:

import fileinput


def total_calories(elf):
    return sum(int(item) for item in elf.splitlines())


lines = (line.strip() for line in fileinput.input())
puzzle_input = "\n".join(lines)
sections = puzzle_input.split("\n\n")
solution = max(total_calories(section) for section in sections)
print(f"Part one: {solution}")

Let's run it against the example:

$ python py/day01/sol.py input/01/example.txt
Part one: 24000

That's what we wanted! Let's see if we can run it against our input:

$ python py/day01/sol.py input/01/input.txt
Part one: 70509

We enter that into the website and hooray! One star! Off to a great start.

Part Two

Now we want the total of the top 3 elves!

We can use sorted (which also accepts an iterable, noticing a trend?) to make a sorted list of all of the elves, then take the top 3:

import fileinput


def total_calories(elf):
    return sum(int(item) for item in elf.splitlines())


lines = (line.strip() for line in fileinput.input())
puzzle_input = "\n".join(lines)
sections = puzzle_input.split("\n\n")
solution = max(total_calories(section) for section in sections)
print(f"Part one: {solution}")
top_three = sorted(total_calories(section) for section in sections)[-3:]
solution = sum(top_three)
print(f"Part two: {solution}")

Let's run it against the example:

$ python py/day01/sol.py input/01/example.txt
Part one: 24000
Part two: 45000

Neato! We're on a roll.

Now on our input:

$ python py/day01/sol.py input/01/input.txt
Part one: 70509
Part two: 208567

Improvements

Maybe we can do a little bit better, while also learning a bit more about the (unnecessarily enormous) Python standard library!

First, let's try to pull our solution into function scope. We'll check to see if our solution module should be run by comparing the global value __name__ against the string "__main__". Check out the Modules section of the Python tutorial for more details.

import fileinput


def total_calories(elf):
    return sum(int(item) for item in elf.splitlines())


def main():
    lines = (line.strip() for line in fileinput.input())
    puzzle_input = "\n".join(lines)
    sections = puzzle_input.split("\n\n")
    solution = max(total_calories(section) for section in sections)
    print(f"Part one: {solution}")
    top_three = sorted(total_calories(section) for section in sections)[-3:]
    solution = sum(top_three)
    print(f"Part two: {solution}")


if __name__ == "__main__":
    main()

As mentioned above, using fileinput is pretty awkward since we are basically reconstructing our input from the file contents, anyway. Let's define a helper module to make reading input a little bit nicer. We'll put this module in the file 2022/py/read_input.py:

import argparse
from pathlib import Path


def read_input(day: int) -> str:
    parser = argparse.ArgumentParser()
    parser.add_argument("input", nargs="?", default="input.txt")
    args = parser.parse_args()

    padded_day = str(day).rjust(2, "0")
    filename = Path("input") / padded_day / args.input

    with open(filename) as f:
        return f.read()

I guess there's a few things to mention, here:

  • What's that -> str nonsense?: That's a return value type hint. Check out the typing module documentation for more information.
  • The argparse standard library module allows you to write simple CLI scripts. There are a lot of great argument parsing libraries available on PyPI, but we don't need to pull in any external dependencies for argparse and it suits our needs just fine.
  • The pathlib standard library module makes it easy to build up file paths in a platform-independent way.

Now, to use this new module, we also need to add a blank __init__.py file to our 2022/py directory; and while we're at it, let's add one to our 2022/py/day01 directory, as well:

$ touch 2022/py/__init__.py
$ touch 2022/py/day01/__init__.py

This might all seem like a lot of boilerplate, but we won't really touch any of this after day 1!

Finally, we can use the new module in our solution:

from py.read_input import read_input


def total_calories(elf):
    return sum(int(item) for item in elf.splitlines())


def main():
    puzzle_input = read_input(1)
    sections = puzzle_input.split("\n\n")
    solution = max(total_calories(section) for section in sections)
    print(f"Part one: {solution}")
    top_three = sorted(total_calories(section) for section in sections)[-3:]
    solution = sum(top_three)
    print(f"Part two: {solution}")


if __name__ == "__main__":
    main()

To avoid Python path nonsense we need to change our command line invocation slightly:

$ python -m py.day01.sol
Part one: 70509
Part two: 208567

Still works! And now we don't even need to enter the filename if we just want to run against our input. Nice.

Type Hints

I'm a bit of a type hinting fanatic: type hints help me avoid making careless mistakes and force me to refine the flow of information around my programs.

Let's add them to what we have so far and we can try to keep everything working as we restructure our program.

First, let's set up a venv within our py subdirectory. I'm using the python3.11 binary installed on my machine, so that looks like this:

$ python3.11 -m venv py/.venv
$ source py/.venv/bin/activate

I personally like to use the pyright LSP, so I'll go ahead and install the dev tools that I typically use. If you want to follow along, at least install mypy:

$ pip install pyright flake8 black mypy

Let's run mypy in strict mode:

$ mypy --strict py
py/day01/sol.py:6: error: Function is missing a type annotation  [no-untyped-def]
py/day01/sol.py:10: error: Function is missing a return type annotation  [no-untyped-def]
py/day01/sol.py:10: note: Use "-> None" if function does not return a value
py/day01/sol.py:13: error: Call to untyped function "total_calories" in typed context  [no-untyped-call]
py/day01/sol.py:15: error: Call to untyped function "total_calories" in typed context  [no-untyped-call]
py/day01/sol.py:21: error: Call to untyped function "main" in typed context  [no-untyped-call]
Found 5 errors in 1 file (checked 4 source files)

Paydirt! Let's go ahead and fix all of those. While we are at it, we will also make main return an integer value so we can be a good command line citizen.

from py.read_input import read_input


def total_calories(elf: str) -> int:
    return sum(int(item) for item in elf.splitlines())


def main() -> int:
    puzzle_input = read_input(1)
    sections = puzzle_input.split("\n\n")
    solution = max(total_calories(section) for section in sections)
    print(f"Part one: {solution}")
    top_three = sorted(total_calories(section) for section in sections)[-3:]
    solution = sum(top_three)
    print(f"Part two: {solution}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

If we run mypy again:

$ mypy --strict py
Success: no issues found in 4 source files

Huzzah!

Now, let's see if we can identify some common ground between parts 1 and 2 and maybe improve our runtime performance.

Let's consider the runtime complexity of the solution that we currently have. We'll say that \(I\) is the length of our input and \(N\) is the number of elves in the input.

Our part 1 boils down to a linear scan over our input, so it runs in \(O(I + N)\) time (and \(N\) is bounded by \(I\), so it's really \(O(I)\)). Our part 2 sorts the collection of elves, so it runs in \(O(I + N \log N)\) time.

Can we do better? Well, when we fetch the top 3 elves, we are discarding a bunch of information that we spent time assembling when we sorted the entire collection of elves. Maybe we can avoid doing all that work when we only really care about the top 3 elves. Heaps are a wonderful data structure that work perfectly for this kind of operation (and they're great to be aware of for all sorts of interview problems). In particular, we can heapify a list of \(N\) elements in \(O(N)\) time. And then fetching \(K\) elements from that heap takes \(O(K \log N)\) time. Since we want to fetch a constant number of elements (\(3\)), the cost of fetching becomes \(O(\log N)\).

So if we can leverage a heap to solve the problem, we will have an \(O(I)\) solution!

There is a very handy module in the standard library called heapq. This library maintains the heap invariant (for a min-heap) atop a vanilla Python list. It also has a couple of very handy functions: nlargest and nsmallest for generating lists of the n largest or smallest elements, respectively. And they do indeed first heapify the input collection and then fetch the first n elements.

And if we look at the documentation, it looks like the result is returned in reverse sorted order, so we can also use the result for part 1!

Let's see how we can use that and set up a bit of infrastructure for comparing the new implementation with our original sort-based approach. I've adjusted our original solution slightly to avoid some duplicate work.

from heapq import nlargest
from time import perf_counter
from typing import Callable

from py.read_input import read_input


def total_calories(elf: str) -> int:
    return sum(int(item) for item in elf.splitlines())


def sort_sol(puzzle_input: str) -> tuple[int, int]:
    sections = puzzle_input.split("\n\n")
    top_three = sorted(total_calories(section) for section in sections)[-3:]
    return top_three[-1], sum(top_three)


def heap_sol(puzzle_input: str) -> tuple[int, int]:
    sections = puzzle_input.split("\n\n")
    top_three = nlargest(3, (total_calories(section) for section in sections))
    return top_three[0], sum(top_three)


def main() -> int:
    puzzle_input = read_input(1)
    expected_output = sort_sol(puzzle_input)
    print(f"Part one: {expected_output[0]}")
    print(f"Part two: {expected_output[1]}")

    def benchmark(solution: Callable[[str], tuple[int, int]]) -> float:
        start = perf_counter()
        for _ in range(1000):
            assert solution(puzzle_input) == expected_output
        end = perf_counter()
        return end - start

    print(f"Sort solution takes {benchmark(sort_sol)}s")
    print(f"Heap solution takes {benchmark(heap_sol)}s")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Let's run it and see how much improvement we get!

$ python -m py.day01.sol example.txt
Part one: 24000
Part two: 45000
Sort solution takes 0.0033642500020505395s
Heap solution takes 0.0045546660003310535s

Now you might be saying, "hey Sam, I thought using a heap would make things faster, what gives?"

Well, unfortunately big-O analysis throws away a bunch of constants and it turns out that in the real world, constants matter. sorted is very well optimized since it is one of the most commonly used built-ins in Python.

This was just our example input, let's try our real input:

$ python -m py.day01.sol
Part one: 70509
Part two: 208567
Sort solution takes 0.24173837499984074s
Heap solution takes 0.23308020799959195s

Faster, but not by much.

Our puzzle input still only consists of 2237 elves. What if we crank that up a little bit?

We replace our line puzzle_input = read_input(1) with puzzle_input = "\n\n".join([read_input(1).strip()] * 1000) and reduce the number of benchmark iterations down to 10:

$ python -m py.day01.sol
Part one: 70509
Part two: 211527
Sort solution takes 2.4853763329992944s
Heap solution takes 2.3238277089985786s

Better, but still not that satisfying of an improvement. I guess the two lessons are:

  • constants matter
  • log grows very slowly

Let's put our heap-based solution in place without all of the benchmarking boilerplate:

from heapq import nlargest

from py.read_input import read_input


def total_calories(elf: str) -> int:
    return sum(int(item) for item in elf.splitlines())


def main() -> int:
    puzzle_input = read_input(1)
    sections = puzzle_input.split("\n\n")
    top_three = nlargest(3, (total_calories(section) for section in sections))
    print(f"Part one: {top_three[0]}")
    print(f"Part two: {sum(top_three)}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

So now our directory structure looks like this:

.
โ””โ”€โ”€ 2022/
    โ”œโ”€โ”€ input/
    โ”‚   โ””โ”€โ”€ 01/
    โ”‚       โ”œโ”€โ”€ input.txt
    โ”‚       โ””โ”€โ”€ example.txt
    โ”œโ”€โ”€ py/
    โ”‚   โ”œโ”€โ”€ __init__.py
    โ”‚   โ”œโ”€โ”€ read_input.py
    โ”‚   โ””โ”€โ”€ 01/
    โ”‚       โ”œโ”€โ”€ __init__.py
    โ”‚       โ””โ”€โ”€ sol.py
    โ””โ”€โ”€ rust

That's enough Python for now, let's see what this looks like in Rust.

Rust

In Rust, I like to use a new crate for each day. I also like to use the nightly toolchain so that I can play around with unstable features.

To get started, check out rustup.

For now, here is what my active toolchain looks like:

$ rustup show
active toolchain
----------------

nightly-aarch64-apple-darwin (default)
rustc 1.69.0-nightly (5e37043d6 2023-01-22)

Alright, let's get this show on the road!

$ cargo new rust/day01 --vcs none --lib
$ cd rust/day01

This creates a new binary crate without version control (hopefully you have version control on your solutions). Your working directory should now look something like this:

.
โ””โ”€โ”€ day01/
    โ”œโ”€โ”€ Cargo.toml
    โ””โ”€โ”€ src/
        โ””โ”€โ”€ lib.rs

And you'll have the default lib.rs:

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Let's try a similar comparison in Rust between a sort-based and heap-based solution.

First, let's define a function for the sort-based approach:

use std::error::Error;

pub fn sort_sol(input: &str) -> Result<(i32, i32), Box<dyn Error>> {
    let mut elves = input
        .split("\n\n")
        .map(|elf| {
            elf.trim()
                .lines()
                .map(|line| -> Result<i32, Box<dyn Error>> { line.parse().map_err(From::from) })
                .sum()
        })
        .collect::<Result<Vec<_>, _>>()?;
    elves.sort_by(|a: &i32, b| b.cmp(a));
    Ok((elves[0], elves.iter().take(3).sum()))
}

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

    const EXAMPLE: &str = r#"1000
2000
3000

4000

5000
6000

7000
8000
9000

10000"#;

    #[test]
    fn test_sort_sol() -> Result<(), Box<dyn Error>> {
        assert_eq!(sort_sol(EXAMPLE)?, (24000, 45000));
        Ok(())
    }
}

This somewhat resembles our Python implementation:

  • we split the input into elf sections,
  • we calculate the sum of each section,
  • we sort the resulting collection and determine our results.

For the most part, Rust just makes us be a little bit more explicit about our error conditions. This solution will still panic for empty inputs, for instance. But our python solution would throw for lines that don't contain valid integers.

Let's see if our test passes!

$ cargo test
   Compiling day01 v0.1.0 (/Users/sgeisenh/projects/aoc/2022/rust/day01)
    Finished test [unoptimized + debuginfo] target(s) in 0.45s
     Running unittests src/lib.rs (target/debug/deps/day01-1b30f96f7eb97059)

running 1 test
test tests::test_sort_sol ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests day01

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Excellent.

Let's wire it up into a binary by creating a day01/src/bin/main.rs:

use std::error::Error;

use day01::sort_sol;

fn main() -> Result<(), Box<dyn Error>> {
    let input = include_str!("../../../../input/01/input.txt");
    let (part1, part2) = sort_sol(input)?;
    println!("Sort sol p1: {part1}, p2: {part2}");
    Ok(())
}

Let's run it!

$ cargo run
   Compiling day01 v0.1.0 (/Users/sgeisenh/projects/aoc/2022/rust/day01)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/main`
Sort sol p1: 70509, p2: 208567

Cool beans. Let's try to get the heap implementation in place. We can use the unstable into_iter_sorted function to convert a BinaryHeap directly to an Iterator and then take the first 3 elements for part 2.

Here's what that looks like:

#![feature(binary_heap_into_iter_sorted)]
use std::{collections::BinaryHeap, error::Error};

pub fn sort_sol(input: &str) -> Result<(i32, i32), Box<dyn Error>> {
    let mut elves = input
        .split("\n\n")
        .map(|elf| {
            elf.trim()
                .lines()
                .map(|line| -> Result<i32, Box<dyn Error>> { line.parse().map_err(From::from) })
                .sum()
        })
        .collect::<Result<Vec<_>, _>>()?;
    elves.sort_by(|a: &i32, b| b.cmp(a));
    Ok((elves[0], elves.iter().take(3).sum()))
}

pub fn heap_sol(input: &str) -> Result<(i32, i32), Box<dyn Error>> {
    let elves = input
        .split("\n\n")
        .map(|elf| {
            elf.trim()
                .lines()
                .map(|line| -> Result<i32, Box<dyn Error>> { line.parse().map_err(From::from) })
                .sum()
        })
        .collect::<Result<BinaryHeap<_>, _>>()?;
    Ok((
        *elves.peek().unwrap(),
        elves.into_iter_sorted().take(3).sum(),
    ))
}

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

    const EXAMPLE: &str = r#"1000
2000
3000

4000

5000
6000

7000
8000
9000

10000"#;

    #[test]
    fn test_sort_sol() -> Result<(), Box<dyn Error>> {
        assert_eq!(sort_sol(EXAMPLE)?, (24000, 45000));
        Ok(())
    }

    #[test]
    fn test_heap_sol() -> Result<(), Box<dyn Error>> {
        assert_eq!(heap_sol(EXAMPLE)?, (24000, 45000));
        Ok(())
    }
}

We run the tests...

$ cargo test
   Compiling day01 v0.1.0 (/Users/sgeisenh/projects/aoc/2022/rust/day01)
    Finished test [unoptimized + debuginfo] target(s) in 0.23s
     Running unittests src/lib.rs (target/debug/deps/day01-1b30f96f7eb97059)

running 2 tests
test tests::test_heap_sol ... ok
test tests::test_sort_sol ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/bin/main.rs (target/debug/deps/main-abe929b1b2534503)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests day01

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

wire it up to our binary...

use std::error::Error;

use day01::{heap_sol, sort_sol};

fn main() -> Result<(), Box<dyn Error>> {
    let input = include_str!("../../../../input/01/input.txt");
    let (sp1, sp2) = sort_sol(input)?;
    println!("Sort sol p1: {sp1}, p2: {sp2}");
    let (hp1, hp2) = heap_sol(input)?;
    println!("Heap sol p1: {hp1}, p2: {hp2}");
    Ok(())
}

and run it:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `/Users/sgeisenh/projects/aoc/2022/rust/day01/target/debug/main`
Sort sol p1: 70509, p2: 208567
Heap sol p1: 70509, p2: 208567

Neat.

There's a little bit of common code that I feel like we should be able to pull into it's own function. Let's give it a shot!

#![feature(binary_heap_into_iter_sorted)]
use std::{collections::BinaryHeap, error::Error};

pub type AocResult<'a, T> = Result<T, Box<dyn Error + 'a>>;

fn parse_input(input: &str) -> impl Iterator<Item = AocResult<i32>> {
    input.split("\n\n").map(|elf| {
        elf.trim()
            .lines()
            .map(|line| -> AocResult<i32> { line.parse().map_err(From::from) })
            .sum()
    })
}

pub fn sort_sol(input: &str) -> AocResult<(i32, i32)> {
    let mut elves = parse_input(input).collect::<Result<Vec<_>, _>>()?;
    elves.sort_by(|a: &i32, b| b.cmp(a));
    Ok((elves[0], elves.iter().take(3).sum()))
}

pub fn heap_sol(input: &str) -> AocResult<(i32, i32)> {
    let elves = parse_input(input).collect::<Result<BinaryHeap<_>, _>>()?;
    Ok((
        *elves.peek().unwrap(),
        elves.into_iter_sorted().take(3).sum(),
    ))
}

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

    const EXAMPLE: &str = r#"1000
2000
3000

4000

5000
6000

7000
8000
9000

10000"#;

    #[test]
    fn test_parse_input() -> AocResult<'static, ()> {
        assert_eq!(
            parse_input(EXAMPLE).collect::<AocResult<Vec<_>>>()?,
            vec![6000, 4000, 11000, 24000, 10000]
        );
        Ok(())
    }

    #[test]
    fn test_sort_sol() -> AocResult<'static, ()> {
        assert_eq!(sort_sol(EXAMPLE)?, (24000, 45000));
        Ok(())
    }

    #[test]
    fn test_heap_sol() -> AocResult<'static, ()> {
        assert_eq!(heap_sol(EXAMPLE)?, (24000, 45000));
        Ok(())
    }
}

Now it's time to see how our solutions compare.

For that we'll use Criterion.rs.

First we install criterion:

$ cargo add criterion

Then we add a benchmark to our Cargo.toml:

[dev-dependencies]
criterion = { version = "0.4.0", features = ["html_reports"] }


[[bench]]
name = "my_benchmark"
harness = false

And add our benchmark file benches/my_benchmark.rs:

use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

static INPUT: &str = include_str!("../../../input/01/input.txt");

pub fn criterion_benchmark(c: &mut Criterion) {
    c.bench_function("sort_sol", |b| b.iter(|| day01::sort_sol(black_box(INPUT))));
    c.bench_function("heap_sol", |b| b.iter(|| day01::heap_sol(black_box(INPUT))));
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

And now we run our benchmarks:

$ cargo bench
...
     Running unittests src/lib.rs (target/release/deps/day01-aec6c446d7e6dd94)

running 3 tests
test tests::test_heap_sol ... ignored
test tests::test_parse_input ... ignored
test tests::test_sort_sol ... ignored

test result: ok. 0 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/bin/main.rs (target/release/deps/main-cc52451e5c082913)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running benches/my_benchmark.rs (target/release/deps/my_benchmark-710fed971563135d)
Gnuplot not found, using plotters backend
sort_sol                time:   [35.656 ยตs 35.678 ยตs 35.699 ยตs]
                        change: [-0.1767% -0.0397% +0.1173%] (p = 0.60 > 0.05)
                        No change in performance detected.
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high severe

heap_sol                time:   [32.523 ยตs 32.553 ยตs 32.601 ยตs]
                        change: [-0.3934% -0.2565% -0.1198%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 6 outliers among 100 measurements (6.00%)
  2 (2.00%) high mild
  4 (4.00%) high severe

Nice, our heap solution is slightly faster than our sort solution. Also, 32 microseconds is pretty quick... so quick that it's not even really worth benchmarking our solution with hyperfine. We'll save that for another time.

That's it!

That's all for now. Now that most of the boilerplate is out of the way, I'm hopeful that the next several days will go much quicker! See you next time.