Project

General

Profile

Actions

Feature #22501

open

Optimize `Hash.new` and `String.new` in `opt_new`

Feature #22501: Optimize `Hash.new` and `String.new` in `opt_new`

Added by byroot (Jean Boussier) 2 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:127317]

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#initialize in Ruby for more efficient keyword argument handling.
  • Add a path in the existing opt_new to peek the stack for the capacity: keyword argument and pass it to the allocator.
  • Let opt_new call initialize like it would for the default new method.

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

Actions

Also available in: PDF Atom