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.
@@ -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`