NAME ^

docs/dev/pmc_freeze.pod - Freeze/Thaw Design Notes

VERSION ^

This document describes freeze/thaw internals version 0.1. This is not the final implementation.

Overview ^

Freezing or serializing arbitrary PMCs is an interesting problem. Aggregates can hold other aggregates and can be deeply nested, so so a recursive approach could easily blow the stack, especially on embedded systems. Also, aggregates can be self-referential -- they can hold pointers to themselves -- so that working on such structures could create infinite loops.

Coverage ^

Although the file is named pmc_freeze.c it ultimately will deal with every kind of operation that deeply traverses an arbitrary data structures. For example:

freeze

Called from user code to serialize the state of a PMC into some (possibly binary) representation held in a STRING.

freeze_at_destruct

A variant of freeze, possibly called from an exception handler or on resource shortage before interpreter shutdown, to save some data before dying. It must not consume any additional resources.

thaw

The opposite of freeze: reconstruct all PMCs to generate an identical copy of the original frozen PMC. As with freeze, can be called from user code.

dclone

Deeply clone an aggregate. dclone(p) is basically the same as thaw(freeze(p)).

dump, pretty_print

Create a visual representation of an aggregate.

destruction ordering

Find the logical dependencies of a collection of PMCs, so that they can be destroyed in an appropriate order. This is also called on interpreter shutdown.

mark

Mark all objects as being live by calling pobject_lives called from DOD. While the functionality is the same, it will not be implemented on top of this general scheme for performance reasons. This leads to some code duplication, but DOD is run permanently and deserves all the speed it can get.

Description ^

The basic scheme of operation looks like this:

  info = init()
  push todo_list, pmc
  while (todo_list)
      current = shift todo_list
      current->visit(info)
  done.

The visit_info structure ^

This structure holds all necessary information and function pointers specific to the desired functionality. It gets passed on to all vtable methods and callback functions.

Working loops ^

These are labeled visit_loop_*. There are currently two schemes to handle the todo_list.

next_for_GC

All PMCs that can contain other PMCs have the next_for_GC pointer in the PMC's extended data area. The todo_list gets built by appending (or prepending) the current PMC to a mark_ptr, which then points to the current PMC, forming a linked list of items.

This pointer is also used during DOD's mark() functionality, so that DOD has to be turned off during operations using this scheme.

As the next_for_GC pointer is inside the PMC, this scheme isn't thread-safe at low-level, because shared PMCs also would share this pointer, so that there can be only one operation at a time.

todo list

A List called todo holds items still to be worked on. This method is slower and consumes more resources, but doesn't interfere with DOD runs and is thread-safe.

Putting items on the todo list ^

This is done by a callback function inside the visit_info structure called visit_pmc_now. It gets called initially to put the first item on the list and is called thereafter from all PMCs for contained PMCs inside the visit vtable method.

There is another callback visit_pmc_later which adds PMCs to the todo list for later processing, but doesn't do any action on these immediately.

The visit() vtable ^

The general scheme above shows that this method is called for all items on the todo_list. visit has to call visit_pmc_now for all contained PMCs, which then get visited until all is done.

The visit_pmc_now() callback ^

The basic operation is:

  (seen, id) = was_already_seen(pmc)
  do_specific_action(pmc, seen, id)
  if (!seen)
     pmc->visit_action()

Avoiding duplicates ^

As stated in the introduction structures can be self-referential, they can contain (at an arbitrary depth) PMCs, that were already processed. Just following these PMCs would lead to endless loops. So already seen PMCs have to be remembered.

The seen hash

Using a Hash is one method to avoid duplicates. The seen hash holds keys being the address of the PMC and values being a PMC id, which is unique for this PMC. While this is straight forward, it consumes 16 bytes per PMC (plus overhead, 32-bit system assumed). Hash lookups also take a considerable amount of time.

next_for_GC

