Walt Jones
Walt Jones

Reputation: 1328

When is it better to use a Struct rather than a Hash in Ruby?

A Ruby Struct allows an instance to be generated with a set of accessors:

# Create a structure named by its constant
Customer = Struct.new(:name, :address)     #=> Customer
Customer.new("Dave", "123 Main")           #=> #<Customer name="Dave", address="123 Main">

This looks convenient and powerful, however, a Hash does something pretty similar:

Customer = {:name => "Dave", :address => "123 Main"}

What are the real-world situations where I should prefer a Struct (and why), and what are the caveats or pitfalls in choosing one over the other?

Upvotes: 48

Views: 12456

Answers (6)

ogirginc
ogirginc

Reputation: 5270

I have rerun @mpospelov's benchmarks on Ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20].

Simple

Warming up --------------------------------------
                hash     1.008M i/100ms
              struct   423.906k i/100ms
             ostruct    16.384k i/100ms
Calculating -------------------------------------
                hash      9.923M (± 1.9%) i/s -     50.412M in   5.082029s
              struct      4.327M (± 2.6%) i/s -     22.043M in   5.097358s
             ostruct    158.771k (± 6.7%) i/s -    802.816k in   5.084066s

Comparison:
                hash:  9923144.6 i/s
              struct:  4327316.1 i/s - 2.29x  (± 0.00) slower
             ostruct:   158771.4 i/s - 62.50x  (± 0.00) slower

Huge List

Warming up --------------------------------------
                hash    71.378k i/100ms
              struct    99.245k i/100ms
             ostruct   855.000  i/100ms
Calculating -------------------------------------
                hash    712.113k (± 4.9%) i/s -      3.569M in   5.024094s
              struct      1.098M (± 2.9%) i/s -      5.558M in   5.066160s
             ostruct      8.629k (± 4.8%) i/s -     43.605k in   5.066147s

Comparison:
              struct:  1098071.6 i/s
                hash:   712112.5 i/s - 1.54x  (± 0.00) slower
             ostruct:     8628.8 i/s - 127.26x  (± 0.00) slower

Conclusion

  • If performance does matter, use struct.
  • If defining fields on the fly matters, go with open struct.

Upvotes: 2

mpospelov
mpospelov

Reputation: 1549

Here is more readable benchmarks for those who loves IPS(iteration per seconds) metrics:

For small instances:

require 'benchmark/ips'
require 'ostruct'

MyStruct = Struct.new(:a)
Benchmark.ips do |x|
  x.report('hash') { a = { a: 1 }; a[:a] }
  x.report('struct') { a = MyStuct.new(1); a.a }
  x.report('ostruct') { a = OpenStruct.new(a: 1); a.a }

  x.compare!
end

results:

Warming up --------------------------------------
                hash   147.162k i/100ms
              struct   171.949k i/100ms
             ostruct    21.086k i/100ms
Calculating -------------------------------------
                hash      2.608M (± 3.1%) i/s -     13.097M in   5.028022s
              struct      3.680M (± 1.8%) i/s -     18.399M in   5.001510s
             ostruct    239.108k (± 5.5%) i/s -      1.202M in   5.046817s

Comparison:
              struct:  3679772.2 i/s
                hash:  2607565.1 i/s - 1.41x  slower
             ostruct:   239108.4 i/s - 15.39x  slower

For huge list:

require 'benchmark/ips'
require 'ostruct'

MyStruct = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z)

