From: "mame (Yusuke Endoh) via ruby-core" Date: 2026-10-15T02:47:52+00:00 Subject: [ruby-core:127117] [Ruby Feature#22437] Speed up creation and basic operations of Struct / Data Issue #22437 has been updated by mame (Yusuke Endoh). Thank you for working on this. As I understand it, experiment/struct-data-core speeds up Struct/Data without breaking compatibility. It will need careful review, but I have no objection to the direction. Personally, I'm much more interested in the ISEQ approach described in the Note. For construction, I think it is the better approach: JIT optimizations should apply straightforwardly, and it can avoid creating a Hash for keyword arguments (at least for Data and `keyword_init: true` Structs; the default Struct, which accepts both positional and keyword arguments, needs some thought). As far as I can tell, all the listed incompatibilities are minor and acceptable. Of course, we should reconsider if trying it reveals an impact on well-known libraries. I think the biggest concern with this approach is how to build the ISEQ. Ideally, it would not go through the parser, yet the code that builds the ISEQ would be short and maintainable. I wonder if that's possible. ---------------------------------------- Feature #22437: Speed up creation and basic operations of Struct / Data https://bugs.ruby-lang.org/issues/22437#change-119517 * Author: shia (Sangyong Sim) * Status: Open ---------------------------------------- ## Summary Struct and Data are slower to create and to operate on than a hand-written class that does the same job. Struct / Data are usually chosen because people expect them to be lightweight value objects, yet once they sit on a hot path they turn out to be slow, and the code ends up being rewritten as a plain class. I think the standard value objects should be chosen for their original reasons, such as readability and immutability, rather than avoided for performance. This ticket proposes to narrow that gap. ## Approach The experimental branch below is the result of asking Claude Opus 5.5 to speed things up under the following constraints: - Do not change any interface - Do not add any fields - Do not change how stack traces look - Do not change any observable behavior So I do not intend to merge this branch as is. I will review the changes one by one, pick only the ones that are realistic and maintainable, and submit them as small PRs. ## Summary of the changes in the experimental branch - Hidden ivar lookups (`__members__` etc.) are cached in six file-static slots in `struct.c` (48 bytes per process), keyed by shape id. - `Struct#initialize` - Reads the member count from self. - Walks the keyword Hash without rb_ensure. - Instance creation of Data classes (`Point.new`, where `Point = Data.define(...)`) - Stores positional and keyword arguments directly. - Allocates the object already frozen. - `Data#with`: copies the existing values directly. - `Data#=`=, `#eql?` and Hash key lookup - Skip the recursion guard. A Data's members are fixed at creation, so the guard has nothing to protect. - Struct keeps the guard, because a member's method can modify and freeze the struct and then re-enter the same computation. - `to_h`, `deconstruct_keys`: fill the Hash directly. - `inspect`: reserves the buffer up front and stringifies built-in types directly. ## Benchmarks master d71692d49f (2026-10-12) vs. the experimental branch, on x86_64 Linux. | | No JIT | YJIT | ZJIT | |---|---|---|---| | Creation, Struct (geometric mean) | 1.48x | 1.60x | 1.32x | | Creation, Data (geometric mean) | 3.90x | 4.74x | 4.26x | | Basic operations (geometric mean) | 2.37x | 3.28x | 2.87x | "Basic operations" are five frequently used methods: `Data#with`, `Struct#==`, Hash lookup with a Struct key, `Struct#to_h` and `Struct#inspect`. Selected cases (no JIT, ns/op; `S = Struct.new(:a, :b, :c)`, `D = Data.define(:a, :b, :c)`): - `D.new(1, 2, 3)`: 214 ��� 39 - `S.new(1, 2, 3)`: 51 ��� 34 - `Data#with`: 334 ��� 86 - `inspect`: 449 ��� 166 - Hash lookup with a Data key: 250 ��� 85 ruby/ruby-bench (ms; no JIT / YJIT / ZJIT): - struct-new-no-escape: 72���47 / 36���24 / 17���12 - The same benchmark rewritten with Data: 180���60 / 150���29 / 139���24 ## Compatibility checks (on the experimental branch) - 47 checks comparing observable behavior against master, run with no JIT / YJIT / ZJIT: all identical. They cover backtraces, TracePoint, overridden methods, re-entry and frozen state. - All of test/ruby in 4 modes: the same set of failures as master. - ruby/spec core/struct, data, hash, marshal, plus language/pattern_matching and keyword_arguments in 3 modes: 0 failures. ## Links - Experimental branch: https://github.com/riseshia/ruby/compare/d71692d49f...experiment/struct-data-core - Benchmark and check scripts: https://github.com/riseshia/ruby/tree/experiment/struct-new-observable-probe ## Note: the ISEQ initialize idea If incompatibilities were acceptable, another idea is to have `Struct.new` generate an ISEQ initialize for each class it defines, so that it gets the same optimizations as a plain class's initialize. I have not tested how far this could go, but a rough PoC showed the following incompatibilities: - Reflection on initialize changes: - owner changes from Struct to the generated class. - arity is no longer `-1`, and parameters is no longer `[[:rest]]`. - source_location is no longer nil. - `:initialize` appears in `private_instance_methods(false)`. - The TracePoint event changes from `:c_call` to `:call`. - When an exception is raised inside initialize, the backtrace frame changes from `Struct#initialize` to the generated method. Whether these incompatibilities are acceptable, and whether the speedup would be large enough to justify them, is left for further discussion. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/