Feature #22501
openOptimize `Hash.new` and `String.new` in `opt_new`
Description
Context¶
Both Hash and String initializer have a capacity: argument, because in performance sensitive code, correctly pre-sizing containers can have a big performance impact when the final size is large, as it saves both a number of reallocations, but also a number of copying / rehashing.
Unfortunately, until now, for smaller capacities, the cost of handling keyword arguments from a C function would tend to outweigh the gains from right-sizing, making it hard to use in cases where the capacity is entirely dynamic and potentially small.
Another concern is from Variable Width Allocation. For performance it's preferable to allocate embedded objects when possible, which requires to
know the size prior to call #initialize, as at that point the slot has already been allocated to the minimum size.
Hence the String.new(capacity:) / Hash.new requires new to be a custom new method to extract the capacity argument and compute the ideal slot size, but doing so defeat the existing opt_new optimization, and cause String#initialize / Hash#initialize to not be called.
Prior Art¶
@tenderlovemaking (Aaron Patterson) opened [Feature #22130] with the idea of a dedicated instruction for String.new but it died off over various concerns, one of which was that adding too many dedicated instruction would risk producing worse code in the interpreter loop.
Solution¶
So instead with @jhawthorn (John Hawthorn) we thought of a smaller change.
- Define
Hash#initializein Ruby for more efficient keyword argument handling. - Add a path in the existing
opt_newto peek the stack for thecapacity:keyword argument and pass it to the allocator. - Let
opt_newcallinitializelike it would for the defaultnewmethod.
Implementation¶
The Hash.new optimization was merged as 7142a3debb with some decent gains, and String.new is being finalized in GH-19674
Concerns¶
However after the Hash.new optimization was merged a subtle behavioral difference was noticed.
Until now, refining Hash#initialize would have no effect when calling Hash.new, but in 4.1 the refined method is invoked by the interpreter (but not by JITs):
module R
refine Hash do
def initialize(*)
raise "Not called prior to 4.1 nor by JITs"
end
end
end
using R
Hash.new
Question¶
What is the correct behavior here? Should this refinement work? or not work? Or is it "implementation defined".
No data to display