Efficient Ruby Coding: A Guide to Immediate and Non-Immediate Objects

alexandrecalaca

Alexandre Calaça

Posted on July 18, 2024

Efficient Ruby Coding: A Guide to Immediate and Non-Immediate Objects

Introduction

Memory management is a critical aspect of any programming language, and Ruby is no exception.

In Ruby, understanding how objects are stored and managed in memory can help developers write more efficient and performant code.

Take the time to know your craft; by doing so, you'll be able to build robust and efficient solutions while carefully considering trade-offs.


Objective

The objective of this article is to explore the differences between immediate and non-immediate objects in Ruby, focusing on how they are stored in memory and how this affects their behavior and performance.

By the end of this article, you should have a better understanding of:

  • What immediate and non-immediate objects are
  • How memory allocation works for these objects
  • The performance and mutability differences between them

Immediate and non-immediate objects

Memory allocation

Immediate objects

Immediate objects are a special category of objects that are not allocated on the heap but are represented directly within the Ruby interpreter's internal data structures.

Since Ruby keeps them right where it can access them quickly, this makes them faster to use because Ruby doesn't have to go looking for them in the usual storage areas.

The main immediate objects in Ruby include:

  • Fixnums: Small integers (on a 64-bit system, integers within the range -2^62 to 2^62-1).

  • Symbols: Interned strings used mainly as identifiers.

  • True, False, and Nil: The singleton objects for true, false, and nil.

These objects are typically small and frequently used, allowing for more efficient memory management and faster access.

For these objects, the Ruby interpreter does not store a reference to a memory location where the object resides; instead, the object's value is encoded directly within the reference itself.

Let's take integers as an example:

a = 24
b = 24
a.object_id
=> 49
b.object_id
=> 49
Enter fullscreen mode Exit fullscreen mode

As you can see, Ruby does not create a new object or memory space for each integer, but reuses the same internal representation.

Since the value 24 is the same, the object_id is also the same.

Ruby code


Non-immediate objects

Non-immediate objects, on the other hand, are allocated on the heap. These objects are accessed via references (pointers) that point to their memory location. Most Ruby objects fall into this category.

Most Ruby objects fall into this category, including:

  • Strings: Mutable sequences of characters.

  • Arrays: Ordered collections of objects.

  • Hashes: Key-value pairs.

  • Objects of custom classes: Instances of user-defined or library-defined classes.

  • Bignums: Integers that exceed the size limit of Fixnums and are stored as larger data structures.

These are larger objects that store references to their data. They require additional memory allocation and management overhead.

Since we used integer before, we can compare with strings now, we noticed that two variables with the same integer value have the same object_id.

Let's check if two strings with the same value have the same object_id:

a = "value"
b = "value"
a.object_id
=> 280
 b.object_id
=> 300
Enter fullscreen mode Exit fullscreen mode

Even with the same string values, the object_id is different for a and b.

Ruby Irb Code


Mutability

Immediate objects

Immediate object are also called immutable objects. Let's see why. Our first example was with integers (fixnum), let's go with symbols now.

s1 = :my_symbol
s1.is_a? Symbol
=> true
s1 << :new_symbol
Enter fullscreen mode Exit fullscreen mode

Since it's not possible to modify the original my_symbol value, we'll get a NoMethodError

Ruby symbol irb code

The point is that you cannot change their value once it is created.

You cannot modify the immediate object's valuee; you can only reassign the variable to a new integer.


Non-immediate objects

Mutable. Their values can be modified after creation.

Remember, our basic list of non-immediate objects is Array, Hashes, Strings, BigNums and Custom objects.

This time, let's use an array to test

irb(main):131:0> a1 = [1, 2, 4]
=> [1, 2, 4]
a1.is_a? Array
=> true
1.object_id
=> 340
a1 << 5
=> [1, 2, 4, 5]
a1.object_id
=> 340
Enter fullscreen mode Exit fullscreen mode

As you can see, you can easily modify a non-immediate object.

