For values such as true
, nil
, or small integers, Ruby does optimization. Instead of using VALUE
pointer as a pointer, it directly uses VALUE
to store data.
I wonder how Ruby makes a difference between these uses:
def foo(x)
...
with x
that will be associated to VALUE
. In low level terms, they are just a number. How can I tell whether or not a certain number is a pointer to an object? All that comes to my mind is to limit pointers to have the MSB set to 0, and direct values with MSB equal to 1. But this is just my guess. How is it done in Ruby?
There are many different implementations of Ruby. The Ruby Language Specification doesn't prescribe any particular internal representation for objects – why should it? It's an internal representation, after all!
For example, JRuby doesn't represent objects as C pointers at all, it represents them as Java objects. IronRuby represents them as .NET objects. Opal represents them as ECMAScript objects. MagLev represents them as Smalltalk objects.
However, there are indeed some implementations that use the strategy you describe. The now abandoned MRI did it that way, YARV and Rubinius also do it.
This is actually a very old trick, dating back to at least the 1960s. It's called a tagged pointer representation, and like the name suggests, you need to tag the pointer with some additional metadata in order to know whether or not it is actually a pointer to an object or an encoding of some other datatype.
Some CPUs have special tag bits specifically for that purpose. (For example, on the AS/400, the CPU doesn't even have pointers, it has 128bit object references, even though the original CPU was only 48bit wide, and the newer POWER-based CPUs 64 bit; the extra bits are used to encode all sorts of metadata like type, owner, access restrictions, etc.) Some CPUs have tag bits for other purposes that can be "abused" for this purpose. However, most modern mainstream CPUs don't have tag bits.
But, you can use a trick! On many modern CPUs, unaligned memory accesses (accessing an address that does not start at a word boundary) are really slow (on some, they aren't even possible at all), which means that on a 32bit CPU, all pointers that are realistically being used, end with two 00
bits and on 64 bit CPUs with three 000
bits. You can use these bits as tag bits: pointers that end with 00
are indeed pointers, pointers that end with 01
, 10
, or 11
are an encoding of some other data type.
In MRI, the pointers ending in 1
were used to encode 31/63 bit Fixnum
s. In YARV, they are used to encode 31/63 bit Fixnum
s, i.e. integers that are encoded as actual machine integers according to the formula 2n+1
(arithmetically speaking) or (n << 1) | 1
(as a bit pattern). On 64 bit platforms, YARV also uses pointers that end in 10
to encode 62 bit flonums using a similar scheme. (If you ever wondered why the object_id
of a Fixnum
in YARV is 2n+1, now you know: YARV uses the memory address for the object ID, and 2n+1 is the "memory address" of n.)
Now, what about nil
, false
and true
? Well, there is no space for them in our current scheme. However, the very low memory addresses are usually reserved for the operating system kernel, which means that a pointer like 0
or 2
or 4
cannot realistically occur in a program. YARV uses that space to encode nil
, false
and true
: false
is encoded as 0
(which is convenient because that's also the encoding of false
in C), nil
is encoded as 0b1000
and true
is encoded as 0b10100
(it used to be 0
, 0b10
and 0b100
in older versions before the introduction of flonums).
Theoretically, there is a lot of space there to encode other objects as well, but YARV doesn't do that. Some Smalltalk or Lisp VMs, for example, encode ASCII or BMP Unicode character objects there, or some often used objects such as the empty list, empty array, or empty string.
There is still some piece missing, though: without an object header, with just the bare bit pattern, how can the VM access the class, the methods, the instance variables, etc.? Well, it can't. Those have to be special-cased and hardcoded into the VM. The VM simply has to know that a pointer ending in 1
is an encoded Fixnum
and has to know that the class is Fixnum
and the methods can be found there. And as for instance variables? Well, you could store them separately from the objects in a dictionary on the side. Or you go the Ruby route and simply disallow them altogether.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With