Re: Few questions about LuaJIT internals on 64 bit

  • From: Peter Cawley <corsix@xxxxxxxxxx>
  • To: luajit@xxxxxxxxxxxxx
  • Date: Thu, 23 Mar 2017 23:07:02 +0000

On Thu, Mar 23, 2017 at 8:53 PM, Blaise R. <rex2k8@xxxxxxxxx> wrote:

1. I understand that LuaJIT works (in particular, in case of light user data
pointers) with the assumption that 16 top bits of a 64 bit pointer are free
for use due to this being the case in current implementations, however, what
if an implementation with more bits than that appears? I know this is highly
theoretical but still an interesting point.

It is actually the top 17 bits, and is less theoretical than you might
think (e.g. Solaris x64 exposes 48 bits to usermode rather than the 47
which Linux exposes, ARM64 sometimes has 51/52 bits exposed, Intel are
making movements towards another level of page translation, etc.).
LuaJIT just runs with the assumption you state; if assertions are
turned on, then you might get assertion failures, otherwise the top 17
bits of your pointers just get zeroed (and subsequently might cause
breakages). In the long term, I'm not sure anybody has a great answer
- as pointers get larger, NaN packing gets harder, and it may well
become impractical, which would then require a major overhaul of
LuaJIT.

2. Why can't the above trick be used with GC objects. I know LuaJIT uses
only bottom 32 bit of the address space which leads to the infamous problem
with using strings to hold binary data. I remember coming across mentions
that GC objects "must be" in the bottom 4 GB of virtual address space. Why
is that?

The general pattern is to spend a bunch of time designing the
object/TValue layout, and then write a bunch of assembly code on top
of that design (i.e. the LuaJIT interpreter), and then write a bunch
of C code which generates machine code on top of that design (i.e. the
LuaJIT compiler). Changing the design after the fact is somewhat
difficult, hence LuaJIT having the following three designs in
chronological order:
1. (x86 mode) All pointers are 32 bits wide; TValues for pointer-y
types are 32 bits of pointer and 32 bits of NaN/type tag.
2. (x64 mode) Almost all pointers are 32 bits wide, except for light
userdata, which can be 47 bits wide.
3. (x64 GC64 mode) All pointers are 47 bits wide; TValues for
pointer-y types are 47 bits of pointer and 17 bits of NaN/type tag.

Going from design 1 to design 2 was a large chunk of work, but
substantially less than going from design 1 to design 3 (hence design
3 being a relatively recent thing).

3. In case the pointers to GC objects have to just be 32 bit values, could a
trick like the one in Java be used? HotSpot and some other JVMs are able to
make pointer aligned to 2, 4 or 8 bytes and then shift (and offset if they
don't start at zero) them left and right appropriately before use and
storage (because 1 to 3 bottom bits are always 0 due to alignment), this
gains them 1, 2 or 3 bits while keeping the pointer value 32 bit at the cost
of a single shift (and maybe addition) which Oracle claims is negligible
compared to the benefits. It's described better in here or by simply
googling 'Java compressed object pointers':
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html#compressedOop

To extend the previous answer, that could be considered design 3b - it
would be as much work as design 3 to implement, but with a smaller
payoff (32+3 bit pointers rather than 47 bit pointers), hence design 3
being chosen instead.

Other related posts: