Escape analysis might be able to remove the Method allocation of unbound.bind(recv).call(*args).
In fact, TruffleRuby does it for such a pattern.
So the interesting question for me is whether this should be fixed by the JIT or by a new method.
Could you share a benchmark representing your usage?
Whoops, sorry for the belated response -- Redmine email seems to not be working for me. We have a replace_method helper that is a shorthand for doing something like:
orig_require = Kernel.instance_method(:require)
Kernel.define_method(:require) do |*args|
# … do some pre-processing
orig_require.bind(self).call(*args)
end
We use it in a number of places, including to replace require as part of our custom autoloader (c.f. this talk: https://www.youtube.com/watch?v=lKMOETQAdzs)
I'm not quite sure how to get a representative microbenchmark; I am sure I could construct ones where the overhead is anywhere from ~0% to arbitrarily high. Profiling shows that as much as ~6% of the allocation on app startup comes from UnboundMethod#bind calls.
There are also other places I'd like to use this idiom. We had an incident the other day related to Sorbet's use of is_a? (a BasicObject subtype that was being passed around had a surprising is_a? implementation) in runtime checking.
I'd love to be able to grab Object.instance_method(:is_a?) and then use that in our typechecking to make sure that we can test true subtyping no matter what monkey-patches are in place, but we know from past work that adding even a single allocation to the common case of runtime typechecking is too expensive.
I think this makes sense for convenience and better performance on MRI or during interpretation (vs in compiled code).
Using an UnboundMethod for getting a copy of a method at a given time is indeed a good usage, we use it in TruffleRuby quite a bit.
For the specific case of is_a?, may I recommend using Module#===?
That's much less often overridden, and it works for BasicObject (#is_a? is only defined in Kernel, so not for BasicObject).
In fact, TruffleRuby does it for such a pattern.
I wonder that people use this pattern! (I'd never used it except test).
I wonder that people use this pattern! (I'd never used it except test).
This pattern currently represents substantial fraction of allocations that Sorbet runtime does, so building a way to not allocate in this pattern might have a sizeable impact in reducing the overhead of runtime type checking.
Here is a benchmark:
Matz approved the feature. The name "apply" was arguable in some terms:
- We may want to use the name "apply" for other purpose in future.
- This API will be used not so frequently. Only some fundamental libraries (like pp, sorbet-runtime, zeitwerk, etc.) will use it.
I proposed UnboundMethod#bind_call at the developers' meeting, and matz liked it. I'll commit it soon.
- Status changed from Open to Closed
Applied in changeset git|83c6a1ef454c51ad1c0ca58e8a95fd67a033f710.
proc.c: Add UnboundMethod#bind_call
umethod.bind_call(obj, ...) is semantically equivalent to
umethod.bind(obj).call(...). This idiom is used in some libraries to
call a method that is overridden. The added method does the same
without allocation of intermediate Method object. [Feature #15955]