How Numbers Work in Ruby: Understanding Integers, Floats & Bigdecimal

Ruby 2.4 merged Fixnum & Bignum into the same class (Integer) so I think this is a good time to review the different number types in Ruby!

That’s what we are going to talk about in this post 🙂

An Overview of Number Types

Let’s start by taking a look at the class hierarchy of all the number related classes in Ruby:

Numeric
  Integer
    Fixnum
    Bignum
  Float
  Complex
  Rational
  BigDecimal (Standard Library)

As you can see, the Numeric class is the parent for all the number classes. Remember that you can use the ancestors method to discover the parent classes for any class.

Example:

Fixnum.ancestors - Fixnum.included_modules

[Fixnum, Integer, Numeric, Object, BasicObject]

Now let’s see these classes in table form:

Class Description Example
Integer Parent class of Fixnum & Bignum 1
Fixnum Whole numbers that fit into the OS integer type (32 or 64 bits) 1
Bignum Used for bigger numbers 111111111111
Float Imprecise decimal numbers 5.0
Complex Used for math stuff with imaginary numbers (1+0i)
Rational Used to represent fractions (2/3)
BigDecimal Perfect precision decimal numbers 3.0

Float Imprecision

The Float class in Ruby is described as “imprecise” in the official Ruby documentation.

Why is that?

Let me show you an example:

0.2 + 0.1 == 0.3
# false

Why is this false?

Let’s look at the result of 0.2 + 0.1:

0.30000000000000004

Exactly! That’s what we mean by imprecision.

This happens because of the way that a float is stored. If you need decimal numbers that are always accurate you can use the BigDecimal class.

Float vs BigDecimal

BigDecimal is a class that gives you arbitrary-precision decimal numbers.

Example:

require 'bigdecimal'

BigDecimal("0.2") + BigDecimal("0.1") == 0.3
# true

Why don’t we always use BigDecimal then? Because it’s a lot slower!

Here is a benchmark:

Calculating -------------------------------------
          bigdecimal    21.559k i/100ms
               float    79.336k i/100ms
-------------------------------------------------
          bigdecimal    311.721k (± 7.4%) i/s -      1.552M
               float      3.817M (±11.7%) i/s -     18.803M

Comparison:
               float:  3817207.2 i/s
          bigdecimal:   311721.2 i/s - 12.25x slower

BigDecimal is 12 times slower than Float, and that’s why it’s not the default 🙂

Fixnum vs Bignum

In this section, I want to explore the differences between Fixnum and Bignum before Ruby 2.4.

Let’s start with some code:

1.class
# Fixnum

100000000000.class
# Bignum

Ruby creates the correct class for us, and it will automatically promote a Fixnum to a Bignum when necessary.

Note: You may need a bigger number to get a Bignum object if you have a 64-bit Ruby interpreter.

Why do we need different classes? The answer is that to work with bigger numbers you need a different implementation, and working with big numbers is slower, so we end up with a similar situation to Float vs BigDecimal.

Special Attributes of Fixnums

The Fixnum class also has some special properties. For example, the object id is calculated using a formula.

1.object_id
# 3
20.object_id
# 41

The formula is: (number * 2) + 1.

But there is more to this, when you use a Fixnum there is no object being created at all. There is no data to store in a Fixnum, because the value is derived from the object id itself.

This is just an implementation detail, but I think it’s interesting to know 🙂

MRI (Matz’s Ruby Interpreter) uses these two macros to convert between value & object id:

INT2FIX(i)  ((VALUE)(((SIGNED_VALUE)(i))<<1 | FIXNUM_FLAG))
FIX2LONG(x) ((long)RSHIFT((SIGNED_VALUE)(x),1))

What happens here is called “bit shifting”, which moves all the bits to the left or the right.

Shifting one position to the left is equivalent to multiplying by 2 & that’s why the formula is (number * 2) + 1. The +1 comes from the FIXNUM_FLAG.

In contrast, Bignum works more like a normal class & uses normal object ids:

111111111111111.object_id
# 23885808

All this means is that Fixnum objects are closer to symbols in terms of how they work at the interpreter level, while Bignum objects are closer to strings.

Integers In 2.4

Since Ruby 2.4 Fixnum & Bignum are deprecated, but behind the scenes they still work the same way.

Ruby switches from one type to another automatically.

Without changing the class.

This means that small Integer numbers still operate in the same way as a Fixnum.

Summary

In this post, you learned about the different number-related classes that exist in Ruby.

You learned that floats are imprecise & that you can use BigDecimal if accuracy is a lot more important than performance. You also learned that Fixnum objects are special at the interpreter level, but Bignums are just regular objects.

If you found this post interesting don’t forget to sign-up to my newsletter in the form below 🙂

2 thoughts on “How Numbers Work in Ruby: Understanding Integers, Floats & Bigdecimal”

    • I used the benchmark-ips gem:

      require 'benchmark/ips'
      require 'bigdecimal'
      
      Benchmark.ips do |x|
        x.report("bigdecimal") { BigDecimal("0.2") + BigDecimal("0.1") }
        x.report("float") { 0.2 + 0.1 }
      
        x.compare!
      end
      

Comments are closed.