Benchmark.ips do |x|
  x.report('hash') do
    hash = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 }
    hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e]; hash[:f]; hash[:g]; hash[:h]; hash[:i]; hash[:j]; hash[:k]; hash[:l]; hash[:m]; hash[:n]; hash[:o]; hash[:p]; hash[:q]; hash[:r]; hash[:s]; hash[:t]; hash[:u]; hash[:v]; hash[:w]; hash[:x]; hash[:y]; hash[:z]
  end

  x.report('struct') do
    struct = MyStruct.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26)
    struct.a;struct.b;struct.c;struct.d;struct.e;struct.f;struct.g;struct.h;struct.i;struct.j;struct.k;struct.l;struct.m;struct.n;struct.o;struct.p;struct.q;struct.r;struct.s;struct.t;struct.u;struct.v;struct.w;struct.x;struct.y;struct.z
  end

  x.report('ostruct') do
    ostruct = OpenStruct.new( a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, u: 21, v: 22, w: 23, x: 24, y: 25, z: 26)
    ostruct.a;ostruct.b;ostruct.c;ostruct.d;ostruct.e;ostruct.f;ostruct.g;ostruct.h;ostruct.i;ostruct.j;ostruct.k;ostruct.l;ostruct.m;ostruct.n;ostruct.o;ostruct.p;ostruct.q;ostruct.r;ostruct.s;ostruct.t;ostruct.u;ostruct.v;ostruct.w;ostruct.x;ostruct.y;ostruct.z;
  end

  x.compare!
end

results:

Warming up --------------------------------------
                hash    51.741k i/100ms
              struct    62.346k i/100ms
             ostruct     1.010k i/100ms
Calculating -------------------------------------
                hash    603.104k (± 3.9%) i/s -      3.053M in   5.070565s
              struct    780.005k (± 3.4%) i/s -      3.928M in   5.041571s
             ostruct     11.321k (± 3.4%) i/s -     56.560k in   5.001660s

Comparison:
              struct:   780004.8 i/s
                hash:   603103.8 i/s - 1.29x  slower
             ostruct:    11321.2 i/s - 68.90x  slower

Conclusion

As you can see struct is a little bit faster, but it requires to define struct fields before using it, so if performance is really matter for you use struct ;)

Upvotes: 11

the Tin Man
the Tin Man

Reputation: 160591

Regarding comments about the speed of using Hashes, Struct or OpenStruct: Hash will always win for general use. It's the basis of OpenStruct without the additional icing so it's not as flexible, but it's lean and mean.

Using Ruby 2.4.1:

require 'fruity'
require 'ostruct'

def _hash
  h = {}
  h['a'] = 1
  h['a']
end

def _struct
  s = Struct.new(:a)
  foo = s.new(1)
  foo.a
end

def _ostruct
  person = OpenStruct.new
  person.a = 1
  person.a
end

compare do
  a_hash { _hash }
  a_struct { _struct }
  an_ostruct { _ostruct }
end

# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than an_ostruct by 13x ± 1.0
# >> an_ostruct is similar to a_struct

Using more concise definitions of the hash and OpenStruct:

require 'fruity'
require 'ostruct'

def _hash
  h = {'a' => 1}
  h['a']
end

def _struct
  s = Struct.new(:a)
  foo = s.new(1)
  foo.a
end

def _ostruct
  person = OpenStruct.new('a' => 1)
  person.a
end

compare do
  a_hash { _hash }
  a_struct { _struct }
  an_ostruct { _ostruct }
end

# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than an_ostruct by 17x ± 10.0
# >> an_ostruct is similar to a_struct

If the structure, Hash or Struct or OpenStruct is defined once then used many times, then the access speed becomes more important, and a Struct begins to shine:

require 'fruity'
require 'ostruct'

HSH = {'a' => 1}
def _hash
  HSH['a']
end

STRCT = Struct.new(:a).new(1)
def _struct
  STRCT.a
end

OSTRCT = OpenStruct.new('a' => 1)
def _ostruct
  OSTRCT.a
end

puts "Ruby version: #{RUBY_VERSION}"

compare do
  a_hash { _hash }
  a_struct { _struct }
  an_ostruct { _ostruct }
end

# >> Ruby version: 2.4.1
# >> Running each test 65536 times. Test will take about 2 seconds.
# >> a_struct is faster than a_hash by 4x ± 1.0
# >> a_hash is similar to an_ostruct

Notice though, that the Struct is only 4x faster than the Hash for accessing, whereas the Hash is 17x faster for initialization, assignment and accessing. You'll have to figure out which is the best to use based on the needs of a particular application. I tend to use Hashes for general use as a result.

Also, the speed of using OpenStruct has improved greatly over the years; It used to be slower than Struct based on benchmarks I've seen in the past and comparing to 1.9.3-p551:

require 'fruity'
require 'ostruct'

