Skip to content

Conversation

jeaye
Copy link
Member

@jeaye jeaye commented Sep 12, 2025

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.

  • Update each typed object to inherit from base object
  • Remove object member from each typed object
  • Update RTTI functions to use dynamic_cast
  • Add back in polymorphic number ops
  • Create necessary behaviors (we want as few as possible, to minimize vtable ptrs)
    • associatively_readable + associatively_writable => map_like
    • stackable => stack_like
    • comparable
    • collection_like
    • countable
    • indexable
    • nameable => named
    • chunk_like
    • chunkable
    • seqable
    • sequence_like
    • sequenceable_in_place => in_place_sequence_like
    • sequential
    • number_like (leave this for me)
    • persistentable => transient_like
    • transientable => editable
    • derefable
    • set_like
    • metadatable
    • Rename callable to function_like
  • Remove type from object once we no longer have any visit_object calls
    • Also remove obj_type from every typed object
  • Remove runtime/visit.hpp altogether

I 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:

  1. Add a struct with the appropriate name, virtual dtor, and (maybe pure) virtual fns
  2. If the struct name collides with the concept name, feel free to use a temporary suffix and put a TODO; we can easily rename later
  3. Find each object which implements the concept(s) for that behavior and have them implement the interface instead
    a. This involves adding the behavior as a base type and then updating the existing functions
    i. Make sure the functions are marked override
    ii. 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
  4. Make sure everything compiles and commit here
  5. Now go through the various usages of that concept within visit_object calls and start replacing them with RTTI checks instead
  6. If you replace all of them, remove the concept and rename the behavior interface if needed

Example visit replacement

Before

  bool is_seq(object_ref const o)
  {
    return visit_object(
      [=](auto const typed_o) -> bool {
        using T = typename decltype(typed_o)::value_type;

        return behavior::sequenceable<T>;
      },
      o);
  }

After

  bool is_seq(object_ref const o)
  {
    return isa<behavior::sequence_like>(o);
  }

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_object calls 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.

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