What is gained and lost with 63-bit integers?
blogs.janestreet.com
blogs.janestreet.com
http://compilers.iecc.com/comparch/article/91-04-082
http://en.wikibooks.org/wiki/SPARC_Assembly/Arithmetic_Instr...
Back in the late 1980's, a Sun employee jokingly pointed out that because the DECstation used the MIPS processor in little endian mode (it supported both big and little endian) to match the VAX, its processor should be called the SPIM. I asked if the SPARC supported little endian mode, would it be called the CRAPS? He was flustered, and told me to shut up and never tell anyone that SPARC spelled backwards was CRAPS.
There are much worse ways to go -- like the way of the Oracle, for example. ;(
If you're willing to reduce integers to 62 bits (and assuming no overflows are allowed), multiplication can also be simplified: (2x+1)(2y+1) = 4xy + 2x + 2y + 1. So:
t = x + y - 2; // + 1 - 3
p = x * y; // 4*x*y + 2*x + 2*y + 1
return (p - t) >> 1; // (4*x*y + 3) / 2
In amd64, this can be done as ; input in rdi, rsi
lea rax, [rdi + rsi - 2] ; slow, but runs concurrently with imul
imul rdi, rsi
sub rdi, rax
shr rdi, 1 ; output leaq (%rdi,%rsi), %rax
Whereas subtracting the tag gives you: leaq -1(%rdi,%rsi), %rax
So pretty much the same either way, though you do get five instruction bytes for the tag version vs. four bytes without.Here are the comparisons for multiply and divide, from "gcc -c -O3 -g -march=native" and "objdump -d":
x * y:
10: 48 89 f8 mov %rdi,%rax
13: 48 0f af c6 imul %rsi,%rax
(x >> 1) * (y - 1) + 1:
40: 48 d1 ff sar %rdi
43: 48 83 ee 01 sub $0x1,%rsi
47: 48 89 f8 mov %rdi,%rax
4a: 48 0f af c6 imul %rsi,%rax
4e: 48 83 c0 01 add $0x1,%rax
x / y:
20: 48 89 f8 mov %rdi,%rax
23: 48 99 cqto
25: 48 f7 fe idiv %rsi
(((x >> 1) / (y >> 1)) << 1) + 1:
60: 48 89 f8 mov %rdi,%rax
63: 48 d1 fe sar %rsi
66: 48 d1 f8 sar %rax
69: 48 99 cqto
6b: 48 f7 fe idiv %rsi
6e: 48 8d 44 00 01 lea 0x1(%rax,%rax,1),%raxCuriously, if you need to move the result to another register (say, to respect calling conventions) the optimal solution is a 2-operand LEA plus a DEC:
lea rax, [rdi + rsi]
dec rax
I was wrong about ADD + DEC being smaller, though. I forgot that amd64 took away the 1-byte INC/DEC instructions to serve as REX prefix, so the DEC takes 3 bytes, not 1.Just showing the different things you can do with tagged pointers I suppose. It's pretty common in Lisp/Scheme runtimes. I love bit twiddling hackery.
In theory, you could unbox some extra things at the cost of complicating the garbage collection algorithm and the memory representatio of values. GHC does this for Haskell but the Ocaml guys don't because they highly value keeping the compiler as simple as possible.
By the way, integer tagging is only important if your code s heavy in arithmetic operations or floating point operations (Ocaml tries to unbox floating point arrays, but sometimes it has to box floats). This often not that big of a problem and in the worst case you can always link Ocaml with C if you really want to.
Except that they are not the same since bools and enums don't need 64 bits and you mostly don't do integer arithmetic with them.
> In theory, you could unbox some extra things at the cost of complicating the garbage collection algorithm and the memory representatio of values. GHC does this for Haskell but the Ocaml guys don't because they highly value keeping the compiler as simple as possible.
I'm unclear on how special-casing integers and foisting a lot of bit-twiddling on all integer operations makes the compiler less complicated. Treating all values uniformly and using machine arithmetic everywhere would seem to minimize compiler complexity and make things faster.
> By the way, integer tagging is only important if your code s heavy in arithmetic operations or floating point operations (Ocaml tries to unbox floating point arrays, but sometimes it has to box floats). This often not that big of a problem and in the worst case you can always link Ocaml with C if you really want to.
Noted: if you want to do numerical work in OCaml, use C ;-)
But seriously, is there code that isn't heavy on integer arithmetic or floating point operations? Do computers do anything else?
https://realworldocaml.org/v1/en/html/memory-representation-...
This makes the generated executables less efficient but it makes the lives of the compiler and garbage collector easier: Every single variable or function parameter has the same size and you can easily tell if a value is a pointer or not just by looking at it.
In a way, its a little bit like executing bytecode in a virtual machine. The executable does some extra bit twiddling, boking and tagging but lots of things get simpler because of the uniform value representation.
> But seriously, is there code that isn't heavy on integer arithmetic or floating point operations? Do computers do anything else?
Lots of software nowadays is bottlenecked by the speed of network interfaces or RAM, meaning that the arithmetic speed isn't a big factor.
The bug is 30-bit signed ints (29bits) with epoch 1993.
That makes sense. The performance penalty might be incremental compared to the overhead of dynamic typing.
That's not true. Most implementations which use a GC and/or are dynamically typed will have less than 64bit integers.
Wait, is the answer not floating point? Using 64-bit IEEE 754 means the lower 52 bits (51-0) are significand, and bit 52 is implicitly 1, so setting bit 0 to keep track of GC stuff only introduces an error of ~1/2^52.
Floating-point multiplication isn't as fast as integer ALUs, perhaps, and you're now limited to 52-bit integers, but that's what Lua does and it gets by fine.
IIRC SpiderMonkey stores doubles -- the fundamental numeric data type in JS -- as "unboxed" values, using this article's terminology. I'm not sure if there's good documentation about this out there, but it's an interesting hack, and much more complicated than just setting the bottom bit of the value.
http://wingolog.org/archives/2011/05/18/value-representation...
The basic idea is that there are 2^53 different bit patterns that are counted as NaN by the computer but only one is actually used in practice. This means that you can "steal" the rest of the NaNs and use them to encode numbers or pointers.
https://github.com/munificent/wren/blob/master/src/wren_valu...
e.g. x+y becomes (x + y | (1<<63))
Or does the loss of overflow detection hurt you more?
IIRC, you can do this just by checking if it is less than zero.
tl;dr: all valid pointer values are possible, regardless of how much memory you actually have.
And, when you get down to the assembly level, it doesn't matter - you can treat an unsigned integer as signed for a comparison if it makes things easier.
That may be rare nowadays (or is it? I remember reading recently that some 64-bit CPU chose to preferably use a memory range from -2GB to +2GB. AMD64?), but whether there is is outside of the control of most language implementers. They can control their own memory allocator, though, and guarantee that it never stores 64-bit quantities at odd addresses.
Pointers are guaranteed to have LSB of zero because of data is typically aligned to the machine word size rather than individual bytes. In this case, that is enforced (or at worst everything must be 16-bit aligned).
It's a pretty clever idea really.
Except pointers to functions in ARM thumb. Or more simply pointers inside strings.
void a(int *b)
{
*b = 5;
}
struct c
{
int d;
};
int main()
{
struct c foo;
a(&foo.d); // no way to do this in Java!
printf("%lu\n", (unsigned long)foo.d);
}There's nothing that requires any particular alignment on x86/x64 either, but it's standard for most compilers because it makes everything run faster at minimal size cost.
The actual instructions are aligned on an even byte boundary; however, on AArch32, encoding interoperability is achieved with BLX or BX, which uses the low bit as a tag value: 0=A32, 1=T16/32. However, in a lisp interpreter, e.g., you should never find a function pointer in an "object" context; i.e., something which could be an integer instead.
OT: Thanks to Don Stewart for the two interesting links: https://ghc.haskell.org/trac/ghc/wiki/Commentary/Rts/Storage... https://ghc.haskell.org/trac/ghc/wiki/Commentary/Rts/Haskell...
Tagged values as a programming-language implementation technique do seem to take off more towards the early 1970s, though, with the ex-MIT team that would produce the Lisp Machine, and the Xerox team that would produce Smalltalk, both using tagged values prominently.
[1] https://en.wikipedia.org/wiki/Rice_Institute_Computer
[2] A somewhat later (1973) paper referenced above: http://www.feustel.us/Feustel%20&%20Associates/Advantages.pd...
The Symbolics does support it in hardware, though.