flexor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +394 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +8 -0
- data/benchmark/compare.rb +173 -0
- data/benchmark/results-with-caching.txt +175 -0
- data/docs/benchmark-results.md +64 -0
- data/docs/original_specification.yaml +426 -0
- data/docs/specification.yaml +453 -0
- data/lib/flexor/hash_delegation.rb +30 -0
- data/lib/flexor/serialization.rb +32 -0
- data/lib/flexor/version.rb +3 -0
- data/lib/flexor/vivification.rb +47 -0
- data/lib/flexor.rb +187 -0
- data/rakelib/benchmark.rake +4 -0
- data/rakelib/rdoc.rake +8 -0
- data/rakelib/version.rake +72 -0
- metadata +64 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require "benchmark/ips"
|
|
2
|
+
require "ostruct"
|
|
3
|
+
require_relative "../lib/flexor"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "hashie"
|
|
7
|
+
Hashie.logger = Logger.new(File::NULL)
|
|
8
|
+
HAS_HASHIE = true
|
|
9
|
+
rescue LoadError
|
|
10
|
+
HAS_HASHIE = false
|
|
11
|
+
warn "Hashie not installed skipping Mash benchmarks. Install with: gem install hashie"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def hashie_report(bench, label, &block)
|
|
15
|
+
bench.report(label, &block) if HAS_HASHIE
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
FLAT_HASH = { name: "alice", age: 30, city: "NYC" }.freeze
|
|
19
|
+
NESTED_HASH = { user: { name: "alice", address: { city: "NYC", zip: "10001" } } }.freeze
|
|
20
|
+
DEEP_HASH = { a: { b: { c: { d: { e: "deep" } } } } }.freeze
|
|
21
|
+
|
|
22
|
+
puts "Ruby #{RUBY_VERSION} (YJIT: #{defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? ? "enabled" : "disabled"})"
|
|
23
|
+
puts "Flexor #{Flexor::VERSION}"
|
|
24
|
+
puts "Hashie #{Hashie::VERSION}" if HAS_HASHIE
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
# --- 1. Construction ---
|
|
28
|
+
puts "=" * 60
|
|
29
|
+
puts "CONSTRUCTION"
|
|
30
|
+
puts "=" * 60
|
|
31
|
+
|
|
32
|
+
Benchmark.ips do |x|
|
|
33
|
+
x.report("Flexor.new (flat)") { Flexor.new(FLAT_HASH) }
|
|
34
|
+
x.report("Mash.new (flat)") { Hashie::Mash.new(FLAT_HASH) } if HAS_HASHIE
|
|
35
|
+
x.report("OpenStruct (flat)") { OpenStruct.new(FLAT_HASH) }
|
|
36
|
+
x.compare!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts
|
|
40
|
+
|
|
41
|
+
Benchmark.ips do |x|
|
|
42
|
+
x.report("Flexor.new (nested)") { Flexor.new(NESTED_HASH) }
|
|
43
|
+
x.report("Mash.new (nested)") { Hashie::Mash.new(NESTED_HASH) } if HAS_HASHIE
|
|
44
|
+
x.report("OpenStruct (nested)") { OpenStruct.new(NESTED_HASH) }
|
|
45
|
+
x.compare!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# --- 2. Reading (method access) ---
|
|
49
|
+
puts
|
|
50
|
+
puts "=" * 60
|
|
51
|
+
puts "READING (method access)"
|
|
52
|
+
puts "=" * 60
|
|
53
|
+
|
|
54
|
+
flexor = Flexor.new(FLAT_HASH)
|
|
55
|
+
mash = HAS_HASHIE ? Hashie::Mash.new(FLAT_HASH) : nil
|
|
56
|
+
ostruct = OpenStruct.new(FLAT_HASH)
|
|
57
|
+
|
|
58
|
+
Benchmark.ips do |x|
|
|
59
|
+
x.report("Flexor#name") { flexor.name }
|
|
60
|
+
x.report("Mash#name") { mash.name } if HAS_HASHIE
|
|
61
|
+
x.report("OpenStruct#name") { ostruct.name }
|
|
62
|
+
x.compare!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# --- 3. Reading (nested chaining) ---
|
|
66
|
+
puts
|
|
67
|
+
puts "=" * 60
|
|
68
|
+
puts "READING (nested chaining)"
|
|
69
|
+
puts "=" * 60
|
|
70
|
+
|
|
71
|
+
flexor_nested = Flexor.new(NESTED_HASH)
|
|
72
|
+
mash_nested = HAS_HASHIE ? Hashie::Mash.new(NESTED_HASH) : nil
|
|
73
|
+
|
|
74
|
+
Benchmark.ips do |x|
|
|
75
|
+
x.report("Flexor chain") { flexor_nested.user.address.city }
|
|
76
|
+
x.report("Mash chain") { mash_nested.user.address.city } if HAS_HASHIE
|
|
77
|
+
x.compare!
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# --- 4. Writing ---
|
|
81
|
+
puts
|
|
82
|
+
puts "=" * 60
|
|
83
|
+
puts "WRITING (method setter)"
|
|
84
|
+
puts "=" * 60
|
|
85
|
+
|
|
86
|
+
Benchmark.ips do |x|
|
|
87
|
+
f = Flexor.new
|
|
88
|
+
m = HAS_HASHIE ? Hashie::Mash.new : nil
|
|
89
|
+
o = OpenStruct.new
|
|
90
|
+
|
|
91
|
+
x.report("Flexor#name=") { f.name = "bob" }
|
|
92
|
+
x.report("Mash#name=") { m.name = "bob" } if HAS_HASHIE
|
|
93
|
+
x.report("OpenStruct#name=") { o.name = "bob" }
|
|
94
|
+
x.compare!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# --- 5. Writing (hash assignment with vivification) ---
|
|
98
|
+
puts
|
|
99
|
+
puts "=" * 60
|
|
100
|
+
puts "WRITING (hash assignment)"
|
|
101
|
+
puts "=" * 60
|
|
102
|
+
|
|
103
|
+
Benchmark.ips do |x|
|
|
104
|
+
x.report("Flexor []= hash") {
|
|
105
|
+
f = Flexor.new
|
|
106
|
+
f[:config] = { db: { host: "localhost" } }
|
|
107
|
+
}
|
|
108
|
+
hashie_report(x, "Mash []= hash") { Hashie::Mash.new.tap { |m| m[:config] = { db: { host: "localhost" } } } }
|
|
109
|
+
x.compare!
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# --- 6. Deep autovivification ---
|
|
113
|
+
puts
|
|
114
|
+
puts "=" * 60
|
|
115
|
+
puts "AUTOVIVIFICATION (deep write)"
|
|
116
|
+
puts "=" * 60
|
|
117
|
+
|
|
118
|
+
Benchmark.ips do |x|
|
|
119
|
+
x.report("Flexor a.b.c.d=") {
|
|
120
|
+
f = Flexor.new
|
|
121
|
+
f.a.b.c.d = "deep"
|
|
122
|
+
}
|
|
123
|
+
hashie_report(x, "Mash a!.b!.c!.d=") { Hashie::Mash.new.tap { |m| m.a!.b!.c!.d = "deep" } }
|
|
124
|
+
x.compare!
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# --- 7. to_h conversion ---
|
|
128
|
+
puts
|
|
129
|
+
puts "=" * 60
|
|
130
|
+
puts "CONVERSION (to_h)"
|
|
131
|
+
puts "=" * 60
|
|
132
|
+
|
|
133
|
+
flexor_deep = Flexor.new(DEEP_HASH)
|
|
134
|
+
mash_deep = HAS_HASHIE ? Hashie::Mash.new(DEEP_HASH) : nil
|
|
135
|
+
ostruct_flat = OpenStruct.new(FLAT_HASH)
|
|
136
|
+
|
|
137
|
+
Benchmark.ips do |x|
|
|
138
|
+
x.report("Flexor#to_h (deep)") { flexor_deep.to_h }
|
|
139
|
+
x.report("Mash#to_h (deep)") { mash_deep.to_h } if HAS_HASHIE
|
|
140
|
+
x.report("OpenStruct#to_h") { ostruct_flat.to_h }
|
|
141
|
+
x.compare!
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- 8. Deep merge ---
|
|
145
|
+
puts
|
|
146
|
+
puts "=" * 60
|
|
147
|
+
puts "DEEP MERGE"
|
|
148
|
+
puts "=" * 60
|
|
149
|
+
|
|
150
|
+
flexor_base = Flexor.new({ db: { host: "localhost", port: 5432 }, log: "info" })
|
|
151
|
+
mash_base = HAS_HASHIE ? Hashie::Mash.new({ db: { host: "localhost", port: 5432 }, log: "info" }) : nil
|
|
152
|
+
override = { db: { port: 3306, name: "mydb" }, log: "debug" }
|
|
153
|
+
|
|
154
|
+
Benchmark.ips do |x|
|
|
155
|
+
x.report("Flexor#merge") { flexor_base.merge(override) }
|
|
156
|
+
x.report("Mash#merge") { mash_base.merge(override) } if HAS_HASHIE
|
|
157
|
+
x.compare!
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# --- 9. Missing key access ---
|
|
161
|
+
puts
|
|
162
|
+
puts "=" * 60
|
|
163
|
+
puts "MISSING KEY ACCESS"
|
|
164
|
+
puts "=" * 60
|
|
165
|
+
|
|
166
|
+
flexor_empty = Flexor.new
|
|
167
|
+
mash_empty = HAS_HASHIE ? Hashie::Mash.new : nil
|
|
168
|
+
|
|
169
|
+
Benchmark.ips do |x|
|
|
170
|
+
x.report("Flexor missing") { flexor_empty.nope }
|
|
171
|
+
x.report("Mash missing") { mash_empty.nope } if HAS_HASHIE
|
|
172
|
+
x.compare!
|
|
173
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Ruby 4.0.1 (YJIT: enabled)
|
|
2
|
+
Flexor 0.0.1
|
|
3
|
+
Hashie 5.1.1
|
|
4
|
+
|
|
5
|
+
============================================================
|
|
6
|
+
CONSTRUCTION
|
|
7
|
+
============================================================
|
|
8
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
9
|
+
Warming up --------------------------------------
|
|
10
|
+
Flexor.new (flat) 163.066k i/100ms
|
|
11
|
+
Mash.new (flat) 52.640k i/100ms
|
|
12
|
+
OpenStruct (flat) 13.099k i/100ms
|
|
13
|
+
Calculating -------------------------------------
|
|
14
|
+
Flexor.new (flat) 1.721M (± 9.6%) i/s (581.18 ns/i) - 8.642M in 5.094349s
|
|
15
|
+
Mash.new (flat) 533.781k (± 1.4%) i/s (1.87 μs/i) - 2.685M in 5.030551s
|
|
16
|
+
OpenStruct (flat) 124.015k (± 5.2%) i/s (8.06 μs/i) - 628.752k in 5.086193s
|
|
17
|
+
|
|
18
|
+
Comparison:
|
|
19
|
+
Flexor.new (flat): 1720629.7 i/s
|
|
20
|
+
Mash.new (flat): 533781.0 i/s - 3.22x slower
|
|
21
|
+
OpenStruct (flat): 124015.0 i/s - 13.87x slower
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
25
|
+
Warming up --------------------------------------
|
|
26
|
+
Flexor.new (nested) 65.146k i/100ms
|
|
27
|
+
Mash.new (nested) 21.485k i/100ms
|
|
28
|
+
OpenStruct (nested) 28.870k i/100ms
|
|
29
|
+
Calculating -------------------------------------
|
|
30
|
+
Flexor.new (nested) 623.855k (± 3.3%) i/s (1.60 μs/i) - 3.127M in 5.018035s
|
|
31
|
+
Mash.new (nested) 214.900k (± 1.5%) i/s (4.65 μs/i) - 1.096M in 5.099994s
|
|
32
|
+
OpenStruct (nested) 252.759k (± 5.2%) i/s (3.96 μs/i) - 1.270M in 5.040492s
|
|
33
|
+
|
|
34
|
+
Comparison:
|
|
35
|
+
Flexor.new (nested): 623854.6 i/s
|
|
36
|
+
OpenStruct (nested): 252759.3 i/s - 2.47x slower
|
|
37
|
+
Mash.new (nested): 214900.2 i/s - 2.90x slower
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
============================================================
|
|
41
|
+
READING (method access)
|
|
42
|
+
============================================================
|
|
43
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
44
|
+
Warming up --------------------------------------
|
|
45
|
+
Flexor#name 799.709k i/100ms
|
|
46
|
+
Mash#name 343.414k i/100ms
|
|
47
|
+
OpenStruct#name 1.220M i/100ms
|
|
48
|
+
Calculating -------------------------------------
|
|
49
|
+
Flexor#name 10.128M (± 9.4%) i/s (98.74 ns/i) - 50.382M in 5.024214s
|
|
50
|
+
Mash#name 3.849M (± 8.8%) i/s (259.78 ns/i) - 19.231M in 5.036833s
|
|
51
|
+
OpenStruct#name 17.994M (± 8.7%) i/s (55.57 ns/i) - 90.310M in 5.059803s
|
|
52
|
+
|
|
53
|
+
Comparison:
|
|
54
|
+
OpenStruct#name: 17993847.7 i/s
|
|
55
|
+
Flexor#name: 10127706.6 i/s - 1.78x slower
|
|
56
|
+
Mash#name: 3849448.9 i/s - 4.67x slower
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
============================================================
|
|
60
|
+
READING (nested chaining)
|
|
61
|
+
============================================================
|
|
62
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
63
|
+
Warming up --------------------------------------
|
|
64
|
+
Flexor chain 294.871k i/100ms
|
|
65
|
+
Mash chain 124.885k i/100ms
|
|
66
|
+
Calculating -------------------------------------
|
|
67
|
+
Flexor chain 3.695M (± 5.7%) i/s (270.66 ns/i) - 18.577M in 5.046400s
|
|
68
|
+
Mash chain 1.428M (± 1.2%) i/s (700.26 ns/i) - 7.243M in 5.073040s
|
|
69
|
+
|
|
70
|
+
Comparison:
|
|
71
|
+
Flexor chain: 3694724.7 i/s
|
|
72
|
+
Mash chain: 1428036.7 i/s - 2.59x slower
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
============================================================
|
|
76
|
+
WRITING (method setter)
|
|
77
|
+
============================================================
|
|
78
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
79
|
+
Warming up --------------------------------------
|
|
80
|
+
Flexor#name= 575.837k i/100ms
|
|
81
|
+
Mash#name= 69.733k i/100ms
|
|
82
|
+
OpenStruct#name= 819.293k i/100ms
|
|
83
|
+
Calculating -------------------------------------
|
|
84
|
+
Flexor#name= 6.786M (± 3.5%) i/s (147.36 ns/i) - 33.974M in 5.014192s
|
|
85
|
+
Mash#name= 715.935k (± 1.2%) i/s (1.40 μs/i) - 3.626M in 5.065556s
|
|
86
|
+
OpenStruct#name= 9.487M (±15.0%) i/s (105.41 ns/i) - 45.880M in 5.005795s
|
|
87
|
+
|
|
88
|
+
Comparison:
|
|
89
|
+
OpenStruct#name=: 9487184.9 i/s
|
|
90
|
+
Flexor#name=: 6785984.8 i/s - 1.40x slower
|
|
91
|
+
Mash#name=: 715934.6 i/s - 13.25x slower
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
============================================================
|
|
95
|
+
WRITING (hash assignment)
|
|
96
|
+
============================================================
|
|
97
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
98
|
+
Warming up --------------------------------------
|
|
99
|
+
Flexor []= hash 70.692k i/100ms
|
|
100
|
+
Mash []= hash 42.105k i/100ms
|
|
101
|
+
Calculating -------------------------------------
|
|
102
|
+
Flexor []= hash 722.453k (± 1.4%) i/s (1.38 μs/i) - 3.676M in 5.089203s
|
|
103
|
+
Mash []= hash 427.759k (± 1.2%) i/s (2.34 μs/i) - 2.147M in 5.020685s
|
|
104
|
+
|
|
105
|
+
Comparison:
|
|
106
|
+
Flexor []= hash: 722453.4 i/s
|
|
107
|
+
Mash []= hash: 427758.7 i/s - 1.69x slower
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
============================================================
|
|
111
|
+
AUTOVIVIFICATION (deep write)
|
|
112
|
+
============================================================
|
|
113
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
114
|
+
Warming up --------------------------------------
|
|
115
|
+
Flexor a.b.c.d= 7.491k i/100ms
|
|
116
|
+
Mash a!.b!.c!.d= 26.742k i/100ms
|
|
117
|
+
Calculating -------------------------------------
|
|
118
|
+
Flexor a.b.c.d= 70.677k (± 5.8%) i/s (14.15 μs/i) - 359.568k in 5.107327s
|
|
119
|
+
Mash a!.b!.c!.d= 269.153k (± 1.5%) i/s (3.72 μs/i) - 1.364M in 5.068281s
|
|
120
|
+
|
|
121
|
+
Comparison:
|
|
122
|
+
Mash a!.b!.c!.d=: 269152.7 i/s
|
|
123
|
+
Flexor a.b.c.d=: 70677.4 i/s - 3.81x slower
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
============================================================
|
|
127
|
+
CONVERSION (to_h)
|
|
128
|
+
============================================================
|
|
129
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
130
|
+
Warming up --------------------------------------
|
|
131
|
+
Flexor#to_h (deep) 53.044k i/100ms
|
|
132
|
+
Mash#to_h (deep) 1.113M i/100ms
|
|
133
|
+
OpenStruct#to_h 583.538k i/100ms
|
|
134
|
+
Calculating -------------------------------------
|
|
135
|
+
Flexor#to_h (deep) 544.961k (± 1.6%) i/s (1.83 μs/i) - 2.758M in 5.062731s
|
|
136
|
+
Mash#to_h (deep) 15.876M (± 1.5%) i/s (62.99 ns/i) - 80.124M in 5.048076s
|
|
137
|
+
OpenStruct#to_h 7.316M (± 1.4%) i/s (136.68 ns/i) - 36.763M in 5.025665s
|
|
138
|
+
|
|
139
|
+
Comparison:
|
|
140
|
+
Mash#to_h (deep): 15875729.1 i/s
|
|
141
|
+
OpenStruct#to_h: 7316392.4 i/s - 2.17x slower
|
|
142
|
+
Flexor#to_h (deep): 544960.9 i/s - 29.13x slower
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
============================================================
|
|
146
|
+
DEEP MERGE
|
|
147
|
+
============================================================
|
|
148
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
149
|
+
Warming up --------------------------------------
|
|
150
|
+
Flexor#merge 141.366k i/100ms
|
|
151
|
+
Mash#merge 17.957k i/100ms
|
|
152
|
+
Calculating -------------------------------------
|
|
153
|
+
Flexor#merge 1.520M (± 1.2%) i/s (657.82 ns/i) - 7.634M in 5.022294s
|
|
154
|
+
Mash#merge 180.829k (± 0.9%) i/s (5.53 μs/i) - 915.807k in 5.064952s
|
|
155
|
+
|
|
156
|
+
Comparison:
|
|
157
|
+
Flexor#merge: 1520181.4 i/s
|
|
158
|
+
Mash#merge: 180828.6 i/s - 8.41x slower
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
============================================================
|
|
162
|
+
MISSING KEY ACCESS
|
|
163
|
+
============================================================
|
|
164
|
+
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [aarch64-linux]
|
|
165
|
+
Warming up --------------------------------------
|
|
166
|
+
Flexor missing 806.094k i/100ms
|
|
167
|
+
Mash missing 133.262k i/100ms
|
|
168
|
+
Calculating -------------------------------------
|
|
169
|
+
Flexor missing 10.920M (± 1.4%) i/s (91.58 ns/i) - 54.814M in 5.020653s
|
|
170
|
+
Mash missing 1.289M (± 1.2%) i/s (775.78 ns/i) - 6.530M in 5.066387s
|
|
171
|
+
|
|
172
|
+
Comparison:
|
|
173
|
+
Flexor missing: 10919839.7 i/s
|
|
174
|
+
Mash missing: 1289028.6 i/s - 8.47x slower
|
|
175
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Benchmark: Flexor vs Hashie::Mash vs OpenStruct
|
|
2
|
+
|
|
3
|
+
Ruby 4.0.1, YJIT enabled, aarch64-linux
|
|
4
|
+
|
|
5
|
+
Flexor 0.0.1 | Hashie 5.1.1 | OpenStruct (stdlib)
|
|
6
|
+
|
|
7
|
+
## Results (with lazy method caching)
|
|
8
|
+
|
|
9
|
+
Flexor caches property accessors via `define_singleton_method`. Getters are cached only when the key already exists in the store — autovivified keys skip the caching cost on first access and cache on the second. Setters cache on first use.
|
|
10
|
+
|
|
11
|
+
| Operation | Flexor | Mash | OpenStruct | Winner |
|
|
12
|
+
|-----------|--------|------|------------|--------|
|
|
13
|
+
| Construction (flat) | 1.74M i/s | 530K i/s | 124K i/s | **Flexor** 3.3x vs Mash, 14.0x vs OS |
|
|
14
|
+
| Construction (nested) | 660K i/s | 216K i/s | 330K i/s | **Flexor** 3.1x vs Mash |
|
|
15
|
+
| Read (method) | 10.8M i/s | 3.69M i/s | 19.4M i/s | **OpenStruct** 1.8x vs Flexor |
|
|
16
|
+
| Read (nested chain) | 3.76M i/s | 1.43M i/s | — | **Flexor** 2.6x |
|
|
17
|
+
| Write (setter) | 6.59M i/s | 717K i/s | 9.97M i/s | **OpenStruct** 1.5x vs Flexor |
|
|
18
|
+
| Write (hash assign) | 720K i/s | 426K i/s | — | **Flexor** 1.7x |
|
|
19
|
+
| Autovivification | 176K i/s | 270K i/s | — | **Mash** 1.5x |
|
|
20
|
+
| to_h (deep) | 547K i/s | 15.3M i/s | 6.6M i/s | **Mash** 28x vs Flexor |
|
|
21
|
+
| Deep merge | 1.50M i/s | 181K i/s | — | **Flexor** 8.3x |
|
|
22
|
+
| Missing key access | 10.5M i/s | 1.32M i/s | — | **Flexor** 8.0x |
|
|
23
|
+
|
|
24
|
+
## Method caching evolution
|
|
25
|
+
|
|
26
|
+
| Operation | No caching | Eager caching | Lazy caching |
|
|
27
|
+
|-----------|-----------|--------------|-------------|
|
|
28
|
+
| Read (method) | 3.76M | 10.1M | **10.8M** |
|
|
29
|
+
| Read (nested chain) | 1.32M | 3.69M | **3.76M** |
|
|
30
|
+
| Write (setter) | 1.27M | 6.79M | **6.59M** |
|
|
31
|
+
| Missing key access | 3.79M | 10.9M | **10.5M** |
|
|
32
|
+
| Autovivification | **291K** | 71K (4x regression) | 176K (2.5x recovered) |
|
|
33
|
+
|
|
34
|
+
Lazy caching keeps all the read/write speedups (2.7-5.3x faster than no caching) while recovering most of the autovivification regression (from 4x slower to 1.5x slower vs Mash).
|
|
35
|
+
|
|
36
|
+
## Analysis
|
|
37
|
+
|
|
38
|
+
### Why Flexor wins construction and merge
|
|
39
|
+
|
|
40
|
+
Flexor's wrapper design is lighter at construction time. Creating a Flexor means building an autovivifying Hash via `default_proc` and iterating the input — no key conversion, no warning checks, no indifferent access setup. Mash converts every key to a string, checks for method collisions, and sets up its indifferent access layer.
|
|
41
|
+
|
|
42
|
+
Deep merge is 8.3x faster because Flexor's `merge` does a `dup` + recursive `merge!`, while Mash's merge goes through its full `deep_update` pipeline with key conversion at every level.
|
|
43
|
+
|
|
44
|
+
### Why Flexor is close to OpenStruct on reads and writes
|
|
45
|
+
|
|
46
|
+
With method caching, Flexor uses the same strategy as OpenStruct: `define_singleton_method` on first access, direct method call thereafter. The remaining 1.8x gap on reads comes from Flexor's cached method delegating through `self[name]` → `@store[name]` (two method calls) vs OpenStruct's direct `@table[:name]` (one ivar access).
|
|
47
|
+
|
|
48
|
+
### Why Mash wins to_h
|
|
49
|
+
|
|
50
|
+
Mash IS a Hash (`Mash < Hash`), so `to_h` is essentially a no-op — it returns itself or a thin copy. Flexor wraps a Hash and must recursively convert every nested Flexor back to a plain Hash, filtering phantom keys along the way. This is the main cost of the wrapper design.
|
|
51
|
+
|
|
52
|
+
For most use cases this doesn't matter — you call `to_h` at serialization boundaries, not in hot loops.
|
|
53
|
+
|
|
54
|
+
### How lazy caching works
|
|
55
|
+
|
|
56
|
+
The key insight: only cache getters when `@store.key?(name)` is true. On first access to an unset key, autovivification creates the key via `default_proc` but no singleton method is defined. On the second access, the key exists, so the method gets cached. Constructor-populated keys cache on first read since they already exist in the store.
|
|
57
|
+
|
|
58
|
+
This means autovivification chains (`store.a.b.c.d = "deep"`) only pay the `define_singleton_method` cost for the final setter, not the intermediate getters. Short-lived Flexors created during chain construction are never burdened with cached methods.
|
|
59
|
+
|
|
60
|
+
## Methodology
|
|
61
|
+
|
|
62
|
+
Benchmarks use `benchmark-ips` which measures iterations per second over a 5-second window with warmup. Each operation is tested in isolation. OpenStruct is excluded from operations it doesn't support (nested chaining, autovivification, merge, missing key handling).
|
|
63
|
+
|
|
64
|
+
Run: `ruby --yjit benchmark/compare.rb`
|