Ruby Numbers and the Memory: how I got confused.

I was suddenly doing a simple task to generate a bunch of random integers. There were no strict requirements, so I was thinking about an upper bound I could put. My gut advised me to come up with a number that could represent various numbers at a reasonable cost. By cost here, I mean memory and performance.

Ruby Official logo

My first attempt was 2⁶³-1 as a signed 8 bytes integer (1 bit for a sign and 63 bit for the number itself). Then, I checked .size to ensure it takes 8 bytes only.

You may start arguing here about the .size method on integers. We will come back to this later.

For a reason I don’t remember, I decided to find when Ruby would start returning 9 bytes, and, surprisingly, for me, it wasn’t 2⁶³.

The first number is 2⁶⁴.

For a second, I thought: okay, maybe it’s stored like an unsigned 8 bytes integer (all 8 bytes for the number itself). But checking how many bytes would take a negative number killed that idea.

How can Ruby fit 8 bytes of data and a sign into 8 bytes?

The short answer: it can’t.

And now goes the long answer.

First of all, the size method depends on the machine.

But that’s not all. As we know, everything in Ruby is an object: including numbers. Some objects may have different behavior than others due to internal implementation. For example, Symbols and Fixnums (now with the Bignum part of the Integer class) are special.

Having this in mind, Integer#size doesn’t return the full size of the object but only its number part.

To get the actual size of the object, we should use ObjectSpace.memsize_of instead.

Now, the size is 40 bytes, equal to the RValue struct size that Ruby allocates to store objects.

You can read more about the memory internals of Ruby here.

What would be the size of 2⁶³-1 in Ruby’s object space? Let’s find out.

Okay, still 40 bytes. That means my initial gut feeling didn’t bring me anywhere. After slightly decreasing the power number, we can end up with the new number.

2⁶²-1 gives 0 bytes in the object space!

That means Ruby VM doesn’t allocate extra memory for every number (from -2⁶² to 2⁶²-1). It stores the number in its reference pointer, which takes 8 bytes only.

Why is it important to know?

There are two main reasons: performance and memory consumption.

Let’s start with performance first.

Operating with Bignum (behind the scene, such Integer numbers are still Bignum ) is slower.

We can check this with the following simple benchmark.

And the results.

As we can see, operating with Fixnum (8 bytes only integers) is almost four times faster on addition operation. I expect a more considerable difference in more complex math operations.

Gladly or not, Ruby developers rarely need to think about memory issues due to Garbage Collector’s (GC) work. Unfortunately, sometimes this freedom from memory control can hit us.

For example, Ruby rarely can free the memory back to the system, which means we would rather avoid memory bloats. Mostly, memory bloats are hard to deal with, so why not eliminate potential issues when we can do it for no cost?

In my case, creating millions of random numbers in a few seconds that would take place in object space wasn’t perfect. So having bounds between -2⁶² and 2⁶²-1 for a random number would make the memory consumption smoother.

Thank you for your time, and follow me for more topics about #ruby and #rails.



Writing about Software Engineering and Self-Development. Stay tuned and grow your expertise. Let's connect on social media.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Evgeniy Demin

Writing about Software Engineering and Self-Development. Stay tuned and grow your expertise. Let's connect on social media.