When should I use Struct vs. OpenStruct?
In general, what are the advantages and disadvantages of using an OpenStruct as compared to a Struct? What type of general use-cases would fit each of these?
With an OpenStruct
, you can arbitrarily create attributes. A Struct
, on the other hand, must have its attributes defined when you create it. The choice of one over the other should be based primarily on whether you need to be able to add attributes later.
The way to think about them is as the middle ground of the spectrum between Hashes on one side and classes on the other. They imply a more concrete relationship amongst the data than does a Hash
, but they don't have the instance methods as would a class. A bunch of options for a function, for example, make sense in a hash; they're only loosely related. A name, email, and phone number needed by a function could be packaged together in a Struct
or OpenStruct
. If that name, email, and phone number needed methods to provide the name in both "First Last" and "Last, First" formats, then you should create a class to handle it.
Other benchmark:
require 'benchmark'
require 'ostruct'
REP = 100000
User = Struct.new(:name, :age)
USER = "User".freeze
AGE = 21
HASH = {:name => USER, :age => AGE}.freeze
Benchmark.bm 20 do |x|
x.report 'OpenStruct slow' do
REP.times do |index|
OpenStruct.new(:name => "User", :age => 21)
end
end
x.report 'OpenStruct fast' do
REP.times do |index|
OpenStruct.new(HASH)
end
end
x.report 'Struct slow' do
REP.times do |index|
User.new("User", 21)
end
end
x.report 'Struct fast' do
REP.times do |index|
User.new(USER, AGE)
end
end
end
For the impatient who wants to get an idea of the benchmark results, without running them themselves, here is the output of the code above (on an MB Pro 2.4GHz i7)
user system total real
OpenStruct slow 4.430000 0.250000 4.680000 ( 4.683851)
OpenStruct fast 4.380000 0.270000 4.650000 ( 4.649809)
Struct slow 0.090000 0.000000 0.090000 ( 0.094136)
Struct fast 0.080000 0.000000 0.080000 ( 0.078940)
UPDATE:
Timings for creating 1 million instances:
0.357788 seconds elapsed for Class.new (Ruby 2.5.5)
0.764953 seconds elapsed for Struct (Ruby 2.5.5)
0.842782 seconds elapsed for Hash (Ruby 2.5.5)
2.211959 seconds elapsed for OpenStruct (Ruby 2.5.5)
0.213175 seconds elapsed for Class.new (Ruby 2.6.3)
0.335341 seconds elapsed for Struct (Ruby 2.6.3)
0.836996 seconds elapsed for Hash (Ruby 2.6.3)
2.070901 seconds elapsed for OpenStruct (Ruby 2.6.3)
0.936016 seconds elapsed for Class.new (Ruby 2.7.2)
0.453067 seconds elapsed for Struct (Ruby 2.7.2)
1.016676 seconds elapsed for Hash (Ruby 2.7.2)
1.482318 seconds elapsed for OpenStruct (Ruby 2.7.2)
0.421272 seconds elapsed for Class.new (Ruby 3.0.0)
0.322617 seconds elapsed for Struct (Ruby 3.0.0)
0.719928 seconds elapsed for Hash (Ruby 3.0.0)
35.130777 seconds elapsed for OpenStruct (Ruby 3.0.0) (oops!)
0.443975 seconds elapsed for Class.new (Ruby 3.0.1)
0.348031 seconds elapsed for Struct (Ruby 3.0.1)
0.737662 seconds elapsed for Hash (Ruby 3.0.1)
16.264204 seconds elapsed for SmartHash (Ruby 3.0.1) (meh)
53.396924 seconds elapsed for OpenStruct (Ruby 3.0.1) (oops!)
See: Ruby 3.0.0 Bug #18032 was closed, because it is a feature, not a bug
Old Answers:
As of Ruby 2.4.1 OpenStruct and Struct are much closer in speed. See https://stackoverflow.com/a/43987844/128421
For completeness: Struct vs. Class vs. Hash vs. OpenStruct
Running similar code as burtlo's, on Ruby 1.9.2, (1 of 4 cores x86_64, 8GB RAM) [table edited to align columns]:
creating 1 Mio Structs : 1.43 sec , 219 MB / 90MB (virt/res) creating 1 Mio Class instances : 1.43 sec , 219 MB / 90MB (virt/res) creating 1 Mio Hashes : 4.46 sec , 493 MB / 364MB (virt/res) creating 1 Mio OpenStructs : 415.13 sec , 2464 MB / 2.3GB (virt/res) # ~100x slower than Hashes creating 100K OpenStructs : 10.96 sec , 369 MB / 242MB (virt/res)
OpenStructs are sloooooow and memory intensive , and don't scale well for large data sets
Here's the script to reproduce the results:
require 'ostruct'
require 'smart_hash'
MAX = 1_000_000
class C;
attr_accessor :name, :age;
def initialize(name, age)
self.name = name
self.age = age
end
end
start = Time.now
collection = (1..MAX).collect do |i|
C.new('User', 21)
end; 1
stop = Time.now
puts " #{stop - start} seconds elapsed for Class.new (Ruby #{RUBY_VERSION})"
s = Struct.new(:name, :age)
start = Time.now
collection = (1..MAX).collect do |i|
s.new('User', 21)
end; 1
stop = Time.now
puts " #{stop - start} seconds elapsed for Struct (Ruby #{RUBY_VERSION})"
start = Time.now
collection = (1..MAX).collect do |i|
{:name => "User" , :age => 21}
end; 1
stop = Time.now
puts " #{stop - start} seconds elapsed for Hash (Ruby #{RUBY_VERSION})"
start = Time.now
collection = (1..MAX).collect do |i|
s = SmartHash[].merge( {:name => "User" , :age => 21} )
end; 1
stop = Time.now
puts " #{stop - start} seconds elapsed for SmartHash (Ruby #{RUBY_VERSION})"
start = Time.now
collection = (1..MAX).collect do |i|
OpenStruct.new(:name => "User" , :age => 21)
end; 1
stop = Time.now
puts " #{stop - start} seconds elapsed for OpenStruct (Ruby #{RUBY_VERSION})"