Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Is that lookup done all the time (for each invocation of the same method):? Or is it done at a single point in time, like a JIT, but after that the method is “fixed”?

With the possibility to intercept/replace code at runtime (at any time, not just at a resolve/jit stage) it must be very hard for the runtime to optimize calls (i.e make direct calls instead of indirect via method pointer lookup)?

I’m not familiar with how this works in any runtime (V8, Hotspot, ...) so I’m curious which runtimes actually pay one extra method pointer lookup forever and which don’t. I’m guessing the answer for nearly all of them is “it depends”.



Many languages use "inline method caching" - that is, a "call_dynamic x_named_method" bytecode is replaced with a "guard_and_call_static 0xfffffff" once the callsite is evaluated. In some languages invalidating this cache by calling a method with polymorphic arguments or altering the receiving class can be extremely expensive, so there are usually second-order optimizations applied.

Objective-C: Takes one method pointer indirection / jmp trampoline forever, but caches "selectors" on the class so that they become a simple lookup rather than a full evaluation. See sibling comments for better write-ups than I could find.

Most JS runtimes: Trace JITs mean many method invocations are compiled into traces and become either inline instructions or native function calls. Also uses "hidden classes" to implement inline method caching, where the callsite is replaced with the specific address of the call once the dispatch is resolved. https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScrip... , https://blog.ghaiklor.com/optimizations-tricks-in-v8-d284b6c...

Ruby: Also uses inline method caching - when a method is resolved that callsite is replaced with a jump straight to the resolved method in the bytecode. Invalidation used to occur any time a class was modified in any way but has been made more specific over time: https://github.com/charliesome/charlie.bz/blob/master/posts/...


Conceptually for every call. In practice, a language can do less, as long as it always calls the correct method.

How exactly it finds that method is an implementation detail, but fast implementations will cache the results of method lookups. For example, https://developer.apple.com/library/content/documentation/Co... states:

”To speed the messaging process, the runtime system caches the selectors and addresses of methods as they are used. There’s a separate cache for each class, and it can contain selectors for inherited methods as well as for methods defined in the class. Before searching the dispatch tables, the messaging routine first checks the cache of the receiving object’s class (on the theory that a method that was used once may likely be used again). If the method selector is in the cache, messaging is only slightly slower than a function call. Once a program has been running long enough to “warm up” its caches, almost all the messages it sends find a cached method. Caches grow dynamically to accommodate new messages as the program runs.”


The lookup happens on each invocation. It’s alway indirect, and the implementation of a method can be changed at runtime. Method calls aren’t optimized to be direct invocations.


There's some good info on that here:

"Dissecting objc_msgSend on ARM64" https://www.mikeash.com/pyblog/friday-qa-2017-06-30-dissecti...

"Let's Build objc_msgSend" https://www.mikeash.com/pyblog/friday-qa-2012-11-16-lets-bui...

There's also the "Illustrated history of objc_msgSend", although it appears a bit dated, it has some nice commentary on the evolution of this very performance-critical part of the objective-c runtime: http://sealiesoftware.com/msg/index.html




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: