Skip to content

Register @ directly for packaged classes so reads don't scale with class depth - #790

Open
bnaras wants to merge 2 commits into
RConsortium:mainfrom
bnaras:at-first-class-method
Open

bnaras wants to merge 2 commits into
RConsortium:mainfrom
bnaras:at-first-class-method

Conversation

@bnaras

@bnaras bnaras commented Oct 22, 2026

Copy link
Copy Markdown

Follow-up to #723 (hot paths found while profiling CVXR, whose S7 class hierarchies go up to eight levels deep).

x@name reaches @.S7_object through S3 dispatch on class(x), which misses once per inheritance level before it finds the method. This PR registers the same method (prop()) under each packaged class's own name, so dispatch hits on the first lookup and the cost of @ no longer grows with class depth. Behavior is unchanged: it is the same method.

  • new_class() registers the method and records the class name in the package's .__S3MethodsTable__., which is saved with the package (as S7 already does for external methods).
  • S7_on_load() re-registers recorded classes, and S7_on_unload() removes them.
  • Classes without a package are not registered, an existing @ method is never replaced, and R < 4.3 is untouched.

Benchmark (bench/property-access.R, 1000 reads per iteration, separate sessions before and after, R 4.6.1, macOS arm64):

class depth before after
2 610 µs 460 µs 1.3x faster
5 790 µs 460 µs 1.7x faster
8 1.0 ms 460 µs 2.2x faster

prop() and attr() timings are unchanged. The remaining gap between @ and prop() (~200 µs per 1000 reads) is the fixed cost of base R's S3 dispatch for @, which S7 can't avoid.

Trade-off to weigh: this adds one entry per packaged S7 class to base R's S3 methods table, managed by the load and unload hooks. If you'd rather not touch that table, the alternative is documenting prop() as the fast spelling for hot paths.

Tests: new tests in test-hooks.R cover registration, no-package classes, not replacing an existing method, load/unload, and an installed package. The full suite passes.

`x@name` reaches `@.S7_object` through S3 dispatch on `class(x)`, which
misses once per inheritance level before it finds the method. Register
the same method (`prop()`) under each packaged class's own name so the
first lookup hits and the cost of `@` no longer grows with class depth.

`new_class()` registers the method and records the class name in the
package's S3 methods table, which is saved with the package.
`S7_on_load()` re-registers recorded classes and `S7_on_unload()`
removes them. Classes without a package are not registered, and an
existing `@` method is never replaced.

Adds bench/property-access.R. 1000 reads, before -> after:
depth 2 610 us -> 460 us (1.3x), depth 5 790 us -> 460 us (1.7x),
depth 8 1.0 ms -> 460 us (2.2x).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant