4
\$\begingroup\$

For example, to add two 16-bit numbers on my 8 bit machine, I add the low bytes together, then the high bytes together, and then add the carry flag to the high byte of the output.

This strategy falls apart when dealing with twos complement signed numbers.

How do I handle adding two 16-bit signed numbers, when my ALU can only process 8 bits?

\$\endgroup\$
4
  • \$\begingroup\$ Arithmetic certainly includes + and - but may very well include * and /, as well. Are you talking about only addition? Or more than that? A note to be aware of is that the use of the borrow/carry varies from processor to processor (there are two quite different methods I've seen used.) So a general answer would need to address these. You might also refer here for some details to study. It may help some. You do need to account for arithmetic overflow in signed cases, as well. \$\endgroup\$
    – jonk
    Commented Aug 13, 2022 at 20:56
  • 1
    \$\begingroup\$ The good old Z80 had an "ADC" or "Add with Carry" instruction allowing the second and third steps in a single instruction. (It also had limited 16 bit arithmetic on the BC, DE, HL register pairs but that would probably be cheating) \$\endgroup\$
    – user16324
    Commented Aug 13, 2022 at 21:15
  • \$\begingroup\$ You write a library where pairs of 8-bit numbers are paired together and you write functions that babysit the operations of them to transfer carry bits from one to the other. Not unlike how you program something like a trig function or exponential function. You just babysit and micromanage everything. \$\endgroup\$
    – DKNguyen
    Commented Aug 14, 2022 at 2:47
  • 1
    \$\begingroup\$ Hacker's Delight by H. Warren is a good book that talks about low-level things like how to do arithmetic. And it's delightful to read. \$\endgroup\$ Commented Aug 14, 2022 at 19:16

3 Answers 3

16
\$\begingroup\$

The same method works perfectly well with two's complement numbers. Add lower bytes (or clear carry and then add with carry), then add higher bytes with carry.

You have to check for overflows.

If the signs of the two numbers are different then no overflow is possible.

If the signs of the numbers are the same and the sign of the result is the same then no overflow occurred. If the signs of the numbers are the same and the result sign has flipped, then an overflow has occurred.

Eg. 0x07FFF (32767 decimal, the largest possible positive number) + 0x0001 = 0x80000 (-32768 decimal, so sign has changed, overflow).

0xFFFF (-1 decimal) + 0xFFFF = 0xFFFE (-2 decimal)

\$\endgroup\$
3
  • 3
    \$\begingroup\$ Just to be pedantic, the OP doesn't necessarily have to check for overflow, e.g. if they can be sure based on external constraints that it will never happen, or if they're simply fine with overflow resulting in wrap-around. Or their CPU might even provide a built-in overflow flag. Still, nice explanation, +1. :) \$\endgroup\$ Commented Aug 14, 2022 at 11:32
  • \$\begingroup\$ One way of understanding why it works is first to understand that 2's complement add/subtract are the same binary operation as unsigned. And that a chain of add/adc (or sub/sbb) is a software way to propagate carry across the boundary between separate register-width operations, exactly like would happen with a wider hardware adder. The simplest hardware adder is just a chain of full adders, with ripple-carry propagation between every bit, including between bit #7 and bit #8. \$\endgroup\$ Commented Aug 14, 2022 at 12:16
  • \$\begingroup\$ (Realized further comments I was going to post were turning into an answer, posted as such.) \$\endgroup\$ Commented Aug 14, 2022 at 12:39
3
\$\begingroup\$

add/adc Just Works. So does sub/sbb (or sbc as some ISAs call it, usually the ones where the carry flag is a not-borrow flag after subtraction, like ARM, opposite of x86).

One way of understanding why it works is first to understand that 2's complement add/subtract are the same binary operation as unsigned. (Except divide, and widening multiply). That's one reason we use 2's complement, so we don't need separate add/sub instructions.

A chain of add/adc (or sub/sbb) is a software way to propagate carry across the boundary between separate register-width operations, exactly like would happen with a wider hardware adder. The simplest hardware adder is just a chain of full adders, with ripple-carry propagation between every bit, including between bit #7 and bit #8. Doing two 8-bit halves is just using CF to propagate that carry from low to high.

Note that only the high bit of the highest chunk is actually the sign bit (with place-value -2^(n-1).) The place-value of the high bit of the low byte is 2^7 = 128, so you could say that each chunk below the highest is unsigned. e.g. 0x00ff is 255 as a 16-bit 2's complement number, not -128.

After addition with an add/adc chain, if your CPU has a signed-overflow flag (like x86's OF or ARM's V), that flag will correctly reflect whether the whole operation had signed overflow or not. As http://teaching.idallen.com/dat2343/10f/notes/040_overflow.txt explains, one way for an ALU to calculate it is by XORing the carry-in and carry-out from the most-significant bit. Those are exactly the same as they'd be if we'd done a single 16-bit add instruction instead of 8-bit add/adc.

I forget whether it's that easy for subtraction. I think so. (Edits welcome, or I'll update this when I have time to check.)
Borrow propagates strictly from low to high. And subtraction is usually implemented by NOTing one input and feeding a 1 as carry-in to an adder. With these control modifications, you have an adder-subtractor built on top of whatever fast carry-lookahead or carry-save adder you're actually using.
Or for sbc, flipping the carry input instead of using it directly.

\$\endgroup\$
1
  • \$\begingroup\$ One mathy way to view it is the 2s complement system implements a range of the 2-adic numbers, where the last n digits correspond to integers mod 2^n. Interestingly this also explains sign-extending negative values. zjkmxy.github.io/posts/2021/11/twos-complement-2-adic \$\endgroup\$
    – qwr
    Commented Aug 15, 2022 at 0:02
-2
\$\begingroup\$

This is like we deal with numbers more than 9 in our regular decimal number system.

Actual examples could be found as AVR202: 16-bit Arithmetics, for AVR architecture, which i worked with. This basis can be applied to any other architecture.

\$\endgroup\$
1
  • 6
    \$\begingroup\$ Vladimir - Hi, Are you able to edit this answer and add more detail and/or example(s) etc.? Answers on Stack Exchange should be self-contained, only referring to (or linking to) other websites for supplementary information, and not relying on them for readers to understand the answer. If you can include more information, so that this answer would be all that someone would need to read, to learn the answer to the question, IMHO that would be helpful to future readers. It's your answer, so it's your choice. Thanks. \$\endgroup\$
    – SamGibson
    Commented Aug 13, 2022 at 20:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.