We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 20 of 21
Reusing buffers instead of allocating per iteration
Count how many input values have each decimal digit length, while keeping heap allocations within the exercise's budget.
pub fn digit_histogram(values: &[u64]) -> [u32; 20]
Slot i counts values with i + 1 digits. Zero has one digit, and
u64::MAX has twenty. Count repeated values separately. An empty input
returns twenty zeros. Inputs have at most u32::MAX elements, so every
counter fits in the output type.
For [0, 5, 42, 999], the first three slots are [2, 1, 1] and the rest
are zero. The fixed-size output array needs no heap allocation of its own.
What the allocation check measures
The large fixture supplies the 200,000 values from 0 through 199_999.
It allows at most four allocator calls during digit_histogram: both
allocation and reallocation calls count, while deallocation calls do not.
Input construction and JSON output formatting are outside that interval.
Other fixtures check histogram values directly. The large fixture also
expects allocs_over_budget to be zero, meaning no calls beyond the four
allowed. This is an allocation check, not a required timing speedup.
The starter creates a new decimal String for each value. Its histogram
logic is correct, but repeated string creation exceeds the budget on the
exercise toolchain. The task is to avoid that repeated allocation while
preserving the output.
Reuse one string
Create a buffer before the loop, then clear and refill it for each value.
Twenty bytes are enough for any decimal u64:
use std::fmt::Write as _;
let mut buf = String::with_capacity(20);
for &v in values {
buf.clear();
write!(buf, "{v}").expect("formatting into a String");
let digits = buf.len();
// Increment the corresponding histogram slot.
}
clear() removes the contents while retaining capacity. Replacing buf
with String::new() would drop the existing buffer instead. Without an
initial capacity reservation, longer values can cause later growth.
Decimal output uses ASCII digits, so its byte length is also its digit
count. That relationship does not hold for arbitrary Unicode text.
write! uses the std::fmt::Write trait here. Writing to a String does
not return a formatting error for these integers; allocation failure is
not a recoverable fmt::Error.
This reuse pattern is also useful for a scratch vector or a line-reading
buffer. Collection clear methods retain capacity, but removing owned
elements may still drop resources inside them. See
capacity and buffer reuse
for more on length, capacity and shrinking.
Count digits without building text
Only the digit count is needed, so integer arithmetic is another valid approach:
let digits = if v == 0 { 1 } else { v.ilog10() as usize + 1 };
ilog10() gives the integer part of the base-ten logarithm. For example,
999.ilog10() is 2, which gives three digits after adding one.
ilog10() panics on zero, so handle zero separately. Its checked variant
returns None for zero.
Both buffer reuse and integer digit counting can meet the budget. Their relative speed depends on the workload, compiler and machine; allocation counts alone do not establish which is faster.
Finish using a slice before changing its buffer
This example fails with E0502 because last still refers to the previous
contents when the next iteration mutates buf:
use std::fmt::Write as _;
let mut buf = String::new();
let mut last: &str = "";
for &v in values {
buf.clear();
write!(buf, "{v}").unwrap();
println!("Previous: {last}");
last = &buf;
}
Finish reading the old slice before clearing the buffer. If the old and new text must coexist, use separate storage, such as two reusable buffers or an owned copy. If only a count or comparison result is needed, retain that value instead of the slice.
Keep temporary owners alive
This also fails, with E0716:
let text = format!("{value}").as_str();
println!("{text}");
The temporary String is dropped at the first statement's end. Bind the
owner before borrowing it:
let rendered = format!("{value}");
let text = rendered.as_str();
println!("{text}");
Temporary lifetime rules depend on syntax. A direct borrowing initializer
such as let text = &format!("{value}")[..]; extends the temporary's lifetime
in this case; the method-call initializer above does not.
Check your understanding When is buffer reuse worth adding? Show answer Hide answer
Use it when profiling identifies repeated allocation as a cost, or when an explicit budget requires it. Reuse introduces mutable state and can retain a large capacity after a one-off large input. Keep the buffer's lifetime and ownership clear, and measure the complete operation when performance is the reason for the change.
Passing requires compilation, correct histograms, the large fixture's allocation budget and Clippy with warnings treated as errors.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.