skip to content

Why does widening the result of a 32-bit multiply to 64 bits happen too late to prevent overflow?

level: middleimportance: must knowfreq 58%

answer

  1. look at the operator, not the assignment
  2. which width is the product formed in
  3. the widening happens after the arithmetic
  4. a small constant multiplier still compounds magnitude
  5. promote one operand before multiplying

basics

~20 s

Operand width, not destination width, decides where arithmetic happens. A product of two 32-bit values is formed in 32 bits and wraps there; widening afterwards faithfully copies the already-wrong result. Widen at least one operand before multiplying.

solid answer

~50 s

Arithmetic is evaluated before assignment, and the width it is evaluated in comes from the operands, not from the variable receiving it. So `millis = seconds * 1000` with a 32-bit `seconds` forms the product in 32 bits: for three million seconds the true product is 3,000,000,000, which exceeds the 32-bit signed ceiling, so it wraps to -1,294,967,296 and *that* is what gets widened into the 64-bit field. The fix is to widen an operand first — promote `seconds` to 64 bits, or make the constant a 64-bit literal — so the multiply itself is performed with room. Multiplication is the dangerous operator here because it compounds magnitude: two operands each comfortably inside the range can produce a product far outside it. The review heuristic is to read the operand types, never the destination type.

code

pseudocode · 9 lines
pseudocode
// seconds : 32-bit signed  (range about -2.1e9 .. 2.1e9)
// millis  : 64-bit signed

seconds = 3000000
millis  = seconds * 1000        // product formed in 32 bits, then widened
// millis  == -1294967296        (3000000000 - 2^32)

millis2 = to_64(seconds) * 1000 // product formed in 64 bits
// millis2 == 3000000000         (correct)

go deeper

for a junior

Recall the ordering: arithmetic runs first, assignment second. If both operands are 32-bit the product is 32-bit, no matter how wide the variable receiving it is. Learn the fix as "widen before you multiply".

for a middle

Explain why multiplication is more dangerous than addition — bit-lengths add, so two in-range operands routinely produce an out-of-range product — and show the correct edit, promoting an operand rather than converting the result.

for a senior

Show how you find these in a codebase: grep for unit-conversion constants and count-times-size shapes, review operand types rather than declarations, and add tests with values above the narrow ceiling so the data-dependent failure is reproducible.

for a principal

Decide the standard: which quantities are 64-bit everywhere by policy, where checked multiplication is mandatory, and whether conversions go through a single reviewed helper so the rule is enforced by construction instead of by reviewer vigilance.

## The rule the trap depends on In a fixed-width numeric model, an expression is evaluated in a width chosen from its **operands**. The variable on the left of the assignment plays no part in that choice — the assignment happens afterwards, on a value that has already been computed. So a multiplication of two 32-bit quantities is a 32-bit multiplication whose result is taken modulo 2^32, and only then converted to whatever wider type is receiving it. Widening after the fact is a faithful copy of a wrong number. That single rule is behind an entire family of production bugs, and unit conversion is its most common costume: seconds to milliseconds, kilobytes to bytes, records to bytes, a fraction to a percentage. In every case a value that is comfortably inside the range is multiplied by a small constant that pushes it outside. ## Walking the timestamp case Take a 32-bit `seconds` value and convert it to a 64-bit millisecond field. Three million seconds is nothing — about 35 days. Multiplied by 1000 the mathematical answer is 3,000,000,000, which is above the 32-bit signed ceiling of 2,147,483,647. The 32-bit multiply therefore yields 3,000,000,000 − 4,294,967,296 = −1,294,967,296, and the 64-bit field receives exactly that. Downstream, a timestamp before the epoch, a negative duration, or a sort that puts recent records first. Nothing anywhere reported an error. Notice how well this hides. The declaration of the destination looks careful — someone deliberately chose 64 bits for the millisecond field. The multiplier is a constant so small it does not read as dangerous. And the bug is data-dependent: values under about 2.1 million seconds convert perfectly, so the conversion works for weeks of test data and fails on the first record with a larger value. ## Why multiplication is the sharp edge Addition grows magnitude by at most one bit per operation, so a loop must run a long time before an accumulator is at risk. Multiplication adds the operands' bit-lengths: a 20-bit value times a 20-bit value needs up to 40 bits. Two inputs that individually use half the available range produce a product that cannot possibly fit. That is why "can these operands overflow?" is the wrong question for a product; the right question is "can the *product* fit in the width where it is formed?" The same reasoning explains why the trap survives review. Reviewers check that inputs are validated and in range. Both operands *are* in range. The defect lives in the operator, not the inputs. ## The three correct fixes 1. **Widen an operand before the multiply.** Promoting `seconds` to 64 bits makes the whole expression 64-bit, which is what the destination width was implying all along. This is the fix that matches the intent. 2. **Make the constant wide.** Multiplying by a 64-bit literal `1000` promotes the other operand for the same reason. Compact, but easy for the next reader to "tidy away", so a comment earns its place. 3. **Use checked multiplication.** Where the correct product genuinely might not fit in any available width, an operation that signals on overflow converts a silent wrong answer into a handled failure. A fourth non-fix deserves naming because it is what people reach for first: converting the *result*, or declaring the destination wider. Both operate on a value that already wrapped. ## Detecting an unsafe multiply before performing it When you cannot widen — you are already at the widest type available — you can pre-check. For non-negative `a` and `b`, the product exceeds the maximum representable value exactly when `a != 0` and `b > MAX / a`, using integer division. This costs one division and is exact. The alternative — multiply, then check whether the result looks wrong (for example, whether dividing back does not return the original operand) — works for wrapping semantics but is meaningless where the model treats overflow as forbidden rather than defined, so the pre-check is the portable habit. That portability point generalises. The same expression may be correct in an arbitrary-precision model, where integers grow to fit and the product is simply large, and wrong in a fixed-width model, where it wraps. Python and Ruby integers grow; Java, Go and Rust integers do not. An algorithm ported between those worlds needs its multiplications re-examined even when the logic is copied verbatim, because the numeric model — not the logic — is what changed. ## The habit to leave with When you read any multiplication in a review, look left of the operator and right of it, never at the assignment target. Then ask for the largest value each operand can hold in production, multiply those, and compare against the ceiling of the operands' width. If the answer does not fit, the expression is already broken — regardless of how wide the thing receiving it is.

  • How do you check that a multiply would overflow before you perform it?
    For non-negative operands, the product exceeds the maximum representable value exactly when `a != 0` and `b > MAX / a`, using integer division — one division, exact, and it never relies on wrapped values. Checking after the fact by dividing the result back works only where wraparound is a defined result; where overflow is treated as forbidden the wrapped value is not something you may reason about.
  • Where else does this trap appear besides unit conversion?
    Anywhere a count meets a size: total bytes as `record_count * record_size`, bandwidth as `bytes * 8`, a scaled percentage as `part * 100 / total`, or pixel counts as `width * height * channels`. All share the shape of two individually reasonable values whose product is not reasonable, and all are usually written with the wide destination already in place.
  • The same expression is correct in one codebase and wrong in another. How?
    Numeric models differ. Where integers are arbitrary-precision they grow to hold the product, so the expression is simply correct and slower. Where they are fixed-width it wraps. Porting an algorithm across that boundary means re-auditing every multiplication and accumulation, because the logic did not change but the arithmetic did.

saying these in an interview costs you the question

  • Thinks the destination's width determines the arithmetic width
  • Says converting the result to 64 bits fixes it
  • Assumes multiplying by a small constant is always safe
  • Believes in-range operands guarantee an in-range product
  • Claims overflow would be reported before the assignment

context