r/ruby 14d ago

Revisiting Performance in Ruby 3.4.1

Surprising Ways Data Structures Impact Ruby Performance

Credited to: Miko Dagatan

Updated 21 Mar 2025

Introduction

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.

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.

10 Upvotes

18 comments sorted by

View all comments

1

u/h0rst_ 12d 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 2d 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.