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
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.
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
Even with the same string values, the object_id
is different for a
and b
.
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
Since it's not possible to modify the original my_symbol
value, we'll get a NoMethodError
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
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
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
.
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
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
.
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
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.
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)"
If you check, you might have something around this
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
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!
Top comments (0)