Course outline

C++ / Advanced / Lesson 18

Performance measurement and API design

Improve a correct program using representative measurements, then design interfaces that expose ownership, errors, complexity, and compatibility honestly.

About 5 min readContent reviewed
On this page

Lesson goals

  • Separate algorithmic complexity, allocation, locality, and timing evidence.
  • Parse a bounded integer without hidden ownership or partial acceptance.
  • Review an API for lifetime, error, and performance contracts.

Measure a representative workload

Performance is a property of a workload on a particular build and machine. Begin with a correct reference behavior and a measurable goal such as reducing median processing time for a stated input distribution. Profile to find expensive work before changing interfaces. Big-O describes scaling, while allocations, cache locality, branch behavior, and system calls affect constants. A microbenchmark can be optimized away if its result is unused, and a debug build can distort comparisons. Record compiler flags, data size, repetitions, and environment, and report variability rather than inventing a universal speed claim.

Reduce unnecessary work while preserving meaning

Contiguous vectors often offer good traversal locality. Reserving a known final count can avoid repeated growth reallocations, but it does not create elements or promise an exact capacity. The first example knows it will append four squares, reserves once, then visits all actual elements. It prints a correctness result, not a benchmark time. For a real optimization, compare equivalent behavior with and without the change using representative sizes. Reserving an enormous speculative capacity can waste memory, increase failure risk, or cost more than occasional growth.

cpp A correctness check before timing allocation changes
#include <iostream>
#include <vector>
int main() {
    std::vector<int> squares;
    squares.reserve(4);
    for (int value{1}; value <= 4; ++value) { squares.push_back(value * value); }
    int total{};
    for (int value : squares) { total += value; }
    std::cout << squares.size() << " " << total << "\n";
}
Expected output
4 30

The four squares are one, four, nine, and sixteen, totaling thirty. No machine-dependent timing or exact capacity is claimed.

Make acceptance and failure explicit

The parsing example takes string_view to borrow input for the duration of the call and returns optional<int> by value. from_chars performs integer parsing without locale-dependent stream formatting or a separate owned string. Its result contains an error code and the first unconsumed pointer. Both must be checked to reject overflow and trailing text, then the application range is checked separately. The function rejects an empty view before pointer arithmetic and never retains the view. Integer from_chars is available from C++17; this C++20 course avoids assuming newer result wrappers or floating implementation support.

cpp A bounded, whole-input parsing contract
#include <charconv>
#include <iostream>
#include <optional>
#include <string_view>
#include <system_error>
std::optional<int> parse_count(std::string_view text) {
    if (text.empty()) { return std::nullopt; }
    int value{};
    const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
    if (result.ec != std::errc{} || result.ptr != text.data() + text.size() ||
        value < 0 || value > 1000) { return std::nullopt; }
    return value;
}
int main() {
    const auto valid = parse_count("42");
    const auto invalid = parse_count("42x");
    std::cout << valid.value() << " " << std::boolalpha << invalid.has_value() << "\n";
}
Expected output
42 false

42 is accepted. 42x has a valid numeric prefix but leaves unconsumed text, so the complete input is rejected.

A fast interface still needs a lifetime contract

Borrowing with string_view or span can avoid copies but introduces a source lifetime dependency. Returning a view into a local buffer is invalid regardless of benchmark results. Taking a value may be better when the operation must retain the data. Document whether results are owned, which inputs may be invalidated, whether callbacks are stored, and what complexity callers can rely on. Avoid exposing internal vector storage just to save a tiny copy if it prevents future changes or creates fragile borrowers. API design balances cost with a contract that remains reviewable.

Integrate a small reporting service

For a capstone, read synthetic item counts, validate each whole input, store owned records in a vector, compute a report with standard algorithms, and expose a pure report function. Keep console interaction at the boundary. Choose exceptions for exceptional construction failures and explicit results for expected invalid records. Test empty input, malformed text, limits, repeated records, and deterministic ordering. Measure only after the same tests pass for the candidate optimization. If concurrency is introduced, define ownership and synchronization before parallelizing; more threads can increase overhead for small workloads. Publish measured assumptions with the interface rather than promising unverified throughput.

Common errors and fixes

A faster benchmark produces different results.

Why it happens
The optimization changed the observable contract.
How to fix it
Restore correctness and compare equivalent workloads.
Prevent it next time
Require regression tests before accepting timing improvements.

A parser accepts 42x as a complete count.

Why it happens
Only the conversion error code was inspected.
How to fix it
Require the returned pointer to equal the input end.
Prevent it next time
Test valid prefixes followed by unwanted text.

A returned string_view becomes dangling.

Why it happens
Its backing string was local or later reallocated.
How to fix it
Return an owned string or document a stable caller-owned source.
Prevent it next time
Include ownership in performance-oriented interface review.

Check your understanding

1. Classify parse_count inputs "0", "1000", "1001", " 42", and "42x". State which rule rejects each invalid input.

Show worked solution

Zero and one thousand are accepted. One thousand one fails the application range. A leading space is not skipped by integer from_chars and fails conversion. 42x leaves an unconsumed suffix and fails the whole-input check. This policy is intentionally strict; if whitespace is permitted, trim it explicitly and document that separate transformation.

2. A benchmark shows span input is faster than copying a vector, but the API queues work after returning. Recommend an ownership design.

Show worked solution

Queued work needs owned data or an explicit owner whose lifetime covers execution. Accept a vector by value and move it into the task, or retain a justified shared immutable owner. A span alone cannot provide that lifetime. Measure the complete queue workload, including ownership transfer, rather than optimizing only a synchronous call that does not match actual behavior.

Key takeaways

  • Measure equivalent behavior under stated conditions.
  • Keep parsing, ownership, and error policies explicit.
  • Correctness and lifetime contracts constrain every optimization.

Official references