Ruby's Open Struct's Slow Performance
Ruby’s OpenStruct is slow, but convenient. I wanted something similar. My requirements:
- Define a read-only property bag of any shape to describe events
- Avoid having to deal with concrete classes per event type
- Be able to access the properties as if they belonged to a concrete class Fast
The conclusion is that OpenStruct is really slow, but it’s possible to to create a similar object with really good perf. Here’s a gist of my tests/results:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# OpenStruct is slow. But if you need/want similar functionality, | |
# you can achieve it with really good perf results. See the impl | |
# of NotificationEvent below. | |
# | |
# Benchmark results from my Macbook (2.6 GHz Intel Core i5) | |
# | |
# Rehearsal ----------------------------------------------------- | |
# Literal 1.060000 0.020000 1.080000 ( 1.080056) | |
# NotificationEvent 1.350000 0.000000 1.350000 ( 1.367066) | |
# OpenStruct 11.500000 0.110000 11.610000 ( 11.646464) | |
# ------------------------------------------- total: 14.040000sec | |
# | |
# user system total real | |
# Literal 1.000000 0.010000 1.010000 ( 1.015018) | |
# NotificationEvent 1.380000 0.010000 1.390000 ( 1.395112) | |
# OpenStruct 11.980000 0.300000 12.280000 ( 12.798998) | |
require 'benchmark' | |
require 'ostruct' | |
class Literal | |
attr_reader :event_name, :user, :age | |
def initialize(event_name, hash) | |
@event_name = event_name | |
@user = hash[:user] | |
@age = hash[:age] | |
end | |
end | |
# NotificationEvent is essentially a struct, no methods, just read-only | |
# properties describing the event. | |
class NotificationEvent | |
attr_reader :event_name | |
def initialize(event_name, hash) | |
@event_name = event_name | |
@hash = hash | |
end | |
# Exposes hash keys as object properties for easier access and better duck-typing | |
def method_missing(mid, *args) | |
if args.length > 0 | |
raise "Property #{mid} does not accept arguments #{args}" | |
end | |
@hash.fetch(mid) | |
end | |
end | |
def test_struct(count) | |
x = 0 | |
for i in 0..count | |
evt = yield | |
x += evt.age + evt.user.length | |
end | |
return x | |
end | |
def literal_test(count) | |
test_struct(count) { Literal.new('foo', user: 'Chris', age: 23) } | |
end | |
def notify_test(count) | |
test_struct(count) { NotificationEvent.new('foo', user: 'Chris', age: 23) } | |
end | |
def open_test(count) | |
test_struct(count) { OpenStruct.new(user: 'Chris', age: 23) } | |
end | |
Benchmark.bmbm do |x| | |
count = 1000000 | |
x.report('Literal') { literal_test(count) } | |
x.report('NotificationEvent') { notify_test(count) } | |
x.report('OpenStruct') { open_test(count) } | |
end |