Does RISC-V mandate two's complement or one's complement signedness, or is it implementation-determined? - twos-complement

I have looked through the ISA spec and searched the internet for the answer to this, but I could not find it.
In the RISC-V ISA, should negative numbers be represented with one's complement or two's complement? Or, is this decision left to implementors?
The reason I ask is that I am writing an RV32I simulator, and this would affect how I store negative numbers in the simulated memory, for example.

The RISC-V architecture requires twos-complement integer arithmetic. This can be most directly seen from the fact that it specifies a single addition instruction, not a pair of signed and unsigned addition instructions. In twos-complement arithmetic, signed and unsigned addition are the same operation; in ones-complement (and sign-magnitude) they are not the same.
It appears to me, skimming the architecture manual, that the authors considered the choice of twos-complement integer arithmetic too obvious to bother mentioning. There hasn't been a CPU manufactured in at least 25 years that used anything else.

The user-level ISA manual (page 13) notes that bitwise NOT rd, rs1 may be performed by XORI rd, rs1, -1, which would imply two's complement, if I see things correctly: XORing with the one's complement of -1 would not invert the least significant bit, while it would work correctly in two's complement.

Yes, the RISC-V instruction set architecture (ISA) mandates two's complement:
The base integer instruction sets use a two’s-complement representation for signed integer values.
(Section 1.3 RISC-V ISA Overview, page 4, The RISC-V Instruction Set Manual. Volume I, 2019-06-08, ratified)
General purpose registers x1–x31 hold values that various instructions interpret as a collection of Boolean values, or as two’s complement signed binary integers or unsigned binary integers.
(Section 2.1 Programmers' Model for Base Integer ISA, page 13, The RISC-V Instruction Set Manual. Volume I, 2019-06-08, ratified)

Related

How does the CPU "cast" a floating point x87 (i think) value?

I just wanted to know how the CPU "Cast" a floating point number.
I mean, i suppouse that when when we use a "float" or "double" in C/C++ the compiler is using the x87 unit, or am i wrong? (i couldn't find the answer) So, if this is the case and the floating point numbers are not emulated how does the compiler cast it?
I mean, i suppouse that when when we use a "float" or "double" in C/C++ the compiler is using the x87 unit, or am i wrong?
On modern Intel processors, the compiler is likely to use the SSE/AVX registers. The FPU is often not in regular use.
I just wanted to know how the CPU "Cast" a floating point number.
Converting an integer to a floating-point number is a computation that is basically (glossing over some details):
Start with the binary (for unsigned types) or two’s complement (for signed types) representation of the integer.
If the number is zero, return all bits zero.
If it is negative, remember that and negate the number to make it positive.
Locate the highest bit set in the integer.
Locate the lowest bit that will fit in the significand of the destination format. (For example, for the IEEE-754 binary32 format commonly used for float, 24 bits fit in the significand, so the 25th bit after the highest bit set does not fit.)
Round the number at that position where the significand will end.
Calculate the exponent, which is a function of where the highest bit set is. Add a “bias” used in encoding the exponent (127 for binary32, 1023 for binary64).
Assemble a sign bit, bits for the exponent, and bits for the significand (omitting the high bit, because it is always one). Return those bits.
That computation prepares the bits that represent a floating-point number. (It omits details involving special cases like NaNs, infinities, and subnormal numbers because these do not occur when converting typical integer formats to typical floating-point formats.)
That computation may be performed “in software” (that is, with general instructions for shifting bits, testing values, and so on) or “in hardware” (that is, with special instructions for doing the conversion). All desktop computers have instructions for this. Small processors for special-purpose embedded use might not have such instructions.
It is not clear what do you mean by
"Cast" a floating point number. ?
If target architecture has FPU then compiler will issue FPU instructions in order to manipulate floating point variables, no mistery there...
In order to assign float variable to int variable, float must be truncated or rounded(up or down). Special instructions usually exists to serve this purpose.
If target architecture is "FPU-less" then compiler(toolchain) might provide software implementation of floating point operations using CPU instructions available. For example, expression like a = x * y; will be equivalent to a = fmul(x, y); Where fmul() is compiler provided special function(intrinsic) to do floating point operations without FPU. Ofcourse this is typically MUCH slower than using hardware FPU. Floating point arithmetic is not used on such platforms if performance matters, fixed point arithmetic https://en.wikipedia.org/wiki/Fixed-point_arithmetic could be used instead.

What method does the computer use to add unsigned integers

int main(){
unsigned int num1 = 0x65764321;
unsigned int num2 = 0x23657432;
unsigned int sum = num1 + num2;
cout << hex << sum;
return 0;
}
If i have two unsigned integers say num1 and num2. And then I tell the computer to unsigned
int sum = num1 + num2;
What method does the computer use to add them, would it be two's complement. Would the sum variable be printed in two's complement.
2's complement addition is identical to unsigned addition as far the actual bits are concerned. In the actual hardware, the design will be something complicated like a https://en.wikipedia.org/wiki/Carry-lookahead_adder, so it can be low latency (not having to wait for the carry to ripple across 32 or 64 bits, because that's too many gate-delays for add to be single-cycle latency.)
One's complement and sign/magnitude are the other signed-integer representations that C++ allows implementations to use, and their wrap-around behaviour is different from unsigned.
For example, one's complement addition has to wrap the carry-out back into the low bit. See this article about optimizing TCP checksum calculation for how you implement one's complement addition on hardware that only provide 2's complement / unsigned addition. (Specifically x86).
C++ leaves signed overflow as undefined behaviour, but real one's complement and sign/magnitude hardware does have specific documented behaviour. reinterpret_casting an unsigned bit pattern to a signed integer gives a result that depends on what kind of hardware you're running on. (All modern hardware is 2's complement, though.)
Since the bitwise operation is the same for unsigned or 2's complement, it's all about how you interpret the results. On CPU architectures like x86 that set flags based on the results of an instruction, the overflow flag is only relevant for the signed interpretation, and the carry flag is only relevant for the unsigned interpretation. The hardware produces both from a single instruction, instead of having separate signed/unsigned add instructions that do the same thing.
See http://teaching.idallen.com/dat2343/10f/notes/040_overflow.txt for a great write-up about unsigned carry vs. signed overflow, and x86 flags.
On other architectures, like MIPS, there is no FLAGS register. You have to use a compare or test instruction to figure out what happened (carry or zero or whatever). The add instruction doesn't set flags. See this MIPS Q&A about add-with-carry for a 64-bit add on 32-bit MIPS.
But for detecting signed overflow, add raises an exception on overflow (where x86 would set OF), so you use addu for signed or unsigned addition if you want it to not fault on signed overflow.
now the overflow flag here is 1(its an example given by our instructor) meaning there is overflow but there is no carry, so how can there be overflow here
You have a C++ program, not an x86 assembly language program! C++ doesn't have a carry or overflow flag.
If you compiled this program for x86 with a non-optimizing compiler, and it used the ADD instruction with your two inputs, you would get OF=1 and CF=0 from that ADD instruction.
But the compiler might use lea edi, [rax+rdx] to do the sum without overwriting either input, and LEA doesn't set flags.
Or if the compiler did the addition at compile time, your code would compile the same as source like this:
cout << hex << 0x88dbb753U;
and no addition of your numbers would take place at run-time. (There will of course be lots of addition in the iostream library functions, and maybe even an add instruction in main() as part of making a stack frame, if your compiler chooses to emit code that sets up a stack frame.)
i have two unsigned integers
What method does the computer use to add them
Whatever method is available on the target CPU architecture. Most have an instruction named ADD.
Would the sum variable be printed in two's complement.
Two's complement is a way to represent an integer type in binary. It is not a way to print numbers.

Signed type representation in c++

In the book I am reading it says that:
The standard does not define how signed types are represented, but does specify that range should be evenly divided between positive and negative values. Hence, an 8-bit signed char is guaranteed to be able to hold values from -127 through 127; most modern machines use representations that allow values from -128 through 127.
I presume that [-128;127] range arises from method called "twos-complement" in which negative number is !A+1 (e.g. 0111 is 7, and 1001 is then -7). But I cannot wrap my head around why in some older(?) machines the values range [-127;127]. Can anyone clarify this?
Both one's complement and signed magnitude are representations that provide the range [-127,127] with an 8 bit number. Both have a different representation for +0 and -0. Both have been used by (mostly) early computer systems.
The signed magnitude representation is perhaps the simplest for humans to imagine and was probably used for the same reason as why people first created decimal computers, rather than binary.
I would imagine that the only reason why one's complement was ever used, was because two's complement hadn't yet been considered by the creators of early computers. Then later on, because of backwards compatibility. Although, this is just my conjecture, so take it with a grain of salt.
Further information: https://en.wikipedia.org/wiki/Signed_number_representations
As a slightly related factoid: In the IEEE floating point representation, the signed exponent uses excess-K representation and the fractional part is represented by signed magnitude.
It's not actually -127 to 127. But -127 to -0 and 0 to 127.
Earlier processor used two methods:
Signed magnitude: In this a a negative answer is form by putting 1 at the most significant bit. So 10000000 and 00000000 both represent 0
One's complement: Just applying not to positive number. This cause two zero representation: 11111111 and 00000000.
Also two's complement is nearly as old as other two. https://www.linuxvoice.com/edsac-dennis-wheeler-and-the-cambridge-connection/

Two's Complement disadvantage?

I was reading about two's complement, I understand this method is most efficient, but there might be some disadvantages too. I could not find any disadvantages, Is there any situation where the conversion to two's complement could fail to represent the number correctly?
Two's complement is awesome - that's why everyone uses it. The biggest disadvantage is that if you try to negate the lowest representable value, you get an overflow. With one's complement or sign and magnitude, that doesn't happen.
With "two complement"-notation you can't compare the size of two Integers with very simple logic operators (at lowest level hardware). Thats the reason why the exponent in IEEE Standard for Floating-Point Arithmetic (IEEE 754) is not represented in "two complement" but in "biased" notation.

Two's complement binary form

In a TC++ compiler, the binary representation of 5 is (00000000000000101).
I know that negative numbers are stored as 2's complement, thus -5 in binary is (111111111111011). The most significant bit (sign bit) is 1 which tells that it is a negative number.
So how does the compiler know that it is -5? If we interpret the binary value given above (111111111111011) as an unsigned number, it will turn out completely different?
Also, why is the 1's compliment of 5 -6 (1111111111111010)?
The compiler doesn't know. If you cast -5 to unsigned int you'll get 32763.
The compiler knows because this is the convention the CPU uses natively. Your computer has a CPU that stores negative numbers in two's complement notation, so the compiler follows suit. If your CPU supported one's complement notation, the compiler would use that (as is the case with IEEE floats, incidentally).
The Wikipedia article on the topic explains how two's complement notation works.
The processor implements signed and unsigned instructions, which will operate on the binary number representation differently. The compiler knows which of these instructions to emit based on the type of the operands involved (i.e. int vs. unsigned int).
The compiler doesn't need to know if a number is negative or not, it simply emits the correct machine or intermediate language instructions for the types involved. The processor or runtime's implementation of these instructions usually doesn't much care if the number is negative or not either, as the formulation of two's complement arithmetic is such that it is the same for positive or negative numbers (in fact, this is the chief advantage of two's complement arithmetic). What would need to know if a number is negative would be something like printf(), and as Andrew Jaffe pointed out, the MSBit being set is indicative of a negative number in two's complement.
The first bit is set only for negative numbers (it's called the sign bit)
Detailed information is available here
The kewl part of two's complement is that the machine language Add, and Subtract instructions can ignore all that, and just do binary arithmetic and it just works...
i.e., -3 + 4
in Binary 2's complement, is
1111 1111 1111 1101 (-3)
+ 0000 0000 0000 0100 ( 4)
-------------------
0000 0000 0000 0001 ( 1)
let us give an example:
we have two numbers in two bytes in binary:
A = 10010111
B = 00100110
(note that the machine does not know the concept of signed or unsigned in this level)
now when you say "add" these two, what does the machine? it simply adds:
R = 10111101 (and carry bit : 1)
now, we -as compiler- need to interpret the operation. we have two options: the numbers can be signed or unsigned.
1- unsigned case: in c, the numbers are of type "unsigned char" and the values are 151 and 38 and the result is 189. this is trivial.
2 - signed case: we, the compiler, interpret the numbers according to their msb and the first number is -105 and the second is still 38. so -105 + 38 = -67. But -67 is 10111101. But this is what we already have in the result (R)! The result is same, the only difference is how the compiler interprets it.
The conclusion is that, no matter how we consider the numbers, the machine does the same operation on the numbers. But the compiler will interpret the results in its turn.
Note that, it is not the machine who knows the concept of 2's complement. it just adds two numbers without caring the content. The compiler, then, looks at the sign bit and decides.
When it comes to subtraction, this time again, the operation is unique: take 2's complement of the second number and add the two.
If the number is declared as a signed data type (and not type cast to an unsigned type), then the compiler will know that, when the sign bit is 1, it's a negative number. As for why 2's complement is used instead of 1's complement, you don't want to be able to have a value of -0, which 1's complement would allow you to do, so they invented 2's complement to fix that.
It's exactly that most significant bit -- if you know a number is signed, then if the MSB=1 the compiler (and the runtime!) knows to interpret it as negative. This is why c-like languages have both integers (positive and negative) and unsigned integers -- in that case you interpret them all as positive. Hence a signed byte goes from -128 to 127, but an unsigned byte from 0 to 255.