A gentle introduction to two's complement
imapenguin.com
imapenguin.com
Let's say x=000110 for example
Invert all bits of x, call that number inv(x)=111001
If we add them together, "the gears mesh together" and we get x+inv(x)=111111
Adding 1 to that, let the carry just drop off the left side, we get x+inv(x)+1=000000
So it's natural to take inv(x)+1 as a representation of -x.
Many articles I read said that 2's complement helps in arithmetic, but I was not able to understand "how".
The naive me was like - why do they complement and set a sign bit? We can just use a sign bit right? So if 0010 means +2, then why not represent -2 as 1010. What's the need for the complement part at all?!
Then only I realized that we are able to retain x + (-x) = 0 using this 2's complement representation. :)
Two's complement is ordinary integer arithmetic. There is no difference between two's complement arithmetic and unsigned arithmetic. All calculations are literally identical.
Unsigned integer arithmetic is modular arithmetic with equivalence classes having labels between 0 (inclusive) and 2^n (exclusive), while two's complement arithmetic is the same thing, but the equivalence classes have labels between negative 2^{n-1} (inclusive) and 2^{n-1} (exclusive).
Here's a table of three-bit values with "unsigned" and "two's complement" labels:
100 4 -4
101 5 -3
110 6 -2
111 7 -1
000 0 0
001 1 1
010 2 2
011 3 3
100 4 -4
101 5 -3
110 6 -2
111 7 -1
Note that when you're working in mod 8, 5 and -3 are the same value, so this is nothing but a labeling difference.https://thesis.laszlokorte.de/demo/number-circle.html
And notice how „just using the sign bit“ and the 2compl are just different ways to align interepretations with bit patterns.
But using the 2compl is the alignment that does not require special cases for handling the negative numbers as opposed to the positive numbers.
For reference (and note the apostrophe locations):
* sign-magnitude is slightly easier for multiplication/division but makes addition/subtraction marginally harder. It's widely used in bigint libraries, and IEEE binary floats use a variant of it (unlike IEEE decimal floats, which are not at all sane).
* two's complement is optimal for addition/subtraction, but has a weird "most negative" value. Fixed-width multiplication and division have a couple warts but .
* ones' complement is not particularly useful compared to the above two choices, though I suppose it's marginally easier to sort than sign-magnitude
* offset-binary is useful for naive sorting, but casting to/from unsigned is not a nop.
For two's complement at least, to avoid the UB you can just cast to/from unsigned around most operations. Division has to be done with some kind of branches, however.
I rather wish we had an integer `NaN` instead of the most-negative value.
Also, there are several useful overflow behaviors: trap, wrap, saturate, unspecified, and undefined. "unspecified" is particularly useful if you're sometimes transpiling to a JS-like language that doesn't actually support integers.
I want to make user defined types like BalancedI32 (32bit signed integer but the magnitude of MAX and MIN are the same) possible in Rust. Today the stdlib is allowed to make NonZeroI32 which as its name suggests lacks zero but you can't make such types yourself except via wrapping these NonZero types with xor or similar.
(In nightly Rust you can just break the rules and do the same thing the stdlib does, but there's no obligation to keep that working and it will never be stabilized)
Because of the Guaranteed Niche Optimisation this means Option<BalancedI32> would have identical size to i32. So you get the same practicality as a normal signed integer with a sentinel value, but the same programming ergonomics as an expensive high level type.
I think that, if NaN was defined to be an unconditional trap but can be deferred for efficiency, then it could have worked much better. NaN surprises people because it was exposed in the first place, while it should really have been hidden behind the scene (because the presence of NaN means you are already wrong somewhere, and you are expected to be notified about that fact). In terms of Rust, say, every f32 operation could have returned `Result<f32, FloatError>` (mostly likely to be aliased though) that has the exactly same layout as the current f32. The current split between qNaN and sNaN (among others) is unnecessarily complex with little benefit.
Some Rust types have niches, bit patterns which the type doesn't use. For example booleans take up a whole byte, but only need two bit patterns, leaving 254 unused. Today there is no stable way to conjure into existence a type with a new niche of your choosing.
The example I gave was BalancedI32, a type which is exactly like i32 except that it doesn't use the most negative value. The most negative value of integers in a two's complement system is a weird outlier, so in many ways this is nicer to work with, but the niche also allows such a type to take advantage of the Guaranteed Niche Optimisation.
Rust's String type is basically just a Vec<u8> with extra promises about the bytes (they're guaranteed to be a UTF-8 string) and there's nothing special about it, you can make your own and several people have. You can even use an existing niche to do clever things, as CompactString does.
Thanks.
The most negative value is a wart in two's complement, but negative zero is a wart in sign-magnitude and ones' complement.
The advantage of ones' complement compared to sign-magnitude is that the former needs minimal circuitry to implement subtraction. Namely, it just needs to invert every bit and then feed it into the adder.
You only need to avoid undefined behavior in two's complement in C and C++. UB is not a property of two's complement, but a particular language's design choices. I can assure you that there is no UB in two's complement in Java or x86 assembly.
Note that offset-binary is used in IEEE 754 floating-point exponents.
The entire point of making unsigned arithmetic UB in C was to support different bit representations of negative numbers depending on the HW.
I think the GP's point was that if you want two's complement wraparound behavior in languages where you are not allowed to wrap around signed integers you can get the same result with unsigned arithmetic.
https://github.com/openjdk/jdk/blob/28026434f74726d72857e781...
s e m
1 11 20 - 6 mantissa digits, exponent range too large (~10^+/-1000)
1 7 24 - 7 mantissa digits, exponent range too small (~10^+/-60)
1 7.6 23.3 - still 7 mantissa digits, but interleaving gives exponent range of ~10^+/-90
1 4 27 - 8 mantissa digits, exponent range too small to even contain binary32
1 1 30 - 9 mantissa digits but no usable exponent range
I guess it is not the worst trade-off.Your point about non-canonical values is entirely valid though. I do know it is mostly due to the expensive normalization step, but that only sounds like a reason to always prefer DPD---there is little reason to define a decimal FP standard if it is not for hardwares nowadays.
To find the two’s complement of 5, we invert all the bits (changing 0s to 1s and 1s to 0s) and then add one to the result.
I am embarrassed by the fact that I wasn't aware of this. I always interpreted the two's complement as the difference between the all the numbers before the high bit, minus the high bit. Aka 10000000 – 01111011 = 0101If you want your CPU to efficiently support multi-precision addition, you can have an overflow machine state flag and have an instruction that uses that overflow flag as the carry-in to the least significant bit during addition.
What really made it click for me was thinking of it as modular arithmetic. If you consider 8-bit integers, they range from 0-255 and you're actually working modulo 256. So you can think of 0-127 as your non-negative numbers. The numbers from 128-255 behave as negatives modulo 256 (e.g. -1 mod 256 = 255).
The decimal value of 11111011 is -5
Is not really explained? I would have thought this was -123
11111011
+ 00000101
--------
(1)00000000
Two's complement is just like those spinning number wheel counters, like the km counter in old cars: If you drove more than 999999 km, it would look like you drove 000000. Now, if you had a funny car where driving backwards would subtract from the km counter, starting at 000000 and driving backwards 1 km would result in 999999, therefor 999999 means -1 (you could say 000000 to 499999 is positive, 500000 to 999999 is negative).