Programming, Software and Code

Turning Contexts into PMCs

I merged the GC branch in yesterday, and though the work wasn't without it's issues the GC API is now in a much better condition then it has been. That out of the way, and AIO still on the back burner, it's time to start looking towards the next project in my list.

This week, the project is converting Parrot's context structures, which at the moment are just normal C structures, into garbage-collectable "Context" PMCs. TT #596 contains some information about the work, although it's currently very sparse. This is not a new idea by any stretch: I've been thinking about it since last summer when I was doing my GC work, and my ambitions were so high I was convinced that all memory allocations in Parrot could be done through the GC—I still think that, but I'm more humbled by the sheer amount of effort required to make that happen. Not only does this project make it easier to manage contexts without all sorts of manual reference counting, there is also going to be a lot of potential for streamlining and optimizing parts of the codebase.

Parrot_Context structures form a linked list of sorts that represent the current calling context and all parent contexts. The context structure contains the array of I, N, S, and P registers too. It also contains a few data fields to help manage the CPS calling system. In short, Contexts are pretty central to Parrot and are very important.

The calling conventions system as it currently stands is pretty inefficient and messy. This isn't a new sentiment, I've blogged about it before (although I can't find a link right now). There are about half a dozen different ways to execute a PIR subroutine, many of those have multiple special-purpose interfaces. Allison is doing some great work in getting things to be more unified, and once she gets her work merged into trunk we will be able to start optimizing the new unified pipeline. A major part of those optimizations is going to be converting Contexts into PMCs.

For an idea of why, consider what happens in the unified calling path when we make a call (note that these aren't exactly in order):
  1. Arguments come in as an array, either as a va_list variadic argument array or as an opcode_t* serialization in bytecode. We also get an invocant object (if any) and a return address. The reason for the difference here is because we can be invoking a subroutine from PIR (via the invoke opcode) or via C (using Parrot_pcc_invoke_from_sig_object)
  2. Arguments and the invocant are added into a CallSignature PMC, which needs to be allocated and initialized.
  3. A new context is created, initialized, and a reference is made to it.
  4. The arguments from the CallSignature PMC are added into the registers of the Context
  5. The invocant, if any, is extracted from the CallSignature PMC and put into the interpreter structure as the current "self".
  6. A new RetContinuation PMC is created to handle a return call and stored in the Context.
  7. Using a series of Call_State structures, and a long loop, extract arguments from the context registers and associate them with parameters in the called sub.
RetContinuations are like Continuations, but single-purpose and lighter-weight, and they're only really used in these cases to provide the behavior of the "return" opcode. If Contexts are a PMC type, we can combine this with the RetContinuation PMC, since the later is never really used without the former. This kills step # 6 in the list above. We can then combine the Context PMC with the CallSignature PMC (since again, the one is almost never used without the other), we can get rid of steps 3, 4, and 5. Finally, combining the Call_State structure with the Context PMC and creating a custom iterator for it will leave us with only this:
  1. Arguments come in as an array, either as a va_list variadic argument array or as an opcode_t* serialization in bytecode. We also get an invocant object (if any) and a return address.
  2. The arguments, the invocant, and the return address are added to the Context PMC
  3. A custom iterator is created for the Context PMC and the subroutine parameters are extracted from it directly.
Quite a bit more streamlined, no? In addition to having fewer steps in the algorithm we also have a number of other significant savings: Fewer PMC allocations, Context PMCs can be allocated from Parrot's memory pools instead of needing to be malloc'd from the OS, and a large number of copying operations can be avoided since the data is put into one place and kept there. Plus, since the Context PMC would suddenly become garbage collectable, we can get rid of all the reference-counting code, and the custom GC hooks to clean up dead context memory. This is only just the beginning, there are major optimizations to be had throughout Parrot once this change happens. It all starts with turning contexts into PMCs though.

Some of my recent work on the GC API refactor has really exposed the current Context system API, and has made it easier to pull individual functions out into the new Context PMC VTABLEs. This is going to be a big project, but it's not unmanageably big. I'm probably going to get started on this work sometime after the next release (which is going to be on May 19th I think). I'll definitely be planning and prototyping before that, however.

This entry was originally posted on Blogger and was automatically converted. There may be some broken links and other errors due to the conversion. Please let me know about any serious problems.