![Rubyhttps://dev-to-uploads.s3.amazonaws.com/uploads/articles/2e0yey05wrge35ck1znn.png)


Comparison

Immediate objects

Comparisons are done by value. Two immediate objects with the same value are considered equal.

It's important to mention that immediate objects represent their value directly, besides, they are unique and don't have distinct instances with the same value.

Let's use true to test immediate objects.

a1 = true
b1 = true

a1 == b1
=> true
a1.equal?(b1)
=> true
Enter fullscreen mode Exit fullscreen mode

When we compare with ==, we're basically using the value/content as the topic of comparasion.

When it comes to the equal? method, it checks if it's the same object, it checks if both have the same address in memory.

It checks for the object's identity.

Since immediate objects are not allocated in the heap and have the same object_id, the return is true.

Image description


Non-immediate objects

Comparisons are done by reference. Two non-immediate objects are considered equal if they refer to the same object in memory.

The == (equality operator) is usually overridden in classes to provide a "correct" value-based comparasion.

Let's have a look:

a1 = {my_key: "my_value", new_key: 2}
b1 = {my_key: "my_value", new_key: 2}

a1 == b1
=> true
Enter fullscreen mode Exit fullscreen mode

Remember the hashes are non-immediate objects. In the comparasion with ==, we're checking the content of the objects, that's why the return is true.

Ruby code

Now, when it comes to the equal? method with non-immediate objects, it'll be only true if it's the exact object in memory.

By definition, we know that non-immediate objects don't share the same memory location.

a1 = {my_key: "my_value", new_key: 2}
b1 = {my_key: "my_value", new_key: 2}

a1.object_id
=> 360
b1.object_id
=> 380
a1.equal?(b1)
=> false
Enter fullscreen mode Exit fullscreen mode

As you can see, a1 and b1 are return true with == because they have the same content/value, however they return false with equal? because they are different objects.

Ruby non-immediate objects comparasion


Performance:

Immediate objects

These are small, fixed-size objects that store their data directly within the object reference itself.

They are stored on the stack and are very efficient in terms of memory usage.


Non-immediate objects

These are larger objects that store references to their data, typically stored on the heap.

They require additional memory allocation and management overhead.


Results

In order to check the results, we'll use the Benchmark.realtime method, which is used to capture the elapsed time for the current block of code. It returns in seconds.

Let's take a look at the results:

require 'benchmark'

num1 = 42
num2 = 43

immediate_time = Benchmark.realtime do
  9_000_000.times do
    num1 + num2
  end
end

str1 = 'hello'
str2 = 'world'

non_immediate_time_strings = Benchmark.realtime do
  9_000_000.times do
    str1 + str2
  end
end

arr1 = [1, 2, 3]
arr2 = [4, 5, 6]

non_immediate_time_arrays = Benchmark.realtime do
  9_000_000.times do
    arr1 + arr2
  end
end

performance_ratio_strings = non_immediate_time_strings / immediate_time
performance_ratio_arrays = non_immediate_time_arrays / immediate_time

# Printing the results
puts "Immediate objects (Integers) are #{performance_ratio_strings.round(4)} times faster than non-immediate objects (Strings)"
puts "Immediate objects (Integers) are #{performance_ratio_arrays.round(4)} times faster than non-immediate objects (Arrays)"

Enter fullscreen mode Exit fullscreen mode

If you check, you might have something around this

benchmark1


Done


Conclusion

Understanding the distinctions between immediate and non-immediate objects in Ruby is essential for writing efficient and performant code.

By grasping the concepts provided in the article, developers can make better decisions about how to structure and manipulate data in Ruby, ultimately leading to more robust and efficient applications.


Celebrate

The it crowd description


Reach me out

Github
LinkedIn
Twitter
Dev.to
Youtube


Final thoughts

Thanks for reading this article.

If you have any questions, thoughts, suggestions, or corrections, please share them with me.

I definitely appreciate your feedback and look forward to hearing from you.

Feel free to suggest topics for future blog articles. Until next time!


💖 💪 🙅 🚩
alexandrecalaca
Alexandre Calaça

Posted on July 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related