Are float immediate values in Ruby?

Viewed 184

According to the Ruby FAQ, fixnum (hold integer), boolean and nil are immediate values. In my understanding, an object holding an immediate value is always exactly the same, no matter how many variables it is assigned to. And this is easy to verify in Ruby:

# Immediate values
i1 = 1
i2 = 1
i1.object_id  # the same as i2's id
i2.object_id  # the same as i1's id

# Not immediate values
s1 = "1"
s2 = "1"
s1.object_id  # NOT the same as s2's id
s2.object_id  # NOT the same as s1's id

float seem also be immediate values:

f1 = 2.1
f2 = 2.1
f1.object_id  # the same as f2's id
f2.object_id  # the same as f1's id

However, I cannot find any reference that float are immediate values. Are float immediate values in Ruby? How do float work in Ruby?

2 Answers

The concept of immediate value does not exist in Ruby

Now, you might ask "Why does the Ruby FAQ talk about immediate values when Ruby does not have immediate values?" The answer to this question is that, unfortunately, a lot of documentation in the Ruby community mixes up the concept of the programming language called "Ruby" and one of the many Ruby implementations, which unfortunately is also often called "Ruby", although its "official" name is "YARV".

If you check for example the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification, you will find no mention of immediate values, neither under that name, nor a similar concept under any other name. They simply don't exist.

Note also the following caveat in the section about immediate values you linked to:

This section or parts of it might be out-dated or in need of confirmation.

So, we have two problems:

  1. This section talks only about one specific implementation of Ruby, out of many, but it does not make that clear, and
  2. Even the implementation-specific information is outdated.

In current versions of YARV, Fixnum no longer exists. In the past, Integer was an abstract class with two concrete subclasses: Fixnum and Bignum. Fixnums were immediate values, Bignums were not.

However, depending on whether you were running in 32 bit or 64 bit, the exact same literal could be either a Fixnum, thus an immediate value, or a Bignum. On 32 bit Fixnum could store 31 bit and on 64 bit Fixnum could store 63 bit. Note that this only applies to YARV, though, e.g. JRuby would store 64 bit (not only 63 bit) both on 32 and 64 bit platforms.

Nowadays, Fixnum and Bignum no longer exist, and Integer is a concrete class, not an abstract class. However, the optimization is actually still being performed: YARV still optimizes 31 bit Integers as fixnums on 32 bit systems and 63 bit Integers on 64 bit systems, JRuby still optimizes 64 bit Integers.

JRuby has optimized Floats for a very long time. In JRuby, Floats are immediate values. In YARV, however, there was no such optimization. Floats were not immediate values. Recently, however, YARV has introduced the concept of flonums. Flonums are Floats that fit into 62 bit. Flonums are immediate values. Also, this optimization is only performed on 64 bit platforms, flonums are completely disabled on 32 bit platforms.

Note also that the FAQ completely ignores Symbols which are immediate values in many implementations, including YARV.

So, to answer your question:

Are float immediate values in Ruby?

No. Maybe. Some of them, sometimes.

No, Floats are not immediate values in Ruby, if by "Ruby" you mean "the Ruby Programming Language". The Ruby Language has no concept of immediate values.

Maybe, Floats are immediate values in Ruby, maybe not, if by "Ruby" you mean "the set of all existing Ruby implementations". There are some implementations where under some circumstances, some Float values may or may not be immediate values.

Some Floats are sometimes immediate values, if by "Ruby" you mean "YARV". On 64 bit platforms, a subset of Floats are immediate values, but not all of them. On 32 bit platforms, none are immediate values.

It is irrelevant

You can only figure out whether a value is immediate or not by mutating it. However, all values for which such optimizations are performed, are immutable, can't have instance variables, and can't have singleton methods or a singleton class.

Therefore, it is actually impossible to tell whether something is an immediate value or not. It is a private internal optimization detail.

That means you can simply ignore the concept altogether since there is no way it can make your program run differently.

What about object_id?

First off: Object#object_id is an abomination. It should not exist. Simply forget you ever heard about it.

It violates one of the fundamental principles of object orientation, namely that an object that simulates another object should be indistinguishable from that object. (The same applies to BasicObject#equal?.)

But secondly, here is a counter-example:

# frozen_string_literal: true

# Not immediate values
s1 = '1'
s2 = '1'

s1.object_id
#=> 60
# SAME as `s2`'s id

s2.object_id
#=> 60
# SAME as `s1`'s id

Oops. I broke your test: frozen String literals are not immediate objects, but they are automatically de-duplicated.

In YARV, objects with immediate value are closely related to their object_id which resembles their internal VALUE. (see Creating Extension Libraries for Ruby)

An object with "immediate value" is fully encoded within its VALUE. As a consequence, such object has the same object_id in every Ruby process. (using the same Ruby binary)

From within Ruby, calling ObjectSpace._id2ref is a way to retrieve these objects: (at some point, if you keep looping, this will also enumerate non-immediate objects from the core and stdlib)

0.upto(31) do |i|
  printf('%08b  ', i)
  begin
    obj = ObjectSpace._id2ref(i)
    printf('%-10s %s', obj.class, obj.inspect)
  rescue RangeError
    print('-')
  end
  puts
end

Output: (on a 64-bit machine)

00000000  FalseClass false
00000001  Integer    0
00000010  Float      2.0
00000011  Integer    1
00000100  -
00000101  Integer    2
00000110  Float      -2.0
00000111  Integer    3
00001000  NilClass   nil
00001001  Integer    4
00001010  Float      2.0000000000000004
00001011  Integer    5
00001100  -
00001101  Integer    6
00001110  Float      -2.0000000000000004
00001111  Integer    7
00010000  -
00010001  Integer    8
00010010  Float      2.000000000000001
00010011  Integer    9
00010100  TrueClass  true
00010101  Integer    10
00010110  Float      -2.000000000000001
00010111  Integer    11
00011000  -
00011001  Integer    12
00011010  Float      2.0000000000000013
00011011  Integer    13
00011100  -
00011101  Integer    14
00011110  Float      -2.0000000000000013
00011111  Integer    15

You can see 3 types of objects:

  1. false, true and nil

    00000000  FalseClass false
    00001000  NilClass   nil
    00010100  TrueClass  true
    
  2. Integers:

    00000001  Integer    0
    00000011  Integer    1
    00000101  Integer    2
    00000111  Integer    3
    
  3. Floats:

    00000010  Float      2.0
    00000110  Float      -2.0
    00001010  Float      2.0000000000000004
    00001110  Float      -2.0000000000000004
    

On 64-bit machines, the object_id is an unsigned long which can hold 64 bits.

The object_id for integers ends with binary 1, so there are 63 bits left to represent the integer value which covers the range:

-4611686018427387904..4611686018427387903

Integers outside that range become non-immediate objects.

The object_id for floats ends with binary 10, so there are 62 bits left to encode the float value, which covers ¼ of all (double precision) floats. Internally these are called "flonum".

YARV introduced flonums in Ruby 2.0 on 64-bit machines.

Related