Linked methods and devirtualization opportunities

Compiler-side investigation found a concrete next speed experiment: direct calls to known final methods while retaining their virtual declarations. Method pruning has useful size potential for some specialized collection programs, but little absolute benefit for the builtins measured here. These are static inventories and models; no compiler/runtime behavior or artifact bytes changed, and no speedup or achieved size reduction is claimed.

Follow-up: direct final-class calls and matched CPU results.

Parent revision: d2afe97e; VM: bac6789. The eight compact builtin/conformance artifacts are the outputs measured in the root ranges report. Four frozen benchmark inputs come from the October 7 baseline; their legacy debug/root encoding makes byte estimates incomparable with the compact outputs. No artifacts were regenerated for this investigation.

What currently retains methods

compiler-artifact’s ReachabilityGraph.visitType marks the complete method range of every reachable class/interface. visitFunction marks its owner, signatures, values, blocks and debug entries. Consequently, referencing a type also keeps methods whose bodies are never called, along with dependencies of those bodies. Basic function dead-code elimination already exists; the opportunity is more precise method reachability.

This retention also satisfies an admission invariant. Native resolve_dispatch_entries considers every retained virtual method and interface-owned declaration, not only declarations referenced by call instructions. For each concrete compatible type it requires a resolvable implementation. Removing an unused implementation while keeping its interface declaration can therefore make an artifact fail admission.

The current linker relocates methodStart but leaves methodCount intact. Selective pruning must rebuild both, retain required implementations/defaults/bridges, relocate references and recompute module identities. Public library fragment assembly (preserveLibraryExports=true) must retain its complete exported callable surface. Optimization belongs in final executable linking, after specialization ownership/reuse has been established.

Modeled size opportunity

The read-only analyzer models a conservative function closure. It retains the entry and every class initializer, follows direct/suspend/task calls, preserves inline debug parent owners, and overapproximates dynamic implementations by method name and parameter count across all modules. Even dynamic sites in otherwise dead bodies contribute. Whenever a retained virtual/interface declaration appears, all matching name/arity candidates remain retained. The model intentionally ignores receiver ancestry and detailed signature discrimination, retaining extra functions.

Two variants are archived. Keeping all existing dispatch declarations finds no removable builtin functions, 17 mapNotNull functions (2841 owned bytes), and two mutableList functions (338 bytes). Allowing unreferenced dispatch declarations to disappear produces the following candidate inventory:

Artifact Functions now Functions outside model closure Their owned records/indexes, KiB Fraction of current file
boot 21 2 0.52 1.48%
edit 53 4 1.05 0.70%
kotlinc 21 6 1.62 4.84%
shell 43 2 0.52 0.45%
vmbench-agent 20 6 1.67 5.26%
vmbench 32 6 1.67 4.43%
kotlin-map-not-null 582 156 70.24 19.34%
kotlin-mutable-list 407 8 2.00 0.76%

These columns count existing function, block, code, exception, root, debug and source-position records plus their index entries. They do not predict the final rewritten artifact size: shared strings/types/constants/imports/paths, section headers, alignment and further reachability changes are excluded. The model is not a replacement for implementing and verifying a linker transformation, and the percentage is not an achieved saving or strict bound.

In mapNotNull, 150 candidates belong to app and six to kotlin.Any; specialized standard-library bodies reside in the application module. Removing only independently linked library modules misses most of this opportunity. Candidate records include unused lastIndexOf, remove, clear, contains implementations/declarations and associated specializations. Of the 71927 owned bytes, CODE accounts for 8591; function/block/root/debug metadata accounts for most of the rest. Most builtin candidates are unused Any.toString/hashCode bodies/declarations. MutableList exercises a much broader method surface, so this conservative model offers little pruning there.

Direct calls to final implementations

K2 lowering selects virtual calls for class methods when the target is non-final or has overridden symbols. The same condition appropriately retains the function’s VIRTUAL flag for calls through a parent declaration. However, call-site selection can separately use a known final target; overriding a parent does not itself imply that a specific source call needs dynamic lookup. Interface-typed calls require a separate proof of their receiver.

Input Virtual sites Interface sites Dynamic sites with concrete final target owner and non-null receiver metadata
kotlin-map-not-null 254 181 228
kotlin-mutable-list 199 120 184
while-indexed-256-300 36 29 32
for-list-256-300 36 30 32
fold-256-300 36 30 32
map-not-null-1024-100-fresh 43 34 37

All six builtins have zero sites matching this specific condition. Collection candidates predominantly target ArrayList and iterator methods such as get, size, add, next, iterator, searches and removal. Counts include cold and potentially unreachable bodies; they are static instruction sites, not runtime invocation frequencies or projected speedup. The final-receiver model finds the same counts on these inputs, without extra interface sites.

The indexed hot fixture declares val data: List<Int> = storage. Its hot reads remain interface calls despite the known original ArrayList allocation. Removing those dispatches needs receiver provenance analysis through aliases/ casts, beyond the narrow final-target experiment. The final-owner count alone does not quantify the hottest work.

A first implementation should change call-site selection while retaining VIRTUAL flags, preserve argument evaluation, null/invalid-reference behavior, super calls, overrides and interface defaults, and respect fake-override targets and library imports. Non-null metadata is a candidate filter, not a sufficient runtime-validity proof. Direct calls to concrete virtual functions already pass the native verifier; no new opcode is needed for that narrow experiment.

Direct call fixed cost is 4 + argument count, virtual 5 + argument count, interface 6 + argument count. Thus compiler optimization can legitimately change guest fixed-unit counters and slice boundaries. Future paired runs must use identical source inputs, compare checksums/semantics and explain expected counter changes rather than requiring every work counter to equal the old artifact. CPU/wall timing should use the updated benchmark harness, with before/after compiler outputs and unchanged VM revision recorded separately.

Recommendation and evidence

Start with the localized final-target lowering experiment and focused dispatch, generic-library, collection and null/super/default-method conformance. Measure construction and hot CPU time on regenerated matched sources; the previous interpreter-only experiments show that fewer apparent lookups do not guarantee a speedup. More precise method pruning is a separate coordinated compiler/linker investigation, particularly useful for mapNotNull-like specializations. Neither result establishes behavior for the unavailable drone PID controller.

The new docs/_tools/analyze_linked_calls.py checks container digest/bounds, indexed sections, call operand framing, block ownership/counts, function layouts and import/export resolution. All 12 recorded inputs also passed the native public artifact verifier. Inventory hashes/totals, candidate count bounds and payload sums agree; repeat runs reproduce the archive. Manual negative checks reject a damaged digest and an invalid instruction frame with a recomputed digest. No Gradle, VM execution, Minecraft or release checks are claimed for this read-only research.

Compact artifact inventory, frozen benchmark inventory, revisions, hashes and exact commands, verification evidence.

Reproduce with python3 docs/_tools/analyze_linked_calls.py ARTIFACT... on the recorded final executable inputs.