The pointer used for the todo_list handling itself can serve as a marker that this item was already processed. There are some issues with this though: Plain scalars (not being able to contain other PMCs) don't have a next_for_GC pointer. This is an optimization reducing the size of scalars and increasing performance considerably.

Second, the next_for_GC pointers have to be cleared beforehand. DOD uses only a nibble-sized flag area located inside the PMCs arena to manage, if a PMC was seen already by checking the live bit. The next_for_GC pointer is just set and never cleared to avoid touching a PMCs memory and polluting caches when possible.

Finally, generating a PMC's id isn't as simple as just incrementing a counter used with the seen hash approach.

PMC ids

We could of course use the PMC's address as its own id, since we know it is unique. However, this is suboptimal for thawing. To manage duplicates during thaw we basically need a mapping PMC_in_image => newly_constructed_PMC. When now the PMC_in_image (the id) is the address, we have to use a hash again, for thaw() with all the negative impact on resources and speed.

So both schemes are using small id values and the seen handling inside thaw is done via a list lookup, which is a lot faster and takes less resources.

The seen hash approach just has a counter for PMC ids, the next_for_GC approach calculates the id from the address of the PMC in its arena, again yielding a small and unique number. The two low bits of PMC ids are used as flags.

The actual action ^

So after all we finally arrived at the point to actually perform the desired functionality. First the PMC-specific part is done inside pmc_freeze.c then the specific vtable method freeze, thaw, whatever, is called, again via a function pointer called visit_action.

Freeze and thaw ^

As stated PMCs are currently processed inside the core, PMC-specific parts are done by calling the PMCs vtable method. This parts could of course be moved to default.pmc too, so that it's simpler to override the functionality.

Serializer interface ^

During initialization the visit_infos image_io data pointer is filled with an object having vtable methods that remarkably look like a PMCs vtable. So io->vtable->push_integer spits out an INTVAL to the frozen image, while shift_integer gets an INTVAL from the frozen stream.

This simplifies final changes when image_io becomes just a PMC of some serializer class. There are currently two serializers:

Plain text

This serializer is mainly intended for testing. Having a readable representation of the image simplifies debugging a lot.

Parrot Byte Code

We already have a platform-independent way of reading and writing opcodes, string, and number-constants. So this serializer uses functionality of the pack-file routines. The produced image isn't as dense as it could be though, because all data are aligned at opcode_t boundaries.

Image data format ^

PMC ids ranging from 1 to N-PMCs are shifted left by two, so that the 2 lo bits can serve as flags:

  id + 0x1   ... PMC was seen
  id + 0x2   ... PMC has same type as previous PMC
  id + 0x3   ... escape flag

A PMCs image generally looks like:

  <id><type><pmc-specific-data>

The text representation of the array

  P0 = [P1=666, P2=777, P0]

may look like:

  0xdf4 30 3 0xdf8 33 666 0xdf2 777 0xdf5

  0xdf4 ... PMC id (with "0x" in front for clarity)
  30    ... enum_class_ResizablePMCArray
  3     ... elements count
  0xdf8 ... id of first element
  33    ... enum_class_Integer
  666   ... value
  0xdf2 ... id of second element, same type as prev element
  777   ... value
  0xdf5 ... id of array itself with lo bit set

The escape flag marks places in the image, where additional data will follow. After the escape flag is an int defining the kind of the following data, passed on in extra_flags. During thaw the PMCs vtable is called again, to restore these data. So a PMCs thaw vtable has to check extra_flags if normal or extra data have to be shifted from the image.

This is e.g. needed for PMC properties or arrays containing sparse holes, to set the array index of the following data.

A Integer(666) with a property hash ("answer"=>42) thus looks like:

  0xdfc 33 666 0xdff 2 0xdf4 32 1 answer 0xdf8 33 42

0xdff is the escape mark for the PMC 0xdfc followed by the constant EXTRA_IS_PROP_HASH.

[ To be continued ]

FILES ^

src/pmc_freeze.c, pf/pf_items.c

Author ^

Leopold Toetsch lt@toetsch.at


parrot