DEV Community

Finley Zhou
Finley Zhou

Posted on

A Free Model Made the Sum More Accurate. memcmp Caught the Regression.

The library's moving-average calculation had an exact binary output. The free model changed it to a more accurate summation. All tests passed. The output still differed.

This is a case study of a small C++ signal-processing library, one floating-point optimization, and an exact-comparison gate that caught what tolerance-based tests missed. The workflow used MonkeyCode's free model access to generate the patch and its free server option to run the verification.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Background

The library computes a moving average over an incoming stream of samples. Consumers use it for a real-time dashboard, and they complained about latency. The core function was straightforward:

// moving_average.cpp (v1)
double moving_average(const std::vector<double>& samples) {
    double sum = 0.0;
    for (double s : samples) {
        sum += s;
    }
    return sum / static_cast<double>(samples.size());
}
Enter fullscreen mode Exit fullscreen mode

Simple. Correct. Good enough. But per-sample latency counted toward the dashboard's end-to-end time. I asked the free model endpoint to optimize it.

The optimization

The model proposed Kahan summation:

// moving_average.cpp (v2)
double moving_average(const std::vector<double>& samples) {
    double sum = 0.0;
    double c = 0.0;
    for (double s : samples) {
        double y = s - c;
        double t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
    return sum / static_cast<double>(samples.size());
}
Enter fullscreen mode Exit fullscreen mode

Kahan summation reduces floating-point error by tracking a compensation term c. The compensation captures the low bits lost in each addition. On later iterations, those bits are added back. For most inputs, the result is closer to the true mathematical sum.

The unit tests passed because they used tolerances:

TEST(MovingAverage, Basic) {
    std::vector<double> samples = {1.0, 2.0, 3.0, 4.0};
    EXPECT_NEAR(moving_average(samples), 2.5, 1e-9);
}
Enter fullscreen mode Exit fullscreen mode

EXPECT_NEAR allows a delta. The old code produced 2.5. The new code produced 2.5. The test passed. I merged the patch.

The gate

Passing unit tests was not enough. The library also had a golden-file test: a precomputed binary file containing the exact output for a known input set. The comparator used memcmp, not a tolerance.

#!/usr/bin/env bash
# golden_gate.sh
set -euo pipefail

./moving_average_runner < input.bin > output.bin
if ! cmp -s output.bin golden.bin; then
    echo "FAIL: output differs from golden"
    exit 1
fi
echo "PASS: output matches golden"
Enter fullscreen mode Exit fullscreen mode

The golden file was generated like this:

  1. Build the runner with the v1 implementation.
  2. Generate the input set with a fixed seed.
  3. Run the runner and capture the output to golden.bin.
  4. Commit golden.bin to the repository.

The verification loop:

  1. Apply the model's patch.
  2. Build the runner.
  3. Run the golden-file gate.
  4. If it fails, inspect the diff.
  5. Decide: accept, reject, or make it optional.

The failure

The golden-file test failed. The first byte differed.

The reason: the input set contained 1,024 samples ranging from 1e-12 to 1e12. For most samples, both implementations produced identical results within double-precision epsilon. But the input contained one specific pair: 1e12 followed by 1e-3. The naive sum added 1e-3 to 1e12, and the result stayed 1e12, because the ulp of 1e12 is roughly 1.2e-4. Kahan summation preserved the 1e-3 through its compensation term. The final average differed by about 1e-15 relative. Small enough that any tolerance test would pass. Large enough that memcmp failed.

This was not a bug. It was a contract change. Consumers of the library relied on the exact output, including its error characteristics. Changing the output, even for the better, breaks downstream comparisons. The dashboard might show different numbers. Historical data might no longer line up with current data.

The decision

Three options were on the table:

  1. Accept the new output and regenerate the golden file.
  2. Reject the patch and keep the old behavior.
  3. Make the optimization optional.

I chose option 3. The optimization became opt-in through a compile flag:

#ifdef USE_KAHAN_SUM
    double sum = 0.0;
    double c = 0.0;
    for (double s : samples) {
        double y = s - c;
        double t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
#else
    double sum = 0.0;
    for (double s : samples) {
        sum += s;
    }
#endif
Enter fullscreen mode Exit fullscreen mode

Consumers who need the extra precision can enable it. The default behavior is unchanged. The golden-file test passes.

Why not option 1? Regenerating the golden file hides the regression. Future changes could alter the output unintentionally, and the gate would catch it. Option 1 effectively disables the gate.

Why not option 2? Kahan summation is a legitimate improvement for consumers who need precision. Rejecting it blocks them from using it.

Option 3 preserves both the gate and the optimization. Consumers choose.

Results

Version Summation Unit tests Golden file Decision
v1 naive pass pass baseline
v2 Kahan pass fail rejected
v3 optional pass pass merged

Lessons

  • Tolerance tests hide numerical regressions. They allow output changes by design. EXPECT_NEAR is the right tool for unit tests, but it does not protect a contract.
  • An exact-comparison gate catches what tolerances miss. For numerical code, you need both.
  • The free model produced technically superior code. Superior is not always correct. The optimization changed the output, regardless of direction.
  • An output change is a contract change. Downstream consumers may depend on the old behavior, including its error characteristics.
  • Making the optimization optional lets consumers choose without the library maintainer deciding what "better" means. It is a compromise, but it keeps the gate intact.

Limitations

The golden-file test is strict. It rejects any output change, including harmless ones. For fast iteration, this can be a source of friction. You need a mechanism to review and intentionally update the golden file, with the reason recorded.

Who should not use this workflow: projects without a fixed output contract, or projects whose output is naturally nondeterministic. If the output differs on every run, a golden-file test is useless.

The artifact

The script above is the entire gate. It fits in a gist and takes five minutes to wire into your existing workflow.

Next time a free model proposes a numerical optimization, ask before merging: does the output change? If it does, who is relying on the old output?

Top comments (0)