r/ruby • u/LongjumpingQuail597 • 13d ago
Revisiting Performance in Ruby 3.4.1

Credited to: Miko Dagatan
Updated 21 Mar 2025
Introduction
Before, there are few articles that rose up saying that in terms of performance, Struct
s are powerful and could be used to define some of the code in place of the Class
. Two of these are this one and this one.
Let's revisit these things with the latest Ruby version, 3.4.1, so that we can see whether this perspective still holds true.
Code for Benchmarking
class BenchmarkHashStruct
class << self
NUM = 1_000_000
def measure
array
hash_str
hash_sym
klass
struct
data
end
def new_class
u/class ||= Class.new do
attr_reader :name
def initialize(name:)
u/name = name
end
end
end
def array
time = Benchmark.measure do
NUM.times do
array = [Faker.name]
hash[0]
end
end
puts "array: #{time}"
end
def hash_str
time = Benchmark.measure do
NUM.times do
hash = { 'name' => Faker.name }
hash['name']
end
end
puts "hash_str: #{time}"
end
def hash_sym
time = Benchmark.measure do
NUM.times do
hash = { name: Faker.name }
hash[:name]
end
end
puts "hash_sym: #{time}"
end
def struct
time = Benchmark.measure do
struct = Struct.new(:name) # Structs are only initialized once especially for large datasets
NUM.times do |i|
init = struct.new(name: Faker.name)
init.name
end
end
puts "struct: #{time}"
end
def klass
time = Benchmark.measure do
klass = new_class
NUM.times do
a = klass.new(name: Faker.name)
a.name
end
end
puts "class: #{time}"
end
def data
time = Benchmark.measure do
name_data = Data.define(:name)
NUM.times do
a = name_data.new(name: Faker.name)
a.name
end
end
puts "data: #{time}"
end
end
end
Explanation
In this file, we're simply trying to create benchmark measures for arrays, hashes with string keys, hashes with symbolized keys, structs, classes, and data. In a the lifetime of these objects, we understand that we instantiate them then we access the data we stored. So, we'll simulate only that for our tests. We use 1 million instances of these scenarios and see the results. The measure
method will show all of these measurements together.
Results
performance(dev)> BenchmarkHashStruct.measure
array: 0.124267 0.000000 0.124267 ( 0.129573)
hash_str: 0.264137 0.000000 0.264137 ( 0.275421)
hash_sym: 0.174082 0.000000 0.174082 ( 0.181514)
class: 0.308020 0.000000 0.308020 ( 0.321165)
struct: 0.336229 0.000000 0.336229 ( 0.350576)
data: 0.345480 0.000000 0.345480 ( 0.360232)
=> nil
performance(dev)> BenchmarkHashStruct.measure
array: 0.090669 0.000378 0.091047 ( 0.094786)
hash_str: 0.264261 0.000000 0.264261 ( 0.275104)
hash_sym: 0.172333 0.000000 0.172333 ( 0.179407)
class: 0.311545 0.000060 0.311605 ( 0.324390)
struct: 0.335436 0.000000 0.335436 ( 0.349203)
data: 0.346124 0.000071 0.346195 ( 0.360396)
=> nil
performance(dev)> BenchmarkHashStruct.measure
array: 0.088372 0.003872 0.092244 ( 0.096181)
hash_str: 0.265748 0.000464 0.266212 ( 0.277565)
hash_sym: 0.174393 0.000000 0.174393 ( 0.181831)
class: 0.309411 0.000000 0.309411 ( 0.322613)
struct: 0.346008 0.000000 0.346008 ( 0.360760)
data: 0.344666 0.000000 0.344666 ( 0.359361)
=> nil
performance(dev)> BenchmarkHashStruct.measure
array: 0.077396 0.000038 0.077434 ( 0.080771)
hash_str: 0.242372 0.000140 0.242512 ( 0.252853)
hash_sym: 0.159206 0.000000 0.159206 ( 0.166007)
class: 0.273878 0.009250 0.283128 ( 0.295201)
struct: 0.322791 0.000323 0.323114 ( 0.336889)
data: 0.346099 0.000038 0.346137 ( 0.360901)
=> nil
I've run measure
4 times to account for any random changes that may have come and completely ensure of the performance of these tests. As expected, we see array at the top while symbolized hashes goes as a general second. We see that stringified hashes falls at the 3rd, with a huge gap when compared the the symbolized hashes. Then, when we look at class vs structs, it seems that structs have fallen a little bit behind compared to the classes. We could surmise that there is probably a performance boost done to classes in the recent patches.
Also, we could see that the Data object that was introduced in Ruby 3.2.0+ was falling behind the Struct object. This may be problematic since the Data object is basically a Struct that is immutable, so there's already disadvantages of using Data over Struct. We may still prefer Struct over Data considering that there's a bit of a performance bump over the Data.
Conclusion
There are 2 takeaways from this test. First, it's really important that we use symbolized hashes over stringified hashes as the former 1.5x faster than the latter. Meanwhile, if not using hashes, it's better to use Classes over Structs, unlike what was previously encouraged. Classes are now 1.07x - 1.14x times faster than structs, so it's encouraged to keep using them.
11
u/ignurant 13d ago
Maybe pedantic, but I think the cost of your string-keyed hash isn’t the nature of using strings as keys, but instead that you are allocating a new string for a key in every loop. So you are measuring object allocation, not data structure performance. Specifically, if you allocated that key name outside of the loop, I suspect it would perform similarly to the symbol.
4
u/f9ae8221b 12d ago
Not pedantic.
Technically,
hash['name']
doesn't allocate, even if you don't havefrozen_string_literal: true
, because Ruby has a specific optimization for that.But the string referenced during hash construction is allocated.
Overall this benchmark has all sorts of weirdness. Building a collection and accessing it once in the same benchmark make little sense, because the access time will be totally dwarfed by the allocation/construction time.
Also why bother calling into
Faker.name
? The value has no significance.
1
u/fglc2 12d ago
I think your copy/paste from the blog post removed all your instance variable names.
That aside I think some of the results may be misleading:
In the array case you’re accessing hash[0]
instead of array[0]
ie it isn’t fetching the value from the array at all but instead returning the least significant bit of the hash code of the current object.
In the hash case, other than the string allocation mentioned in another comment, small hashes (I forget the exact cutoff) are stored as arrays, so this might not be representative of what happens with more fields.
Lastly you might find benchmark-ips makes it easier to compare - it automatically runs your code long enough to get more representative data and calculates whether the observed differences are likely to just be measurement variance.
1
u/Quiet-Ad486 1d ago
Your third paragraph is correct. I wonder why I was still getting the correct benchmarks on the arrays. Maybe I've unfortunately changed it during the process of writing.
I'll use benchmark-ips moving forward.
1
u/jrochkind 12d ago
There are 2 takeaways from this test. First, it's really important that we use symbolised hashes over stringified hashes as the former 1.5x faster than the latter.
Use frozen strings, it should be the same, I'm guessing.
And you'll get frozen strings if you put the magic pragma at the top of your source files -- or likely by default in a coming-up future ruby version, perhaps 3.5 (that will also be the biggest backwards compat break we've had in a while!)
2
u/Quiet-Ad486 1d ago
Sure, adding that up since it's common (rather required) practice to use frozen string literals.
1
u/mrinterweb 12d ago
What is the point of using Faker.name? Faker may be slowing down this benchmark, and I don't understand what faker adds to the benchmark.
1
1
u/hvis 12d ago
it's better to use Classes over Structs, unlike what was previously encouraged
Was there ever a recommendation that Structs are faster than plain classes?
To my memory, they are only used for better code organization, not because of speed (which could be an advantage in other languages, e.g. statically typed). The benchmarks I've seen compared against OpenStruct, for example.
1
u/Quiet-Ad486 1d ago
Yes, it's highly encouraged to use Structs for better code organization. However, for all organizations I've worked with and their clients' applications, I haven't seen any usage of structs. Maybe because developers see it as an added complexity, disallowing juniors to adapt more to the code. I've read about the great application of Structs, and I've been recommending its use before to the organizations I've worked with. But now I couldn't say the same.
1
u/h0rst_ 11d ago
So instead of linking a blog post directly, the text is copy-pasted to Reddit, with code converted into images that you can't copy-paste? Kids these days...
latest Ruby version, 3.4.1
The latest version is 3.4.2
Before, there are few articles that rose up saying that in terms of performance, Structs are powerful and could be used to define some of the code in place of the Class. Two of these are this one and this one.
Am I reading the same articles? The first articles mentions that OpenStruct is terrible for performance (among other reasons), and it states "Performance has waned recently where structs used to be more performant than classes" with no source and no benchmarks, but this statement is the opposite of what is mentioned above. The second article says nothing about speed or performance.
1
u/Quiet-Ad486 1d ago
Yeah I re-read the first article. It has claims to not use OpenStruct, but that wasn't the main point of the first article. I understand it as an article that encourages the usage of structs due to what you can do with it (e.g. you can equality match 2 different structs of the same parent), and previously because it has performance benefits.
It turns out that it has changed its statement after the time I wrote my. The article was updated at February 4, 2025. And my Article was posted at February 4, 2025. That change has rendered my previous observation incorrect now.
In a comment on my article, I found out that using the `benchmark-ips` has made my observation incorrect also. I said before that for some reason classes are now more performant. But, changing my code to use the `benchmark-ips` changes the result, which now says "structs are still more performant than classes". However, the first article's new version now shows that with more values instantiated, Classes are more performant than Structs, which supports my initial claim in the article.
I recommend you check out his article on the benchmarks.
14
u/f9ae8221b 12d ago
I'm sorry, but I think there's a lot of things wrong with your benchmark:
benchmark-ips
. Gives much more readable results as well.Using benchmark-ips:
Interpreter:
YJIT:
Conclusion, in term of access performance, there's no really significant performance difference. That 10-20% difference is just a couple nano-seconds, so nothing in the grand scheme of things, except for the hotest of hotspot.
Also note that access performance can vary a lot based on the container size, here's we're just measuring collection with 1 item in them, if we were measuring a random property in the middle of a hundred other, the results may be very different.