Port to inheritance-based object model #483
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
Around two years ago, I did some benchmarking of jank's previous object model versus the model we have today. Ultimately, the cost of vtable pointers was very high, when it came to allocating objects. On top of that, the number of interfaces which Clojure JVM objects implement is quite impractical in C++, and in some cases actually impossible.
The outcome was the model we have today, which is closed. Each object is a self-contained type and behaviors are determined via concepts. Type erasure is done by point to a member in each type object and that member holds an enum value of the type of that object. Given the enum, and a large
switch, we can get back to the original type.The details are here: https://jank-lang.org/blog/2023-07-08-object-model/
Challenges
Our closed model is faster than the open model, hands down. But it sacrifices a lot.
Openness
Implementing Clojure protocols, records, and interfaces is going to be quite difficult, since the current model is closed.
Usability from C++
The visitor pattern usage we have, combined with concepts, is quite advanced and relies on C++20 knowledge. It also significantly impacts compile times, since each visitor function needs to be instantiated 50+ times (once for each object type).
Goals for this PR
I would like to add the open object model back in, on this branch, in order to benchmark how it looks in jank today. It will be slower, but how much slower? Slow enough to justify losing all of the openness and functionality and ease of use from C++? It might be. We may never merge this PR, since we'll decide that it's overall still worthwhile.
jank has changed significantly in the last two years, though. I think it's worth checking again.
Process
Here's a rough outline of what needs to be done in order for this branch to be successfully ported.
objectobjectmember from each typed objectdynamic_castassociatively_readable+associatively_writable=>map_likestackable=>stack_likecomparablecollection_likecountableindexablenameable=>namedchunk_likechunkableseqablesequence_likesequenceable_in_place=>in_place_sequence_likesequentialnumber_like(leave this for me)persistentable=>transient_liketransientable=>editablederefableset_likemetadatablecallabletofunction_liketypefromobjectonce we no longer have anyvisit_objectcallsobj_typefrom every typed objectruntime/visit.hppaltogetherI suspect the behaviors above can be combined even further, but we can always combine more once we get things ported.
Porting a behavior
Firstly, ignore
math.cpp. I will take care of it, since it's quite complex.In order to port a particular behavior, we need to do the following:
a. This involves adding the behavior as a base type and then updating the existing functions
i. Make sure the functions are marked
overrideii. Make sure the functions match the exact type from the interface; concepts are more lenient about this and we took advantage of it in some cases
visit_objectcalls and start replacing them with RTTI checks insteadExample visit replacement
Before
After
Keep things working
Our goal here is to keep jank compiling (and passing tests) at each step of the way. So far, I have done that. We can keep our
visit_objectcalls and replace them only as needed, for one behavior at a time. This also allows us to benchmark as we go.I will keep this PR up to date with
main, but it'll get harder and harder to do as we replace more and more.