Re-Revisiting Performance in Ruby 3.4.1
Credited to: Miko Dagatan Introduction I've had my article, Revisiting Performance in Ruby 3.4.1, published and have received various reactions regarding it through this reddit page. I would say I'm very thankful for those who have provided their feedback so that I could improve on benchmarking code and presenting my observations. There are 3 points that have come to importance from all the feedback: The articles (the first, which is an Alchemist article and second, which is a medium article) I provided do not support my past observation that "Structs are powerful and could be used to define some of the code in place of the Class" Use benchmark-ips to better benchmark the code I'm benchmarking. My new conclusion that Classes are now faster than Structs holds false when I use benchmark-ips I understand these points challenge my observations and I would like to further dive deeper to support my initial findings. Past observation: Structs are powerful and could be used to define some of the code in place of the Class I've been reading articles and comments that claim Structs could be used instead of other code. Some said in place of Hashes, some said in place of Classes. Structs provide structure, organisation, and readability to your data so it's better to use instead of Hashes in that regard. This Stack Overflow comment said that structs are better than hashes. This Stack Overflow comment said that in ruby 3.0.0p0, structs are faster than hashes. This Stack Overflow comment points out the real advantage of Structs in 2015 and that is it's faster. This GitHub gist that compares hashes, ostructs, and structs shows the initialisation (write-only) of data. Hashes are faster in here, but there's still a clear advantage of using Structs. This Crystal forum page shows that in 2020, structs are faster than classes. Things like these, practically performance comparisons with classes, are what appealed to me. This Honeybadger article's point is to not use Open Structs. However, it also shows in the benchmarks that Structs were faster compared to the classes. So, there you go. I've added more links to help give a general understanding of what I understood the majority claims in previous years, that Structs are faster than Classes, and it's great to make use of it as much as possible when your coding situation permits it. The Alchemist article provides a great explanation on when to use it. Should have checked three times! In my previous article I've claimed that throughout the years, Ruby may have improved Classes to the point that in certain cases they are faster than Structs. When I initially tested it, I was shocked to find it out, and was very excited to share it to the world. I made adjustments to the benchmarks to ensure that I'm definitely seeing this correctly. Then I've put the article for the world to see. One of the first comments in the Reddit thread was a suggestion to use benchmark-ips, and that my code should separate the reads and the writes. I followed his advice on the benchmark-ips but while trying to retain my code (to explain later), and what do you know? Turns out that Struct is still faster than Classes. I've been wrong about it! I guessed that I should have probably checked three times before! Here's the result when using the benchamrk-ips to my benchmarking code. attr_reader is the Class object. ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24] Calculating ------------------------------------- array 27.014M (± 1.6%) i/s (37.02 ns/i) - 136.720M in 5.062568s sym_hash 21.751M (± 2.4%) i/s (45.98 ns/i) - 110.675M in 5.091684s str_hash 20.719M (± 4.6%) i/s (48.27 ns/i) - 105.263M in 5.094066s attr_reader 7.954M (± 1.0%) i/s (125.72 ns/i) - 40.392M in 5.078593s struct 10.973M (± 1.7%) i/s (91.13 ns/i) - 54.974M in 5.011294s data 6.813M (± 1.3%) i/s (146.77 ns/i) - 34.326M in 5.038833s Comparison: array: 27013631.8 i/s sym_hash: 21750676.4 i/s - 1.24x slower str_hash: 20718679.0 i/s - 1.30x slower struct: 10973472.4 i/s - 2.46x slower attr_reader: 7954235.5 i/s - 3.40x slower data: 6813492.5 i/s - 3.96x slower An unbelievable twist! There was a comment that came about in the Reddit thread. I've already spent days trying to grind at my job. So I forgot to check on it. The commenter said "Am I reading the same articles? The first(Alchemist) 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" It was odd for me because I definitely understood that the articles I referenced are promoting the use of Structs and support my understanding that the general opini

Credited to: Miko Dagatan
Introduction
I've had my article, Revisiting Performance in Ruby 3.4.1, published and have received various reactions regarding it through this reddit page. I would say I'm very thankful for those who have provided their feedback so that I could improve on benchmarking code and presenting my observations.
There are 3 points that have come to importance from all the feedback:
- The articles (the first, which is an Alchemist article and second, which is a medium article) I provided do not support my past observation that
"Structs are powerful and could be used to define some of the code in place of the Class"
- Use
benchmark-ips
to better benchmark the code I'm benchmarking. - My new conclusion that
Classes are now faster than Structs
holds false when I usebenchmark-ips
I understand these points challenge my observations and I would like to further dive deeper to support my initial findings.
Past observation: Structs are powerful and could be used to define some of the code in place of the Class
I've been reading articles and comments that claim Structs could be used instead of other code. Some said in place of Hashes, some said in place of Classes. Structs provide structure, organisation, and readability to your data so it's better to use instead of Hashes in that regard.
- This Stack Overflow comment said that structs are better than hashes.
- This Stack Overflow comment said that in ruby 3.0.0p0, structs are faster than hashes.
- This Stack Overflow comment points out the real advantage of Structs in 2015 and that is it's faster.
- This GitHub gist that compares hashes, ostructs, and structs shows the initialisation (write-only) of data. Hashes are faster in here, but there's still a clear advantage of using Structs.
- This Crystal forum page shows that in 2020, structs are faster than classes. Things like these, practically performance comparisons with classes, are what appealed to me.
- This Honeybadger article's point is to not use Open Structs. However, it also shows in the benchmarks that Structs were faster compared to the classes.
So, there you go. I've added more links to help give a general understanding of what I understood the majority claims in previous years, that Structs are faster than Classes, and it's great to make use of it as much as possible when your coding situation permits it. The Alchemist article provides a great explanation on when to use it.
Should have checked three times!
In my previous article I've claimed that throughout the years, Ruby may have improved Classes to the point that in certain cases they are faster than Structs. When I initially tested it, I was shocked to find it out, and was very excited to share it to the world. I made adjustments to the benchmarks to ensure that I'm definitely seeing this correctly. Then I've put the article for the world to see.
One of the first comments in the Reddit thread was a suggestion to use benchmark-ips
, and that my code should separate the reads and the writes. I followed his advice on the benchmark-ips
but while trying to retain my code (to explain later)
, and what do you know? Turns out that Struct is still faster than Classes. I've been wrong about it! I guessed that I should have probably checked three times before!
Here's the result when using the benchamrk-ips
to my benchmarking code. attr_reader
is the Class object.
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
Calculating -------------------------------------
array 27.014M (± 1.6%) i/s (37.02 ns/i) - 136.720M in 5.062568s
sym_hash 21.751M (± 2.4%) i/s (45.98 ns/i) - 110.675M in 5.091684s
str_hash 20.719M (± 4.6%) i/s (48.27 ns/i) - 105.263M in 5.094066s
attr_reader 7.954M (± 1.0%) i/s (125.72 ns/i) - 40.392M in 5.078593s
struct 10.973M (± 1.7%) i/s (91.13 ns/i) - 54.974M in 5.011294s
data 6.813M (± 1.3%) i/s (146.77 ns/i) - 34.326M in 5.038833s
Comparison:
array: 27013631.8 i/s
sym_hash: 21750676.4 i/s - 1.24x slower
str_hash: 20718679.0 i/s - 1.30x slower
struct: 10973472.4 i/s - 2.46x slower
attr_reader: 7954235.5 i/s - 3.40x slower
data: 6813492.5 i/s - 3.96x slower
An unbelievable twist!
There was a comment that came about in the Reddit thread. I've already spent days trying to grind at my job. So I forgot to check on it. The commenter said "Am I reading the same articles? The first(Alchemist) 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"
It was odd for me because I definitely understood that the articles I referenced are promoting the use of Structs and support my understanding that the general opinion is to make use of them when you can over classes and hashes
. So, I re-read both articles, Medium article, which was a faster read, then the Alchemist article. This took a long time, but I enjoyed re-reading it. I noticed that the writer of the article wrote "Performance has waned recently where structs used to be more performant than classes"
in the article, and I was sure that I never read that before. I took a look at when it's last updated. Turns out it got updated after I wrote the article, and the Alchemist article got updated the same day as my previous article. February 4, 2025
That makes sense, now I understand why some readers looked confused in their comments about it.
What strikes me is that the Alchemist article changed its stance to support the claim I made in my previous article! Yes, indeed, my article became thoroughly confusing because of that. However, it's more interesting that the Alchemist article supports my initial claim!
The article's benchmark was great because it has 5 attributes instantiated into the objects. It's closer to real-life use, as we're silly to simply use these different data structures, yet provide only one attribute.
I'll copy the code it provided, but I'll try to add more code into it to provide more scenarios. Let's see how these things fare in 2025.
Why Benchmark both Read & Write?
When benchmarking these objects A reddit user mentioned that it's best to test the read
and write
of the objects in isolation. However, I cannot agree with that as I see in the multitude of codebases I've touched, there's always a write
and there can be more than one read
when using these objects. I prefer to be close to the real life scenarios.
In my previous article's benchmarks, I've only simulated 1:1 read-write benchmarking. But today, I'll double down on this perspective and benchmark 1:1, 2:1, 3:1, 5:1, and 10:1 read-write situations. This will give us a better understanding of the real-life scenarios for these objects.
Benchmarking
We're using the benchmarking code from the Alchemist's article, and we're adding a few more things there. Here's the new code for benchmarking. I've also added a "Hash string" test so that we can also determine the difference between symbolised hashes and stringified hashes (with frozen string literal comment):
Benchmarking Code
##! /usr/bin/env ruby
## frozen_string_literal: true
## Save as `benchmark`, then `chmod 755 benchmark`, and run as `./benchmark`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "benchmark-ips"
gem "debug"
gem "ostruct"
end
Warning[:performance] = false
require "ostruct"
DataDemo = Data.define :a, :b, :c, :d, :e
StructDemo = Struct.new :a, :b, :c, :d, :e
ClassDemo = Class.new do
attr_reader :a, :b, :c, :d, :e
def initialize a:, b:, c:, d:, e:
u/a = a
u/b = b
@c = c
@d = d
@e = e
end
end
puts "--- 1 Read to 1 Write ---"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0] }
benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a] }
benchmark.report("Hash String") { hash = {a: "1", b: "2", c: "3", d: "4", e: "5"}; hash[:a] }
benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a }
benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a }
benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a }
benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a }
benchmark.compare!
end
puts "--- 2 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1] }
benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b] }
benchmark.report("Hash String") { hash = {a: "1", b: "2", c: "3", d: "4", e: "5"}; hash[:a]; hash[:b] }
benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b }
benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b }
benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b }
benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b }
benchmark.compare!
end
puts "--- 3 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1]; arr[2] }
benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b]; hash[:c] }
benchmark.report("Hash String") { hash = {a: "1", b: "2", c: "3", d: "4", e: "5"}; hash[:a]; hash[:b]; hash[:c] }
benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b; data.c }
benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b; struct.c }
benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b; ostruct.c }
benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b; klass.c }
benchmark.compare!
end
puts "--- 5 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1]; arr[2]; arr[3]; arr[4] }
benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e] }
benchmark.report("Hash String") { hash = {a: "1", b: "2", c: "3", d: "4", e: "5"}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e] }
benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b; data.c; data.d; data.e }
benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b; struct.c; struct.d; struct.e }
benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b; ostruct.c; ostruct.d; ostruct.e }
benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b; klass.c; klass.d; klass.e }
benchmark.compare!
end
puts "--- 10 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("Array") { arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; arr[0]; arr[1]; arr[2]; arr[3]; arr[4]; arr[5]; arr[6]; arr[7]; arr[8]; arr[9] }
benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e]; hash[:f]; hash[:g]; hash[:h]; hash[:i]; hash[:j] }
benchmark.report("Hash String") { hash = {a: "1", b: "2", c: "3", d: "4", e: "5", f: "6", g: "7", h: "8", i: "9", j: "10"}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e]; hash[:f]; hash[:g]; hash[:h]; hash[:i]; hash[:j] }
benchmark.report("Data") { data = Data.define(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j).new(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); data.a; data.b; data.c; data.d; data.e; data.f; data.g; data.h; data.i; data.j }
benchmark.report("Struct") { struct = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j).new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); struct.a; struct.b; struct.c; struct.d; struct.e; struct.f; struct.g; struct.h; struct.i; struct.j }
benchmark.report("OpenStruct") { ostruct = OpenStruct.new(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); ostruct.a; ostruct.b; ostruct.c; ostruct.d; ostruct.e; ostruct.f; ostruct.g; ostruct.h; ostruct.i; ostruct.j }
benchmark.report("Class") { klass = Class.new { attr_reader :a, :b, :c, :d, :e, :f, :g, :h, :i, :j; def initialize(a:, b:, c:, d:, e:, f:, g:, h:, i:, j:); @a=a; @b=b; @c=c; @d=d; @e=e; @f=f; @g=g; @h=h; @i=i; @j=j; end }.new(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); klass.a; klass.b; klass.c; klass.d; klass.e; klass.f; klass.g; klass.h; klass.i; klass.j }
benchmark.compare!
end
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
1 read to 1 write
Calculating -------------------------------------
Array 25.764M (± 0.9%) i/s (38.81 ns/i) - 128.815M in 5.000339s
Hash 21.860M (± 0.4%) i/s (45.75 ns/i) - 111.235M in 5.088522s
Hash String 20.215M (± 0.4%) i/s (49.47 ns/i) - 102.154M in 5.053419s
Data 4.158M (± 2.3%) i/s (240.52 ns/i) - 21.125M in 5.083854s
Struct 4.101M (± 1.9%) i/s (243.83 ns/i) - 20.603M in 5.025646s
OpenStruct 122.586k (± 0.7%) i/s (8.16 μs/i) - 616.400k in 5.028558s
Class 4.540M (± 1.7%) i/s (220.25 ns/i) - 22.995M in 5.066432s
Comparison:
Array: 25763513.4 i/s
Hash: 21860209.5 i/s - 1.18x slower
Hash String: 20215108.6 i/s - 1.27x slower
Class: 4540193.3 i/s - 5.67x slower
Data: 4157661.5 i/s - 6.20x slower
Struct: 4101170.2 i/s - 6.28x slower
OpenStruct: 122586.4 i/s - 210.17x slower
2 reads to 1 write
Calculating -------------------------------------
Array 20.633M (± 0.7%) i/s (48.47 ns/i) - 103.215M in 5.002565s
Hash 18.106M (± 1.2%) i/s (55.23 ns/i) - 92.276M in 5.097036s
Hash String 16.850M (± 0.4%) i/s (59.35 ns/i) - 84.474M in 5.013416s
Data 4.088M (± 2.0%) i/s (244.64 ns/i) - 20.519M in 5.021858s
Struct 4.034M (± 1.6%) i/s (247.90 ns/i) - 20.316M in 5.037631s
OpenStruct 120.040k (± 1.0%) i/s (8.33 μs/i) - 605.064k in 5.041019s
Class 4.440M (± 1.5%) i/s (225.21 ns/i) - 22.449M in 5.056871s
Comparison:
Array: 20633383.1 i/s
Hash: 18106481.9 i/s - 1.14x slower
Hash String: 16849875.2 i/s - 1.22x slower
Class: 4440226.6 i/s - 4.65x slower
Data: 4087571.4 i/s - 5.05x slower
Struct: 4033868.2 i/s - 5.12x slower
OpenStruct: 120039.9 i/s - 171.89x slower
3 reads to 1 write
Calculating -------------------------------------
Array 18.320M (± 0.9%) i/s (54.58 ns/i) - 92.829M in 5.067386s
Hash 16.198M (± 0.4%) i/s (61.74 ns/i) - 82.530M in 5.095210s
Hash String 14.845M (± 0.8%) i/s (67.36 ns/i) - 74.947M in 5.048993s
Data 3.993M (± 2.6%) i/s (250.45 ns/i) - 20.235M in 5.071372s
Struct 3.721M (± 8.3%) i/s (268.72 ns/i) - 18.474M in 5.030555s
OpenStruct 109.286k (±16.7%) i/s (9.15 μs/i) - 504.820k in 5.042702s
Class 4.311M (± 1.8%) i/s (231.98 ns/i) - 21.626M in 5.018517s
Comparison:
Array: 18320261.7 i/s
Hash: 16197886.7 i/s - 1.13x slower
Hash String: 14844935.0 i/s - 1.23x slower
Class: 4310699.6 i/s - 4.25x slower
Data: 3992742.9 i/s - 4.59x slower
Struct: 3721375.0 i/s - 4.92x slower
OpenStruct: 109285.6 i/s - 167.64x slower
5 reads to 1 write
Calculating -------------------------------------
Array 15.308M (± 2.2%) i/s (65.32 ns/i) - 77.630M in 5.073563s
Hash 14.129M (±21.2%) i/s (70.78 ns/i) - 64.798M in 4.984178s
Hash String 12.384M (± 1.6%) i/s (80.75 ns/i) - 62.810M in 5.073061s
Data 3.740M (± 2.1%) i/s (267.40 ns/i) - 18.929M in 5.063717s
Struct 3.731M (± 1.6%) i/s (267.99 ns/i) - 18.722M in 5.018610s
OpenStruct 114.473k (± 1.0%) i/s (8.74 μs/i) - 578.442k in 5.053565s
Class 4.142M (± 1.2%) i/s (241.42 ns/i) - 20.902M in 5.046783s
Comparison:
Array: 15308341.0 i/s
Hash: 14129046.7 i/s - same-ish: difference falls within error
Hash String: 12384465.8 i/s - 1.24x slower
Class: 4142199.3 i/s - 3.70x slower
Data: 3739735.6 i/s - 4.09x slower
Struct: 3731458.8 i/s - 4.10x slower
OpenStruct: 114472.7 i/s - 133.73x slower
10 reads to 1 write -- 5 attributes
Calculating -------------------------------------
Array 10.777M (± 0.7%) i/s (92.79 ns/i) - 54.867M in 5.091625s
Hash 8.618M (± 1.2%) i/s (116.04 ns/i) - 43.838M in 5.087596s
Hash String 7.962M (± 1.0%) i/s (125.60 ns/i) - 40.308M in 5.062987s
Data 3.440M (± 1.8%) i/s (290.74 ns/i) - 17.278M in 5.025126s
Struct 3.416M (± 1.2%) i/s (292.75 ns/i) - 17.342M in 5.077662s
OpenStruct 113.327k (± 0.8%) i/s (8.82 μs/i) - 568.500k in 5.016805s
Class 3.576M (± 1.4%) i/s (279.64 ns/i) - 18.189M in 5.087567s
Comparison:
Array: 10776504.7 i/s
Hash: 8617805.7 i/s - 1.25x slower
Hash String: 7962076.7 i/s - 1.35x slower
Class: 3575982.6 i/s - 3.01x slower
Data: 3439532.9 i/s - 3.13x slower
Struct: 3415890.4 i/s - 3.15x slower
OpenStruct: 113326.8 i/s - 95.09x slower
10 reads to 1 write -- 10 attributes
Calculating -------------------------------------
Array 10.683M (± 0.7%) i/s (93.61 ns/i) - 53.429M in 5.001617s
Hash 4.285M (± 1.7%) i/s (233.38 ns/i) - 21.832M in 5.096654s
Hash String 4.063M (± 3.0%) i/s (246.12 ns/i) - 20.582M in 5.070259s
Data 216.697k (± 1.5%) i/s (4.61 μs/i) - 1.096M in 5.058142s
Struct 176.620k (± 2.4%) i/s (5.66 μs/i) - 889.185k in 5.037525s
OpenStruct 58.750k (± 1.7%) i/s (17.02 μs/i) - 298.401k in 5.080777s
Class 197.641k (± 1.8%) i/s (5.06 μs/i) - 993.100k in 5.026439s
Comparison:
Array: 10682769.2 i/s
Hash: 4284848.5 i/s - 2.49x slower
Hash String: 4063024.0 i/s - 2.63x slower
Data: 216697.3 i/s - 49.30x slower
Class: 197641.2 i/s - 54.05x slower
Struct: 176620.1 i/s - 60.48x slower
OpenStruct: 58749.8 i/s - 181.83x slower
Current Observations
That's a lot of benchmarking! I was really hoping that with many reads, that Structs comes out more performant, but it didn't work that way on ruby 3.4.2. But I'm happy with the results. What we can see on the tests with more real-life usages is that Classes are really now more performant than Structs. So, it would be best to use Classes most of the time when dealing with your code. However, Data objects are more performant than Classes and Structs, when we deal with 10 attributes.
Stringified Hashes are also performant under the frozen string literal comment, so there's not much impact on using between symbolised and stringified Hashes.
Surprising Observation
What surprises me is how exponentially slow the Data, Classes, and Structs are when dealing with 10 attributes. Having 50-60 times slower performance than Arrays has got to be excruciatingly painful on dealing with. (Hash to - Class: 21.68x, Hash to - Data: 19.77, Hash to Struct - 24.26x)
So, if you're dealing with large data (well, 10 seems large enough considering the impact), it would be best to use more primitive data objects, like Arrays and Hashes, especially Hashes since it has at least some structure on to it.
Don't forget why to use Structs
I think one of the most important thing with Structs (and Data) is that they're value objects. In my own words, it means that you can compare them by themselves. Class instances cannot be compared by themselves, and that's the only disadvantage I could see with classes, considering they're more performant in most cases now.
Take a look at the code to show this behavior:
irb(main):001* class A
irb(main):002* attr_reader :a
irb(main):003* def initialize(a)
irb(main):004* @a = a
irb(main):005* end
irb(main):006> end
=> :initialize
irb(main):007> a = A.new(1)
=> ##
irb(main):008> b = A.new(1)
=> ##
irb(main):009> a == b
=> false
Conclusion
I think it's a great decision to write this second article, because I've learned more things with the wonderful Ruby language. I hope you've enjoyed reading as I've enjoyed writing this.
Here are my takeaways on this:
- In Ruby 3.4.2, Classes are now more performant than Structs in close to real-life situations.
- The order of priority (in terms of performance) when using data structures are Arrays, Hashes, Classes, Structs, Data. But, you can prioritise the other objects depending on your inclinations.
- Prioritise Arrays or Hashes when dealing with large data sets to avoid performance penalties.
- Symbolised Hashes are better than Stringified Hashes even with the frozen string literal comment, but not very far off.
- Always use the frozen string literal comment.
- Don't check twice, check 3 or 4 times!
- Articles you reference update themselves and make your referring article confusing.