Introducing Derived Binding

This is the text from a Feb 3, 2018 commit that I've reproduced as a forum post for easier editing and commentary.


In R3-Alpha, if you wrote:

o1: make object! [a: 10 b: does [loop 10 [print a]]]
o2: make o1 [a: 20]

...historically what had to happen was that the B function had to have its body structure deep copied, so that all the A references in the B function which had bindings to O1 could be replaced with new A references with bindings to O2.

This was because the invisible "binding" property that an ANY-WORD! could have was part and parcel of the cell, resident in the bit pattern of the array. You couldn't have a single block of data with words in it that could be interpreted at one moment as pointing to one object, and interpreted at another moment as pointing to another object.

This led to a problem where if you have 10 methods in an object, with an average of let's say 10 blocks/arrays in each, with an average of 10 elements per block... then each new instance of that object would cost 10 * 10 * 10 * 4x(sizeof(void*)). That's not counting overhead for the data...that's just what it would cost on each instance to duplicate the methods. Not only would it cost the memory, but it would cost the time taken to duplicate and rebind the blocks...which would be paid for regardless of whether that method was ever called.

(It copied strings and binaries in function bodies too, so it's worse than even that.)

Derived binding removes the need for the copies. Here's how it works:


FUNCTION! value cells have 4 platform-pointer-sized elements. First is the header (as with all REBVAL cells, plus sneaky things that pretend to be cells for only one pointer of size...enough to signal an END). Then there's a pointer to the "paramlist", array w/cells linking parameter names and embedding typesets. Then a pointer to the body, which is a so-called "singular" array--it fits in a series node, but holds the value directly inside it. What goes in that cell varies by dispatcher...which is the C function that interprets the body's contents to execute whatever code is appropriate for the function type.

Slot #4 is used to point to some form of "binding", particular to that instance of a function cell. This mechanism was originally designed for RETURN; so that a single archetypal return paramlist and body could be reused by varied cells that only poked a different value into this binding (in particular, which FRAME! that particular RETURN was supposed to return from).

This so-called "derived binding" expands the concept, where the 4th pointer can also point at one object (at the moment, only one) for which any words in the function body bound to a less derived version of that object will be forwarded. The end result gives something largely indistinguishable from the behavior in R3-Alpha, with significantly less per-instance tax on memory or the GC.

There is a small tradeoff, in that a bound method of this kind pays a small amount of additional runtime cost when looking up words in its body. But it only applies to "methods" made in this way, and there could be several optimizations (for instance, keeping track of a bit on objects of whether they've ever been used as a base class for any other objects, and not checking for a derivation match if not, even if running the body of a method).