Struct vs. OpenStruct Benchmark

In Ruby können Daten auf verschiedene Art und Weise gekapselt werden. So zum Beispiel in Klassen, aber auch in Hashes, Structs und OpenStructs, je nach Anwendungsfall.
Für den Fall einfacher Datencontainer ohne Logik, wird oft auf Hashes zurückgegriffen. Sobald Logik an die Daten gebunden werden soll, werden Klassen genutzt. Irgendwo dazwischen liegen Struct und OpenStruct. Der Zugriff auf die Daten ist allerdings fast ähnlich komfortabel.
Aber wie sieht es aus Sicht der Performance aus?

Behauptung

Die Instantiierung einfacher Datencontainer (wie Hashes) sollte schneller sein, als die Instantiierung komplexerer Klassen.
Ein Benchmarktest soll das nachweisen. Zunächst die Instantiierungen:
Struct

GeoStruct = Struct.new(:latitude, :longitude)
GeoStruct.new(53.6, 11.4)

OpenStruct

OpenStruct.new(latitude: 53.6, longitude: 11.4)

Class

class GeoClass
  attr_accessor :latitude, :longitude

  def initialize(latitude:, longitude:)
    @latitude = latitude
    @longitude = longitude
  end
end
GeoClass.new(latitude: 53.6, longitude: 11.4)

Hash

{ latitude: 53.6, longitude: 11.4 }

Benchmarks

In dem Benchmark wird das Gem benchmark-ips verwendet. Es misst die Anzahl der Iterationen pro Sekunde. Außerdem läßt sich damit der Ruby Garbage Collector (GC) während des Benchmarks ausschalten.
Das Benchmark Script:

require 'benchmark/ips'
require 'ostruct'

class GCSuite
  def warming(*)
    run_gc
  end

  def running(*)
    run_gc
  end

  def warmup_stats(*); end

  def add_report(*); end

  private

  def run_gc
    GC.enable
    GC.start
    GC.disable
  end
end
suite = GCSuite.new

Benchmark.ips do |bm|
  bm.config(suite: suite)

  bm.report('GeoStruct.new') do
    GeoStruct.new(53.6, 11.4)
  end

  bm.report('Hash {}') do
    { latitude: 53.6, longitude: 11.4 }
  end

  bm.report('GeoClass.new') do
    GeoClass.new(latitude: 53.6, longitude: 11.4)
  end

  bm.report('OpenStruct.new') do
    OpenStruct.new(latitude: 53.6, longitude: 11.4)
  end
end

Resultate

Die Ergebnisse sind erstaunlich:

Warming up --------------------------------------
       GeoStruct.new    99.444k i/100ms
             Hash {}    80.241k i/100ms
        GeoClass.new    45.358k i/100ms
      OpenStruct.new    39.240k i/100ms
Calculating -------------------------------------
       GeoStruct.new      2.517M (± 3.6%) i/s -     12.629M in   5.024347s
             Hash {}      1.606M (± 9.9%) i/s -      8.024M in   5.036606s
        GeoClass.new    730.282k (± 1.1%) i/s -      3.674M in   5.031556s
      OpenStruct.new    574.970k (± 3.0%) i/s -      2.904M in   5.055057s

Offensichtlich benötigt die Instantiierung von Structs die wenigsten Ressourcen. Es lassen sich im gleichen Zeitraum ca. 60% mehr Struct Objekte instantiieren als Hashes. Struct schlägt Class sogar um den Faktor 3,5.

Fazit

Wenn es um Performance geht, ist Struct die Wahl. Hash als Datencontainer ist ebenfalls noch performant. Klassen sollten verwendet werden, wenn Objekte erstellt werden müssen, die umfangreichere Logik enthalten.
OpenStruct dagegen ist aufgrund der Flexibilität eher nur geeignet für Prototyping, aber weniger für performante Instantiierung von massenhaften Objekten.