September 28, 2026
One instruction can still be the slow choice
A divide instruction is the obvious way to divide integers. Reusing the denominator lets a longer sequence of cheaper operations run faster in these benchmarks.
The simple, obvious solution can still be suboptimal, even when its
arithmetic compiles to a single instruction. Write n / denominator, see
a divide instruction in the assembly, and it is tempting to conclude that
there is nothing left to optimize.
The processor can express division in one instruction without making it cheap. When the same denominator is reused, computing a reciprocal once can replace each divide with a longer sequence of multiplications, shifts, and additions that takes less time. The opportunity comes from what the program knows about the workload: the denominator stays the same.
On an Intel Xeon E5-4657L v2, the precomputed version processed a small array 2.43 times as fast as ordinary runtime division. Both loops were scalar. The longer arithmetic sequence won without SIMD, after the divisor's setup cost had been paid. I also measured Apple M4 Max and Loongson 3A6000, and separately tested how much reuse it takes to recover that setup cost.
The starting point was this LinkedIn post about C++26 contracts.
The example divides two integers, with a precondition that the denominator is
nonzero. The quoted follow-up reports that ignoring the contract leaves just
the division, while quick_enforce adds a test and a conditional jump. Its
Google Benchmark results were 1.18 versus 1.94 nanoseconds.
That is a useful observation about the cost of checking a precondition. The phrase “Zero overhead” describes what happens to the contract check when it is disabled. The division still has a cost, and there are workloads where we can avoid paying it for every value.
Instruction count is an incomplete measure of CPU work. Instructions have different latencies, use different execution resources, and permit different amounts of overlap. A sequence of multiplies, shifts, and additions can beat a divide instruction. An array loop also gives the compiler a chance to process several values with SIMD instructions.
The compiler can exploit a constant divisor. On the M4 Max, write n / 3,
and Apple Clang emits multiplication and sign correction. For a scalar
signed 32-bit numerator, its strategy is equivalent to:
auto product = std::int64_t(n) * 0x55555556;
auto quotient = (product >> 32) + (n < 0);
Those constants encode an integer reciprocal. The result is exact, including truncation toward zero for negative inputs. There is no floating-point approximation involved.
The interesting case is when the divisor comes from configuration, a file, or a request. The compiler cannot substitute a particular reciprocal ahead of time. But the program may know that this one divisor will be used for the next million values. We can compute the reciprocal once, at runtime, and reuse it.
That gives us an API like this:
auto d = Divide(3);
d(7); // 2
d(-7); // -2
Divide calculates a multiplier, a shift count, and a sign mask in its
constructor. Its call operator uses a wide multiply, an arithmetic shift,
and sign correction. The constructor itself performs a division; that cost
has to be recovered by reusing the object.
I benchmarked three cases with the same numerical divisor, 3: ordinary
n / denominator, where the denominator is known only at runtime; literal
n / 3; and calls through an already constructed Divide object.
The machine was an Apple M4 Max, running macOS 26.5.1, with Apple
Clang 21.0.0. The main build used
-std=c++26 -O3 -DNDEBUG -mcpu=apple-m4 -fno-lto. A second build added
-fno-vectorize -fno-slp-vectorize to show the scalar result.
Each kernel read 4,096 signed 32-bit inputs and wrote 4,096 quotients. The inputs included both signs and the integer limits. The arrays stayed hot in cache. I used 15 samples per case, shuffled their execution order, and calibrated batches to take at least 25 milliseconds before collecting samples.
Here are the medians, in nanoseconds per value; lower is better:
| Operation | Normal -O3 |
Vectorization disabled |
|---|---|---|
n / denominator, runtime divisor |
0.451 | 0.449 |
n / 3, compile-time divisor |
0.062 | 0.293 |
d(n), precomputed runtime divisor |
0.280 | 0.332 |
In the normal build, the class reduced time per value by about 38%. With vectorization disabled, it still reduced time per value by about 26%. The single divide instruction was slower in both comparisons.
The 0.062 ns figure measures throughput across an array. It does not mean one dependent division completes in 0.062 ns. The CPU overlaps work, and the constant-divisor loop processes multiple values together. These results include array loads, stores, and loop overhead. They describe this workload on this machine, rather than instruction latency or an application-wide speedup. They also measure a different question from the original contract benchmark, so its 1.18 and 1.94 ns figures are not directly comparable.
I checked the generated code. Ordinary runtime division uses ARM64 sdiv.
The constant case uses multiply-based SIMD arithmetic. The class also gets a
vectorized loop, although this implementation's wide multiplies are partly
scalarized. Knowing the literal divisor gives the compiler more freedom:
it can choose its own multiplier, use fixed shifts, remove denominator sign
handling, and generate a more efficient vector loop. The scalar column shows
that cheaper arithmetic contributes a gain even without SIMD.
I repeated the unchanged source on a machine with four Intel Xeon
E5-4657L v2 processors. This was still a single-threaded benchmark,
pinned to logical CPU 2. The installed compiler was GCC 11.4, using
-std=c++20 -O3 -DNDEBUG -march=native -fno-lto; the class uses features
already available in C++20. GCC selected its Ivy Bridge target.
The Xeon medians, also in nanoseconds per value, were:
| Operation | Normal -O3 |
Vectorization disabled |
|---|---|---|
n / denominator, runtime divisor |
3.415 | 3.414 |
n / 3, compile-time divisor |
0.353 | 1.042 |
d(n), precomputed runtime divisor |
1.408 | 1.406 |
Here, the class cut time per value by about 59%. Its loop remained scalar
in both builds, while literal division used vector multiplication. The
ordinary runtime loop used x86 idivl. The improvement from the class on
this machine therefore came without vectorizing it. These are measurements
of particular CPU/compiler combinations; the different toolchains matter when
comparing the machines. The Xeon raw data and assembly
include the compiler's vectorization report.
The third machine was a Loongson 3A6000. Its instruction set includes
vector integer division, which makes it an interesting test of whether
replacing division is still useful. I used GCC 15.2 in C++26 mode, with the
benchmark pinned to logical CPU 2. With -march=native, GCC selected la664
and enabled 256-bit LASX instructions.
The system reported a maximum clock of 2.0 GHz and was already using its
performance governor.
The Loongson medians, in nanoseconds per value, were:
| Operation | Normal -O3 |
Vectorization disabled |
|---|---|---|
n / denominator, runtime divisor |
0.906 | 5.817 |
n / 3, compile-time divisor |
0.095 | 5.817 |
d(n), precomputed runtime divisor |
0.345 | 1.392 |
The normal runtime loop really does use xvdiv.w: each instruction divides
eight signed 32-bit lanes. The compiler unrolled that loop to process 32
values per iteration. This is native vector integer division, and it brings
a substantial improvement over the scalar loop. The precomputed class
still used about 62% less time per value than that vector-division loop.
It also vectorized, using multiply, shift, and sign-correction instructions.
GCC exposes the vector division operation in its
LASX intrinsics,
and the assembly from this run
shows it in the actual timed kernel.
The scalar constant result deserves attention. With vectorization disabled,
this GCC build retained div.w even for literal / 3. In the vectorized
constant loop, it used xvmuh.w and sign correction instead. The large gap
between those two results therefore includes a change in the division
algorithm as well as SIMD. A constant divisor gives the compiler an
opportunity to use reciprocal multiplication; checking the generated code
tells us whether it took that opportunity. The
Loongson results preserve both versions.
There are two easy ways to invalidate this comparison. Giving the compiler a visible literal in the supposedly runtime case can turn it into another constant-division test. Failing to use the results can let it remove the work. Here, the runtime divisor is parsed from the command line, and the kernels are compiled in a separate translation unit without link-time optimization. The driver checks their output against ordinary division and consumes a checksum after each timed sample. There are no volatile accesses in the arithmetic loops. The benchmark source, kernels, and raw results are included so the measurement can be inspected. You can also download the complete source and results.
Construction changes the calculation. I also timed batches that construct a
new Divide object once per batch, with construction inside the timer. This
table uses the normal optimized M4 Max build and again reports time per value:
| Values sharing one divisor | Ordinary / |
Construct once, then use Divide |
|---|---|---|
| 1 | 0.710 ns | 1.472 ns |
| 4 | 0.483 ns | 0.543 ns |
| 16 | 0.461 ns | 0.312 ns |
| 64 | 0.459 ns | 0.291 ns |
| 256 | 0.471 ns | 0.287 ns |
| 1,024 | 0.459 ns | 0.282 ns |
| 4,096 | 0.465 ns | 0.281 ns |
At the tested batch sizes, the class loses for one and four values and wins from sixteen onward. That is a crossover for this particular loop, compiler, and divisor. Function-call overhead, vectorization thresholds, and scheduling also affect these small batches. Sixteen is not a universal break-even point. The Xeon showed the same pattern at the tested sizes: construction lost at one and four values, then won at sixteen. At sixteen values it took 2.365 ns/value including setup, compared with 3.365 ns/value for ordinary division. On the Loongson, the first winning tested size was four values: 1.760 ns/value including construction versus 1.814 for ordinary division. At sixteen values the difference grew to 0.594 versus 1.798 ns/value. Its different vector-loop thresholds are another reason to measure the batch sizes the application actually uses.
I would use this when the divisor is discovered at runtime and remains the same across a substantial amount of work: converting offsets to block numbers using a block size read from a file, for example, or applying an integer scale selected for a batch of data. Construction belongs outside the loop:
const Divide divide(denominator_from_config);
for (auto& value : values) {
value = divide(value);
}
If the denominator is already a compile-time constant, ordinary / gives
the compiler the information it needs. If it changes for every value,
repeatedly constructing this class adds setup work with little opportunity
to recover the cost. The measurements here are for divisor 3; other
divisors and CPUs need their own measurements. Memory bandwidth and unrelated
work can also dominate a larger application.
The implementation is short enough to show in full. It uses signed 32-bit
inputs and signed 64-bit intermediates. For the positive magnitude a of the
denominator, construction computes:
k = ceil(log2(a))
s = 31 + k
m = floor(2^s / a) + 1
The reciprocal is deliberately rounded strictly upward, including when a
is a power of two. For a positive numerator, its error is too small to move
the result into the next quotient. For a negative numerator, the arithmetic
shift rounds down, and adding one corrects the result to truncation toward
zero. For Divide(3), this formula produces multiplier 0xAAAAAAAB and
shift 33. The compiler's own / 3 strategy uses a different valid
multiplier; this class uses one uniform formula for all supported divisors.
#pragma once
#include <bit>
#include <cassert>
#include <cstdint>
#include <limits>
#include <stdexcept>
// Reusable signed 32-bit division, rounding toward zero.
// Construction may divide; operator() only multiplies, shifts, and corrects signs.
class Divide {
public:
using value_type = std::int32_t;
// Zero is rejected once, when the object is constructed.
explicit constexpr Divide(value_type denominator)
: sign_(static_cast<std::int8_t>(denominator < 0 ? -1 : 0)) {
if (denominator == 0) {
throw std::invalid_argument("Divide: denominator must not be zero");
}
// Widen before negating so that INT32_MIN is handled correctly.
const auto wide = static_cast<std::int64_t>(denominator);
const auto magnitude = static_cast<std::uint32_t>(wide < 0 ? -wide : wide);
// k = ceil(log2(magnitude)); shift is in [31, 62].
const auto k = std::bit_width(magnitude - 1u);
shift_ = static_cast<std::uint8_t>(31 + k);
const auto scale = std::uint64_t{1} << shift_;
// This reciprocal must be STRICTLY greater than scale / magnitude,
// even for powers of two. See README.md for the error bound.
multiplier_ = static_cast<std::uint32_t>(scale / magnitude + 1);
}
// Precondition, as for built-in int32 division: INT32_MIN / -1 is excluded.
// A debug assertion checks this; NDEBUG removes it from the hot path.
[[nodiscard]] constexpr value_type operator()(value_type numerator) const noexcept {
// shift == 31 means |denominator| == 1.
assert(!(numerator == std::numeric_limits<value_type>::min()
&& sign_ == -1 && shift_ == 31));
const auto product = static_cast<std::int64_t>(numerator) * multiplier_;
auto quotient = product >> shift_; // Arithmetic shift (C++20 and later).
quotient += (numerator < 0); // Correct to truncation toward zero.
// sign_ is 0 or -1: conditionally negate without another multiply.
return static_cast<value_type>((quotient ^ sign_) - sign_);
}
private:
std::uint32_t multiplier_{};
std::uint8_t shift_{};
std::int8_t sign_{};
};
The denominator check happens once during construction. The call operator
retains the usual signed-division precondition: INT32_MIN / -1 cannot be
represented in the return type. Its debug assertion is disabled in the
release benchmarks, while zero-denominator rejection remains enabled.
Both construction and evaluation are constexpr. The code builds in C++26
mode, though the features it uses are available in C++20 and do not require
native contracts support.
The implementation passed 43,143,231 comparisons against built-in division on each of the three machines. The M4 Max and Xeon also passed the same suite with address and undefined-behavior sanitizers. The selected Loongson GCC 15 installation did not provide the sanitizer runtime libraries, so that additional check was unavailable there. The derivation explains the error bound and why the intermediate product fits in 64 bits; the tests exercise signs, powers of two, quotient boundaries, and the integer limits.
Runtime invariant division is an established technique. For a production library with more integer types and specialized SIMD implementations, libdivide is worth examining. This small class shows where the opportunity comes from: the divisor becomes fixed during execution, so work that would otherwise be repeated for every numerator can be moved into construction.
Counting the remaining instructions would have missed that opportunity. Knowing how long the divisor stays the same is what makes it useful.