Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Proper tco optimization #121

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

CertainLach
Copy link
Owner

@CertainLach CertainLach commented Jun 18, 2023

C++ jsonnet does have proper TCO optimization.
However, I haven't used this approach since it is hard to maintain in some cases, especially when it is used in most of the code.
But now I got an idea: Why not implement a hybrid approach where TCO exists only for the main interpreter loop and not in the rest of the codebase, i.e. builtins?
In this PR I explore this possibility, it may also allow for async builtins/std.parallelMap/huge optimizations in recursive functions/better GC root management strategies.

Stack traces and some other things are broken for now, not ready for use.

@CertainLach
Copy link
Owner Author

Why not implement a hybrid approach where TCO exists only for the main interpreter loop and not in the rest of the codebase, i.e. builtins?

Builtins can be implemented too, I.e by utilizing generators/async-await

In cpp-jsonnet, there is a PR to implement manifestification inside of interpreter loop, and it looks like a cool idea
google/jsonnet#1142

@CertainLach CertainLach mentioned this pull request Apr 26, 2024
pub(crate) struct TcVM {
pub(crate) vals: Fifo<Val>,
pub(crate) ctxs: Fifo<Context>,
pub(crate) apply: Fifo<TailCallApply>,
Copy link
Owner Author

@CertainLach CertainLach Apr 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right here I have 3 stacks, where TailCallApply is an enum with items of varying length, that contains references to other stacks.
This enum value is pretty big, with no means to shrink without moving some things to allocation.
rsjsonnet did funny thing here, where it has 9 stacks instead (https://github.com/eduardosm/rsjsonnet/blob/main/rsjsonnet-lang/src/program/eval/mod.rs#L32-L40), and keeps allocations even, because all the variable-size data is in the other stacks. This is pretty verbose code, with some implicit relations...

But I think it is possible to extend this approach using proc macro:

#[derive(FifoData)]
enum Op {
  A { a: u32, b: u32 },
  B { v: String },
  C { a: Context, b: Val, c: u32, d: String }
  // There should be a way to opt-out from moving data to stacks,
  // the remaining data might be attached to... OpTag enum?
}

Which would expand into

enum OpTag {
  A,
  B,
  C,
}

struct OpFifo {
  ops: Vec<OpTag>,
  u32s: Vec<u32>,
  strings: Vec<String>,
  // Where Val can also be inlined to some extent together with its children, so this proc macro
  // should allow composition of its elements
  vals: Vec<Val>,
  contexts: Vec<Context>,
}

impl Op {
  fn pop(fifo: &mut OpFifo) -> Self {
    match fifo.ops.pop() {
      OpTag::A => Self::A { b: fifo.u32s.pop(), a: fifo.u32s.pop() },
      OpTag::B => Self::B { v: fifo.strings.pop() },
      OpTag::C => Self::C { d: fifo.strings.pop(), c: fifo.u32s.pop(), b: fifo.vals.pop() },
    }
  }
  fn push(self, fifo: &mut OpFifo) {
    match self {
      Self::A {a, b} => { fifo.ops.push(OpTag::A); fifo.u32s.push(a); fifo.u32s.push(b); },
      _ => todo!(),
    }
  }
}

This structure is kinda dumb for the provided enum, but might make sense for the whole jrsonnet?

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