def _hash
  h = {}
  h['a'] = 1
  h['a']
end

def _struct
  s = Struct.new(:a)
  foo = s.new(1)
  foo.a
end

def _ostruct
  person = OpenStruct.new
  person.a = 1
  person.a
end

puts "Ruby version: #{RUBY_VERSION}"

compare do
  a_hash { _hash }
  a_struct { _struct }
  an_ostruct { _ostruct }
end

# >> Ruby version: 1.9.3
# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than a_struct by 7x ± 1.0
# >> a_struct is faster than an_ostruct by 2x ± 0.1

and:

require 'fruity'
require 'ostruct'

def _hash
  h = {'a' => 1}
  h['a']
end

def _struct
  s = Struct.new(:a)
  foo = s.new(1)
  foo.a
end

def _ostruct
  person = OpenStruct.new('a' => 1)
  person.a
end

puts "Ruby version: #{RUBY_VERSION}"

compare do
  a_hash { _hash }
  a_struct { _struct }
  an_ostruct { _ostruct }
end

# >> Ruby version: 1.9.3
# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than a_struct by 7x ± 1.0
# >> a_struct is faster than an_ostruct by 2x ± 1.0

and:

require 'fruity'
require 'ostruct'

HSH = {'a' => 1}
def _hash
  HSH['a']
end

STRCT = Struct.new(:a).new(1)
def _struct
  STRCT.a
end

OSTRCT = OpenStruct.new('a' => 1)
def _ostruct
  OSTRCT.a
end

puts "Ruby version: #{RUBY_VERSION}"

compare do
  a_hash { _hash }
  a_struct { _struct }
  an_ostruct { _ostruct }
end

# >> Ruby version: 1.9.3
# >> Running each test 32768 times. Test will take about 1 second.
# >> a_struct is faster than an_ostruct by 3x ± 1.0
# >> an_ostruct is similar to a_hash

Upvotes: 6

lzap
lzap

Reputation: 17173

It's mainly performance. Struct is much faster, by order of magnitudes. And consumes less memory when compared to Hash or OpenStruct. More info here: When should I use Struct vs. OpenStruct?

Upvotes: 13

JDonner
JDonner

Reputation: 502

A Struct has the feature that you can get at its elements by index as well as by name:

irb(main):004:0> Person = Struct.new(:name, :age)
=> Person
irb(main):005:0> p = Person.new("fred", 26)
=> #
irb(main):006:0> p[0]
=> "fred"
irb(main):007:0> p[1]
=> 26
irb(main):008:0> p.name
=> "fred"
irb(main):009:0> p.age
=> 26

which sometimes is useful.

Upvotes: 12

gaqzi
gaqzi

Reputation: 3807

Personally I use a struct in cases when I want to make a piece of data act like a collection of data instead of loosely coupled under a Hash.

For instance I've made a script that downloads videos from Youtube and in there I've a struct to represent a Video and to test whether all data is in place:


Video = Struct.new(:title, :video_id, :id) do
  def to_s
    "http://youtube.com/get_video.php?t=#{id}&video_id=#{video_id}&fmt=18"
  end

  def empty?
    @title.nil? and @video_id.nil? and @id.nil?
  end
end

Later on in my code I've a loop that goes through all rows in the videos source HTML-page until empty? doesn't return true.

Another example I've seen is James Edward Gray IIs configuration class which uses OpenStruct to easily add configuration variables loaded from an external file:

#!/usr/bin/env ruby -wKU

require "ostruct"

module Config
  module_function

  def load_config_file(path)
    eval <<-END_CONFIG
    config = OpenStruct.new
    #{File.read(path)}
    config
    END_CONFIG
  end
end

# configuration_file.rb
config.db = File.join(ENV['HOME'], '.cool-program.db')
config.user = ENV['USER']

# Usage:
Config = Config.load_config('configuration_file.rb')
Config.db   # => /home/ba/.cool-program.db
Config.user # => ba
Config.non_existant # => Nil

The difference between Struct and OpenStruct is that Struct only responds to the attributes that you've set, OpenStruct responds to any attribute set - but those with no value set will return Nil

Upvotes: 22

Related Questions