<!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Archiving and Interchange DTD v1.0 20120330//EN" "JATS-archivearticle1.dtd">
<article xmlns:xlink="http://www.w3.org/1999/xlink">
  <front>
    <journal-meta />
    <article-meta>
      <title-group>
        <article-title>Threaded-Execution and CPS Provide Smooth Switching Between Execution Modes</article-title>
      </title-group>
      <contrib-group>
        <contrib contrib-type="author">
          <string-name>Dave Mason</string-name>
          <xref ref-type="aff" rid="aff0">0</xref>
        </contrib>
        <aff id="aff0">
          <label>0</label>
          <institution>Toronto Metropolitan University</institution>
          ,
          <addr-line>Toronto</addr-line>
          ,
          <country country="CA">Canada</country>
        </aff>
      </contrib-group>
      <abstract>
        <p>In executing programs, there is a tension among the need to execute quickly, to not take excessive space, and to be able to debug. This paper discusses using a combination of Continuation-PassingStyle for native code in combination with a Threaded-Execution model to address this tension and provide the best of all worlds. Programs can execute at full native speed and then drop instantly into a fully-debuggable execution. Threaded code can be the first level of compilation and then can be easily translated into CPS-style native code that runs approximately 4 times as fast. The execution models can be interleaved seamlessly even within a method.</p>
      </abstract>
      <kwd-group>
        <kwd>eol&gt;Continuation Passing Style</kwd>
        <kwd>Threaded Execution</kwd>
        <kwd>Debugging</kwd>
      </kwd-group>
    </article-meta>
  </front>
  <body>
    <sec id="sec-1">
      <title>1. Introduction</title>
      <p>
        This paper explores diferent execution models for Smalltalk[
        <xref ref-type="bibr" rid="ref1">1</xref>
        ].
      </p>
      <p>
        One of the intrinsic tensions in computer science is the space-time tradeof. Nowhere is this
more obvious than in the compilation process. Many of the most important optimizations in
the compiler-writer’s toolkit exhibit this tradeof and many heuristics have been developed to
address this.[
        <xref ref-type="bibr" rid="ref2">2</xref>
        ]
      </p>
      <p>Beyond optimizations, code representation and execution models both exhibit this. Bytecode
interpreters lie near one end of the spectrum - very compact representation, but slow execution.
Native code lies at the other end of the spectrum - very fast execution, but significantly larger.
Ideally we would like to have small code where it’s not performance critical, and fast code
where it is.</p>
      <p>The rest of this paper is structured as follows: Section 2 describes Continuation Passing Style
and Threaded Execution; Section 3 talks about some of the key implementation parameters that
enable seamless transition between the execution models; Section 4 presents some preliminary
validation of the principles; references to related work are throught the paper; finally, Section 5
provides some concluding thoughts and plans for future work.</p>
    </sec>
    <sec id="sec-2">
      <title>2. Execution Models</title>
      <p>There are a range of execution models that can be used to implement a programming language.
The most common are native code and bytecode interpreters.</p>
      <p>An intermediate option between those extremes is threaded execution. The basic idea is that
programs are constructed by stringing together “words”, where each word is the address of some
code that performs its operation and then passing control to the next “word” without using the
traditional procedure calls. This makes threaded code as easy to generate as bytecode. While it is
not quite as compact as bytecode, it runs significantly faster than byte-code interpreters. Rather
than traditional procedure calls/returns these threads pass along the current continuation (stack
and context in our case), and execute the next “word” via a tailcall.</p>
      <p>If we want to smoothly inter-operate native code with threaded code, we have to write the
native code so it it uses the same parameters. The compilation technique that aligns with those
parameters is continuation passing style.</p>
      <sec id="sec-2-1">
        <title>2.1. Continuation Passing Style</title>
        <p>
          Continuation Passing Style (CPS)[
          <xref ref-type="bibr" rid="ref4 ref5 ref6">4, 5, 6</xref>
          ] is a style of programming where, rather than calling
other functions/methods with an implicit return address, the continuation (the return and the
rest of the computation) is passed explicitly. In the original papers, which were describing
functional programming languages, the continuation was passed as a closure. In our case the
continuation is passed as a stack pointer and a Context.
        </p>
        <p>In CPS, the control flow is explicit in the form of jumps. Thus all returns are in the form
of tail calls that pass control to the next function without pushing any parameters or return
addresses onto a stack. Return is explicit by tail-calling a continuation (the return address in
the Context).</p>
        <p>
          In our system, CPS means that a call to a new method involves saving the address of the next
code - that is the next word in the threaded representation, and the next native function - in
the Context, and then tail-calling the new method. Returning is simply tail-calling the saved
address. In assembler/machine code the address of the next code is directly accessible. In a
higher-level language such as our implementation language, Zig[
          <xref ref-type="bibr" rid="ref7 ref8">7, 8</xref>
          ], the address must be of
a function, so functions must be split at call points. If a call point is within a loop, the loop
now would span multiple functions, so the loop head is another point where functions must
be split. Figure 1 shows a simple function in Zig written in normal style. Figure 2 shows the
same function in CPS. The context.save and context.get are storing and retrieving the variables
n and sum at their temp locations (0 for n and 1 for sum) in the context. This is simplified from
the CPS we actually use, for expository purposes. For the same reasons, we are not using the
actual Zig syntax for the tail-calls, but rather use tailcall. Here foo is split at the top of the loop
because the loop contains a call. foo1 contains the loop test followed either by saving the return
address of foo2 and calling bar, or by passing the result back to the calling Context. foo2 has
the tail of the loop, and then goes back to the top of the loop. Since on most architectures there
are more eficient ways to access values on the stack than at arbitrary locations in memory,
there may be a small cost for using the CPS, but if all the methods on Context are inlined (which
they can be in Zig), the cost will be minimal.
        </p>
        <p>1 const n = 0; // position in context
2 const sum = 1; // position in context
3 fn foo(caller: Context) void {
4 const newContext = caller.push();
5 newContext.save(n,10);
6 newContext.save(sum,0);
7 tailcall foo1(newContext);
8 }
9 fn foo1(context: Context) void {
if (context.get(n)&gt;0) {
context.setReturn(foo2);
tailcall bar(context,context.get(n));
10
11
12
13 }
14 const returnC = context.pop();
15 tailcall returnC.getReturn()(returnC,</p>
        <p>context.get(sum));
1 fn foo() u64 {
2 var n:u64 = 10; 16 }
3 var sum: u64 = 0; 17 fn foo2(context: Context, result: u64)
4 while (n&gt;0) { void {
sum=sum+bar(n); 18 context.save(sum,context.get(sum)+result
n=n-1; );
19 context.save(n,context.get(n)-1);</p>
        <p>tailcall foo1(context);
5
6
7 }
8 return sum; 20
9 } 21 }
10 fn bar(v: u64) u64 { 22 fn bar(caller: Context, v: u64) void {
11 return v+1; 23 tailcall caller.getReturn()(caller,v+1);
12 } 24 }</p>
      </sec>
      <sec id="sec-2-2">
        <title>2.2. Threaded Execution</title>
        <p>Threaded Execution is a form of execution where rather than a sequence of calls to other
functions, a function is a sequence of addresses of functions. The hardware stack is unchanged
by the sequence of functions (although internally any of them may do so, as long as there is no
net change). Figure 3 gives an example of a normal function that calls a sequence of functions.
For each call, the language would push the parameters (just ptr in this case) and the return
addresses onto the hardware stack.1</p>
        <p>
          Figure 4 shows the same sequence in threaded mode. Note that since each threaded function
passes control to the next, there is no activity on the hardware stack. Since the pc and ptr
parameters are likely to be passed in registers, there is no overhead of memory trafic apart from
fetching the next function address, and the only other overhead is advancing pc to point to the
1Note that the [*] Zig syntax means a pointer to multiple values.
1 fn foo(_ptr: [*]data) [*]data {
2 var ptr = _ptr;
3 ptr = foo1(ptr);
4 ptr = foo2(ptr);
5 ptr = foo3(ptr);
6 return ptr;
7 }
8 n foo1(ptr: [*]data) [*]data {
9 // do something using the data at ptr,
10 // possibly modifying to newPtr
11 return newPtr;
12 }
13 ...
1 const foo = [_]ThreadedFn {&amp;foo1,&amp;foo2,&amp;foo3,&amp;end};
2 fn executeFoo(ptr: [*]data) [*]data {
3 tailcall foo[0].*(&amp;foo[
          <xref ref-type="bibr" rid="ref1">1</xref>
          ],ptr);
4 }
5 fn foo1(pc: [*]ThreadedFn, ptr: [*]data) [*]data {
6 // do something using the data at ptr,
7 // possibly modifying to newPtr
8 tailcall pc[0].*(pc+1,newPtr);
9 }
next threaded function. However that is additional overhead, as there is a level of indirection
not found in normal execution, and the advancing of the program counter is something that is
automatically done by the hardware for normal program execution.
        </p>
        <p>
          The first example of threaded program execution known to the author, was the FORTRAN
compiler for the PDP-11 [
          <xref ref-type="bibr" rid="ref9">9</xref>
          ]. In order to get the compiler available as quickly as possible after
the introduction of the machine, the manufacturer generated threaded code where some of
the threaded words were boilerplate and others did simple combinations of operations. This
was certainly not an optimizing compiler, but it performed quite well, partially because the
PDP-11 had an addressing mode (jmp @(r5)+) that made the transfer instruction at the end
of a word be a single instruction.
        </p>
        <p>
          The same instruction made the first FORTH [
          <xref ref-type="bibr" rid="ref10 ref11">10, 11</xref>
          ] implementation particularly performant,
and made writing one’s own implementation of FORTH a fun weekend project.
        </p>
        <p>
          [
          <xref ref-type="bibr" rid="ref12">12</xref>
          ] describes 3 kinds of interpreters including byte-coded, direct threaded (what we describe
in this work), and indirect threaded. While direct threaded produces the best results, they
characterize indirect threaded as more flexible. We attain that flexibility with other mechanisms
that are beyond the scope of this paper.
        </p>
        <p>
          Threaded code has been used in Smalltalk compilers [
          <xref ref-type="bibr" rid="ref13 ref14">13, 14</xref>
          ] as well as OCaml. In both of
these systems, the native byte-code has been translated to threaded code to good efect.
        </p>
        <p>
          The SableVM compiler converts Java byte codes to a threaded execution model [
          <xref ref-type="bibr" rid="ref15">15</xref>
          ].
[
          <xref ref-type="bibr" rid="ref16">16</xref>
          ] describes using selective inlining to make direct threaded code close to native
performance.
        </p>
        <p>
          [
          <xref ref-type="bibr" rid="ref17">17</xref>
          ] describes a related technique they call indirect-threaded code.
        </p>
      </sec>
    </sec>
    <sec id="sec-3">
      <title>3. Implementation</title>
      <p>To enable seamless transition between CPS and threaded execution we have to make some
particular implementation decisions.</p>
      <p>1. the stack cannot reasonably be woven into the hardware stack with function calls;
2. contexts have to contain not only native return points, but also threaded return points;
3. CompiledMethods have to facilitate seamless switching between execution modes.</p>
      <p>But first let’s look at some background on the memory model.</p>
      <sec id="sec-3-1">
        <title>3.1. Memory Model</title>
        <p>The Smalltalk system we are building is designed to support multiple Smalltalk processes
executing simultaneously on multiple cores.2 Each process has a private stack. In addition there
is a heap, details of which are outside the scope of this paper. Most objects are allocated on the
heap, with the exception of three kinds of objects described below.</p>
        <p>
          As was observed by Deutsch and Schifman [
          <xref ref-type="bibr" rid="ref18">18</xref>
          ], while Context objects (Smalltalk activation
records) semantically are heap allocated, in practice they are almost always used in a strictly
last-in-first-out pattern and can therefore profitably be allocated on a stack. As long as we can
reliably recognize escaping values and promote them to the heap and thus maintain the required
semantics, it is a performance win. Objects that are allocated on the stack (in a Context) include
the Context itself, referenced as thisContext, BlockClosure objects which may be created
as part of a Context, or ClosureData objects referred to by the Context and some BlockClosure.
        </p>
        <p>Values can escape in one of two ways:
1. a reference to an object is assigned to a heap object (since we must maintain the invariant
that an object cannot point to a younger area of storage, the pointed-to object must be
promoted to the heap);
2. a reference to an object is returned from the Context in which it is defined, in which case
we must promote the object to the heap.</p>
        <p>Promotion of BlockClosure or Context objects may force the promotion of ClosureData objects
that they reference. Promotion of a Context object may force the promotion of other Context
objects that it references. The word ’may’ is because, while the object must come to reside in
the heap, the object may have already been promoted to the heap.
2The Smalltalk processes are executed with operating system threads, but we will use ’process’ to avoid confusion
with ’thread’ed execution.</p>
      </sec>
      <sec id="sec-3-2">
        <title>3.2. Interface Points and Data Structures</title>
        <p>There are 3 data structures that are the connection points for moving between the execution
models: the Stack, the Method, and the Context. Figure 5 shows the relationship among the
header
ctxtStructure
d
o
h selector
t
e</p>
        <sec id="sec-3-2-1">
          <title>2M code *</title>
          <p>M
· · ·
references *</p>
        </sec>
        <sec id="sec-3-2-2">
          <title>M3 context</title>
        </sec>
        <sec id="sec-3-2-3">
          <title>M2 context Stack</title>
          <p>· · ·
header ?
threaded PC ?
native PC ?
previous
tcNumber
method
header ?
method
threaded PC
native PC
previous
tcNumber
· · ·
?
?
?
· · ·
?
?</p>
        </sec>
        <sec id="sec-3-2-4">
          <title>M3 stack</title>
          <p>parameter 2 for M3
parameter 1 for M3
self/result for M3</p>
        </sec>
        <sec id="sec-3-2-5">
          <title>M2 stack</title>
          <p>temp1 for M2
self/result for M2</p>
        </sec>
        <sec id="sec-3-2-6">
          <title>M1 stack</title>
          <p>Heap
header
method
subsequent
t
x
te next word
n
o
c previous
1
M tcNumber
temp1 for M1
temp2 for M1
self for M1
header
method
t
x
ten ignored
o
c
e exit CPS
s
a
Bprevious=Nil
tcNumber
stack, heap, Contexts, and Methods. This represents the state of execution where a method, M1,
has called M2, which has called M3 which is executing, but hasn’t called any other method.
3.2.1. Stack
The first decision is to not use the hardware stack, but instead use a stack allocated per process,
with overflow to the heap. In addition to supporting this multiple execution model, by putting
all roots in this stack there is no confusion or complexity of tagged values, tagged pointers,
and native pointers; everything on the stack is tagged. This simplifies the garbage collector
significantly.
3.2.2. Method
Every CompiledMethod regardless of execution model has a code area (at a fixed ofset in the
CompiledMethod object). Execution of a CompiledMethod begins with a threaded call to the
ifrst word. The interpretation of the remainder of the code area is completely defined by this
function.</p>
          <p>1. For a pure native code method, this will be an array of pointers to the CPS chunks.
2. For a pure threaded method, this will be the threaded code addresses, preceded by a
selectorVerify function - which might be replaced at some point with a pointer to native
code.
3. For native code backed by threaded code, this would be as in the previous example, except
the first word points to the native code.
4. For interpreted code, such as the proof of concept mentioned in Section 4, the first word
points to the interpreter, and the remainder of the area would contain the byte-codes.</p>
          <p>This could even support multiple interpreters for diferent byte-codes.
3.2.3. Contexts
A Context is the representation of a method/function activation record or stack frame. These
are initially allocated on the top of the stack, but may migrate to the heap when the stack gets
too large or the Context escapes, as explained in Section 3.1. When they are allocated on
the stack, they are only partially populated, because they will likely be discarded before they
need to be treated as proper objects. Before a Context migrates to the heap, the remaining
ifelds are filled in and it can then be treated as a first-class object and can be used to implement
Scheme’s call-with-current-continuatiion, light-weight threads, or other control
lfows as well as return.</p>
          <p>Method pointer points to the method/function for the context. This is set up when the
context is created on the stack.
threaded PC is the address of the next pointer in the threaded version of the method. This is
only filled in when a method/function is called.
native PC is the address of the next CPS part of the native implementation. This is only filled
in when a method/function is called. For a native CPS method, this will be the next CPS chunk
address If there is no native implementation of this method, this will be the next threaded word
in the method. If this method were interpreted, this would be the address of the interpreter.
Therefore return from a call always just invokes the native PC, passing the threaded PC.
Previous context points to the context that invoked the current method/function. This is
set up when the context is created on the stack.
tcNumber is used to handle non-local returns eficiently in the face of handling exceptions.
This is set up when the context is created on the stack.
temps are the temporary values for the method. They are initialized to Nil when the context
is created on the stack.</p>
          <p>BlockClosure and ClosureData references Any BlockClosure or ClosureData is allocated
above the Context, with references here. They are accessed as temp indices.
parameters are the parameters being passed to the method. This is part of the caller’s stack.
These, and everything above are discarded when the method returns. They are accessed as
temp indices.
result is the location of the result of the current function, and also is the self value in an
object-oriented language or the first parameter in non-OO languages. As part of the caller’s
stack, it will be left on the stack when the method returns. It is accessed as the last temp index.
stack is the rest of the caller’s stack. This is diferent from a context in traditional Smalltalk
VMs, where the stack for the method is part of the method’s context. The reason for the change
is that it eliminates the need to move the stack values around in the normal context-on-stack
case. On return from a Context that resides in the heap, the result value and stack will need to
be copied to the stack, becoming the complete stack, as all of the Contexts implicitly reside in
the heap.</p>
          <p>If a Context is on the stack, then when it is returned from, all of the context before the result
is discarded by simply adjusting the stack pointer. This makes the round-trip cost of creating
and deleting a Context on the order of 20 instructions - only a small factor worse than a native
function call.</p>
          <p>If a Context is on the heap, then when it is returned from, the stack portion of the context is
copied to the stack area. Combined with the creation and migration of the Context to the
heap this is about 3-4 times the round-trip cost of the simpler on-stack cost.</p>
          <p>
            Our contexts are similar to [
            <xref ref-type="bibr" rid="ref19">19</xref>
            ] except that they have a native and a threaded return address,
and that the stack and parameters are from the sender in a more natural way.
          </p>
        </sec>
      </sec>
      <sec id="sec-3-3">
        <title>3.3. Primitives</title>
        <p>Smalltalk is conceptually a very simple language. The only operations are message sends,
assignment of simple variables, and return. The connection to the underlying hardware all
happens through primitives. Figure 6 shows the addition operation for SmallInteger. The
annotation &lt;primitive: 1&gt; says that this method will attempt to evaluate by invoking
primitive 1, which is implemented in the runtime/virtual-machine. If the primitive is successful,
&lt;primitive: 1&gt;
^ super + aNumber
the parameters will be consumed and the result returned. If the primitive fails, then the following
Smalltalk code will be executed with the parameters unchanged. This gets translated into the
Zig code in Figure 7. Here the &amp;primitive.p1 refers to primitive 1. Normally a Context
1 &amp;primitive.p1,
2 &amp;embedded.superTailSend,sym.@"+",
would be created if the primitive failed. However in this case, since there are no local variables,
and there is only one message send in tail-call position we avoid creating a Context.</p>
        <p>
          The Zig code for the primitive is shown in Figure 8. It first checks that the message sent was
1 pub fn p1(pc: [*]const Code, sp: [*]Object, process: *Process,
context: ContextPtr, selector: Object) void {// SmallInteger
&gt;&gt;#+
2 if (!sym.@"+".equals(selector))
3 tailcall dnu(pc,sp,process,context,selector);
4 sp[
          <xref ref-type="bibr" rid="ref1">1</xref>
          ] = inlined.p1(sp[
          <xref ref-type="bibr" rid="ref1">1</xref>
          ],sp[0]) catch
5 tailcall pc[0].prim(pc+1,sp,process,context,selector);
6 tailcall context.npc,(context.tpc,sp+1,process,context,
selector);
7 }
+3. Then it calls the code that does the tagged addition, passing it self and aNumber. If it
fails (aNumber wasn’t a SmallInteger, or the sum doesn’t fit a SmallInteger), we fall through to
the rest of the method, executed in threaded mode. The assumption is that failure is rare, so
threaded code is adequate. If the addition succeeded, we return to the method that sent the +
message in the first place.
        </p>
        <p>For completeness, Figure 9 shows the inline code that does the sum operation. We assume
that self is a SmallInteger, and if the other value is a SmallInteger, and the result of the
summation is a SmallInteger, then we return that new object. Otherwise we return an error.</p>
        <p>
          One of the key compilation/performance approaches for this system is aggressive inlining
of Smalltalk code. The details are outside the scope of this paper, but if a method is inlined
3The reason we check is related to how dispatch is handled, and is outside the scope of this paper.
5
6
7
8 }
}
return error.primitiveError;
1 pub inline fn p1(self: Object, other: Object) !Object { // Add
2 if (other.isInt()) {
3 const result =
4 @bitCast(Object,self.i()+%other.toUnchecked(i64))
;
if (result.isInt()) return result;
that starts with a primitive, we need to embed that primitive call in the method that sends the
message. This looks like &amp;embedded.p1 and Figure 10 shows the code that is referenced in
this case. For a method to invoke an embedded primitive that could fail, a Context must have
1 pub fn @"+"(pc: [*]const Code, sp: [*]Object, process: *Process
, context: ContextPtr, selector: Object) [*]Object {
2 sp[
          <xref ref-type="bibr" rid="ref1">1</xref>
          ] = inlines.p1(sp[
          <xref ref-type="bibr" rid="ref1">1</xref>
          ], sp[0]) catch
3 tailcall fallback( pc + 1, sp, process, context, Sym.@"
+" );
4 tailcall pc[0].prim( pc + 1, sp + 1, process, context,
selector );
5 }
already been created. Because this was not a message send, we don’t have to check that the
selector is correct. If the inline summation fails, we invoke a fallback - sending the + method to
the object. Otherwise, we pass control to the next threaded word, having updated the stack.
        </p>
      </sec>
      <sec id="sec-3-4">
        <title>3.4. Switching between Threaded and CPS</title>
        <p>As mentioned in Section 3.2.2 when execution of a method commences, the first threaded word
of the code area is invoked. This means that execution will proceed in the manner defined by
that method, so threaded code can call native code and native code can call threaded code with
no conditional code required.</p>
        <p>Similarly, as mentioned in Section 3.2.3 when a return is made to a Context, the native PC
ifeld is invoked, passing the threaded PC field. Hence any kind of executing mode can return to
any other with no conditional code required.</p>
        <p>In fact, moving in both directions could easily support other execution models such as
interpreters.</p>
        <p>Starting Debug Mode The first three modes of program execution described in Section 3.2.2
seamlessly support interruption and single-stepping of code. Support for interpreter execution
modes would need to be implemented slightly diferently, e.g. with recognition that the return
was an interpreter and setting the return to a debugging entry point to the interpreter.. The first
word for each method, along with checking for the correct selector, checks to see if interruption
is required. This interruption could be debugging, interaction with the global garbage collector,
a user or other process requesting interruption, etc. Performance would be negatively afected
if this checking is performed too frequently, so it currently is done at the entry point for a
method, and at the top of loops.</p>
        <p>While executing within native code, if an interruption is required, execution is switched to
threaded execution, which can easily be single-stepped. The native PC field in a Context can
similarly be set to the threaded function equivalent, which is accessible from the threaded PC
ifeld.</p>
      </sec>
    </sec>
    <sec id="sec-4">
      <title>4. Validation &amp; Results</title>
      <p>This is still a work in progress, but here we will describe a hand-compiled implementation of
ifbonacci.</p>
      <sec id="sec-4-1">
        <title>4.1. Versions of fibonacci code</title>
        <p>1 fibonacci
2 self &lt;= 2 ifTrue: [ ^ 1 ].
3 ^ (self - 1) fibonacci + (self - 2) fibonacci</p>
        <p>Figure 11 shows a simple Smalltalk version.</p>
        <p>Figure 12 shows the threaded implementation.4 All of the benchmarks use direct early-binding
of the recursive calls to make them as comparable as possible (dynamic dispatch is covered in
another paper). This is similar to the IR code for the Integer&gt;&gt;#fibonacci function in
Pharo. p5 is &lt;= which leaves a boolean on top of the stack. If true, then lines 6-8 replace self
with 1, and then return. Line 10 creates a Context (which we hadn’t needed because no error
was possible and we hadn’t called anything). Then we subtract the literal 1 from self and call
recursively in lines 11-13, then the same -2, and finally at lines 17-18 we add the values and
return.</p>
        <p>Figure 13 shows the first part of the CPS native code paralleling the threaded code. This
covers lines 1-13 of Figure 12, Lines 3-6 check for the base case and return 1. Lines 7-8 create the
Context (the 0, 2, 0 parameters say 0 locals, 2 max needed stack, and self is temp/local 0, and
fibThread refers to the code in Figure 12). Line 9 tries to subtract 1. If it fails, line 10 switches
4This is somewhat de-optimized from the actual benchmark for explanatory purposes.
&amp;embedded.verifySelector,
":recurse",
&amp;embedded.dup, // self
&amp;embedded.pushLiteral, Object.from(2),
&amp;embedded.p5, // &lt;=
&amp;embedded.ifFalse,"label3",
&amp;embedded.drop, // self
&amp;embedded.pushLiteral1,
&amp;embedded.returnNoContext,
":label3",
&amp;embedded.pushContext,"^",
&amp;embedded.pushLocal0, // self
&amp;embedded.pushLiteral1,
&amp;embedded.p2, //
&amp;embedded.callRecursive, "recurse",
&amp;embedded.pushLocal0, //self
&amp;embedded.pushLiteral2,
&amp;embedded.p2, //
&amp;embedded.callRecursive, "recurse",
&amp;embedded.p1, // +
&amp;embedded.returnTop,
to the threaded code to try the operation, because we obviously have underflow. 5 Line 11-13
sets up for, and makes the recursive call. Line 11 is pointing to line 14 of Figure 12, and line 12
points to the actual code.</p>
        <p>Figure 14 shows the second part of the CPS code, corresponding to lines 14-16 of Figure 12.</p>
        <p>Finally, Figure 15 shows the end of the CPS code, including possible overflow. Line 8 calls
back the native PC from the saved Context.</p>
        <p>5Can’t actually underflow here, but this is the correct code.
}
const newContext = context.push(sp,process,fibThread.</p>
        <p>
          asCompiledMethodPtr(),0,2,0);
const newSp = newContext.asObjectPtr()-1;
newSp[0] = inlined.p2L(sp[0],1) catch tailcall pc[
          <xref ref-type="bibr" rid="ref10">10</xref>
          ].prim(
pc+11,newSp+1,process,context,fibSym);
newContext.setReturnBoth(fibCPS1, pc + 13); // after first
callRecursive (line 15 above)
tailcall fibCPS(fibCPST+1,newSp,process,newContext,fibSym);
1 fn fibCPS1(pc: [*]const Code, sp: [*]Object, process: *Process,
context: ContextPtr, _: Object) void {
2 const newSp = sp-1;
3 newSp[0] = inlined.p2L(context.getTemp(0),2) catch tailcall
pc[0].prim(pc+1,newSp,process,context,fibSym);
4 context.setReturnBoth(fibCPS2, pc + 3); // after 2nd
callRecursive (line 19 above)
tailcall fibCPS(fibCPST+1,newSp,process,context,fibSym);
1 fn fibCPS2(pc: [*]const Code, sp: [*]Object, process: *Process,
context: ContextPtr, selector: Object) void {
2 const sum = inlined.p1(sp[
          <xref ref-type="bibr" rid="ref1">1</xref>
          ],sp[0]) catch tailcall pc[0].
        </p>
        <p>prim(pc+1,sp,process,context,fibSym);
3 const result = context.pop(process);
4 const newSp = result.sp;
5 newSp[0] = sum;
6 const callerContext = result.ctxt;
7 tailcall callerContext.npc(callerContext.tpc,newSp,process,
callerContext,selector);
8 }</p>
        <p>The Pharo data is from running on Pharo 10, on the Pharo version of the OpenSmalltalk VM.
Pharo JIT is running on a JIT VM, and Pharo Stack is using the Stack Interpreter VM.</p>
        <p>Native is the straightforward implementation in Zig using 64 bit integers. Not surprisingly it
is very fast, but interestingly the native optimized code for this micro-benchmarks is also very
small.</p>
        <p>Object is the straightforward implementation in Zig using tagged Objects, with local Zig
variables and recursion. This is as good as we could hope to be, using the well-supported
hardware stack and call/return semantics. It also demonstrates that using tagged integers
doesn’t add too much overhead.</p>
        <p>CPS is the CPS conversion of this using our Context objects. This doesn’t use any of the
standard conventions - stack, hardware recursion, static types. The fact that this is only 20%
slower, at least on AArch64 is surprisingly good. It’s also 10-40% faster than the Pharo JIT’ed
code. We don’t know the size of the Pharo JIT’ed code, but we suspect that the CPS code is
larger.</p>
        <p>Threaded is the fully threaded version. This is extremely easy to generate, and is rewarding
that it is only 2-3 times slower than the Pharo JIT’ed code. There was almost no optimization
available for such a simple method, so we are very optimistic about the potential for our system.
The threaded code is about 25 the size of the CPS code and 3-5 times slower, which seems like a
good trade-of.</p>
        <p>ByteCode is an unoptimized byte-code interpreter. It is about 12 the size of the threaded
version and almost 4 times slower. Since it doesn’t save much space, and is actually slightly
more dificult to generate than the threaded version, it doesn’t seem worth pursuing at this
time.</p>
        <p>As a micro-benchmark of hand-compiled code, this should be taken as simply a validation
that this a viable approach to a Smalltalk runtime.</p>
      </sec>
    </sec>
    <sec id="sec-5">
      <title>5. Conclusion &amp; Future Work</title>
      <p>Summary We have shown how we support two (or more) modes of program execution,
particularly the seamless transition between the modes. This supports code in either dense or
fast modes. We also can easily enter and exit debugging modes.</p>
      <p>The decisions to support the easy simultaneous support for native (CPS) and threaded
execution (use of a separate stack, and our design for contexts and methods), seem to have been
validated. Now we can work on the optimizations</p>
      <p>The current benchmark cannot be taken too seriously, but appears to show, in some cases,
that the CPS form is only 20% slower than optimal, and the threaded code is only a factor of 5
slower.</p>
      <p>Future Work This describes work in progress. The next stage is to automatically compile
code so we can get some real performance numbers.</p>
      <p>Then we will be working on the optimizations that are enabled by the decisions that we’ve
discussed here as well as others we’re exploring. We are very optimistic.</p>
      <p>As mentioned in Section 3.1, Context, BlockClosure, and ClosureData objects are initially
allocated on the stack. This could also apply to small, known-sized objects, and experiments
should be run to see if this is advantageous.
1984, p. 290–296. URL: https://doi.org/10.1145/800017.800541. doi:10.1145/800017.
800541.
[21] PLDI, Conference Record of the 1993 ACM SIGPLAN Conference on Programming
Language Design and Implementation, volume 28, Association for Computing Machinery,
Albuquerque, NM, USA, 1993.
[22] Proceedings of the 11th Annual ACM Symposium on Principles of Programming Languages,
POPL ’84, Association for Computing Machinery, 1984.</p>
    </sec>
  </body>
  <back>
    <ref-list>
      <ref id="ref1">
        <mixed-citation>
          [1]
          <string-name>
            <given-names>A.</given-names>
            <surname>Goldberg</surname>
          </string-name>
          ,
          <string-name>
            <given-names>D.</given-names>
            <surname>Robson</surname>
          </string-name>
          , Smalltalk-
          <volume>80</volume>
          :
          <article-title>The Language and its Implementation, AddisonWesley</article-title>
          , Don Mills, Ontario,
          <year>1983</year>
          . URL: https://rmod-files.lille.inria.fr/FreeBooks/BlueBook/ Bluebook.pdf.
        </mixed-citation>
      </ref>
      <ref id="ref2">
        <mixed-citation>
          [2]
          <string-name>
            <given-names>T.</given-names>
            <surname>Ball</surname>
          </string-name>
          ,
          <string-name>
            <given-names>J. R.</given-names>
            <surname>Larus</surname>
          </string-name>
          ,
          <article-title>Branch prediction for free</article-title>
          ,
          <source>in: [21]</source>
          ,
          <year>1993</year>
          , pp.
          <fpage>300</fpage>
          -
          <lpage>313</lpage>
          .
        </mixed-citation>
      </ref>
      <ref id="ref3">
        <mixed-citation>
          [3]
          <string-name>
            <given-names>R. E.</given-names>
            <surname>Johnson</surname>
          </string-name>
          ,
          <string-name>
            <given-names>J. O.</given-names>
            <surname>Graver</surname>
          </string-name>
          ,
          <string-name>
            <given-names>L. W.</given-names>
            <surname>Zurawski</surname>
          </string-name>
          ,
          <string-name>
            <surname>Ts:</surname>
          </string-name>
          <article-title>An optimizing compiler for smalltalk</article-title>
          ,
          <source>SIGPLAN Not</source>
          .
          <volume>23</volume>
          (
          <year>1988</year>
          )
          <fpage>18</fpage>
          -
          <lpage>26</lpage>
          . URL: https://doi.org/10.1145/62084.62086. doi:
          <volume>10</volume>
          .1145/ 62084.62086.
        </mixed-citation>
      </ref>
      <ref id="ref4">
        <mixed-citation>
          [4]
          <string-name>
            <given-names>A. W.</given-names>
            <surname>Appel</surname>
          </string-name>
          , T. Jim,
          <article-title>Continuation-passing, closure-passing style</article-title>
          ,
          <source>in: Proceedings of the 16th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL '89</source>
          ,
          <string-name>
            <surname>Association</surname>
          </string-name>
          for Computing Machinery, New York, NY, USA,
          <year>1989</year>
          , p.
          <fpage>293</fpage>
          -
          <lpage>302</lpage>
          . URL: https://doi.org/10.1145/75277.75303. doi:
          <volume>10</volume>
          .1145/75277.75303.
        </mixed-citation>
      </ref>
      <ref id="ref5">
        <mixed-citation>
          [5]
          <string-name>
            <given-names>A. W.</given-names>
            <surname>Appel</surname>
          </string-name>
          , Compiling with Continuations, Cambridge University Press,
          <year>1992</year>
          .
        </mixed-citation>
      </ref>
      <ref id="ref6">
        <mixed-citation>
          [6]
          <string-name>
            <given-names>C.</given-names>
            <surname>Flanagan</surname>
          </string-name>
          ,
          <string-name>
            <given-names>A.</given-names>
            <surname>Sabry</surname>
          </string-name>
          ,
          <string-name>
            <given-names>B. F.</given-names>
            <surname>Duba</surname>
          </string-name>
          ,
          <string-name>
            <given-names>M.</given-names>
            <surname>Felleisen</surname>
          </string-name>
          ,
          <article-title>The essence of compiling with continuations</article-title>
          ,
          <source>in: [21]</source>
          ,
          <year>1993</year>
          , pp.
          <fpage>237</fpage>
          -
          <lpage>247</lpage>
          .
        </mixed-citation>
      </ref>
      <ref id="ref7">
        <mixed-citation>
          [7]
          <string-name>
            <given-names>Z.</given-names>
            <surname>Foundation</surname>
          </string-name>
          ,
          <article-title>Zig is a general-purpose programming language and toolchain for maintaining robust, optimal</article-title>
          , and reusable software,
          <year>2022</year>
          . URL: https://ziglang.org.
        </mixed-citation>
      </ref>
      <ref id="ref8">
        <mixed-citation>
          [8]
          <string-name>
            <given-names>A.</given-names>
            <surname>Kelly</surname>
          </string-name>
          , Zig,
          <year>2022</year>
          . URL: https://ziglang.org/.
        </mixed-citation>
      </ref>
      <ref id="ref9">
        <mixed-citation>
          [9]
          <string-name>
            <given-names>J.</given-names>
            <surname>Bell</surname>
          </string-name>
          , Threaded code,
          <source>Communications of the ACM</source>
          <volume>16</volume>
          (
          <year>1973</year>
          )
          <fpage>370</fpage>
          -
          <lpage>372</lpage>
          .
        </mixed-citation>
      </ref>
      <ref id="ref10">
        <mixed-citation>
          [10]
          <string-name>
            <given-names>C. H.</given-names>
            <surname>Moore</surname>
          </string-name>
          ,
          <article-title>Forth: a new way to program a mini computer</article-title>
          ,
          <source>Astronomy and Astrophysics Supplement</source>
          <volume>15</volume>
          (
          <year>1974</year>
          )
          <fpage>497</fpage>
          -
          <lpage>511</lpage>
          .
        </mixed-citation>
      </ref>
      <ref id="ref11">
        <mixed-citation>
          [11]
          <string-name>
            <given-names>E. D.</given-names>
            <surname>Rather</surname>
          </string-name>
          ,
          <string-name>
            <given-names>D. R.</given-names>
            <surname>Colburn</surname>
          </string-name>
          ,
          <string-name>
            <given-names>C. H.</given-names>
            <surname>Moore</surname>
          </string-name>
          ,
          <article-title>The evolution of forth, in: History of Programming Languages II,</article-title>
          <year>1993</year>
          , pp.
          <fpage>177</fpage>
          -
          <lpage>199</lpage>
          .
        </mixed-citation>
      </ref>
      <ref id="ref12">
        <mixed-citation>
          [12]
          <string-name>
            <given-names>P.</given-names>
            <surname>Klint</surname>
          </string-name>
          , Interpretation techniques,
          <source>Software: Practice and Experience</source>
          <volume>11</volume>
          (
          <year>1981</year>
          ). URL: https://doi.org/10.1002/spe.4380110908. doi:
          <volume>10</volume>
          .1002/spe.4380110908.
        </mixed-citation>
      </ref>
      <ref id="ref13">
        <mixed-citation>
          [13]
          <string-name>
            <given-names>E.</given-names>
            <surname>Miranda</surname>
          </string-name>
          ,
          <article-title>Brouhaha- a portable smalltalk interpreter</article-title>
          ,
          <source>SIGPLAN Not</source>
          .
          <volume>22</volume>
          (
          <year>1987</year>
          )
          <fpage>354</fpage>
          -
          <lpage>365</lpage>
          . URL: https://doi.org/10.1145/38807.38839. doi:
          <volume>10</volume>
          .1145/38807.38839.
        </mixed-citation>
      </ref>
      <ref id="ref14">
        <mixed-citation>
          [14]
          <string-name>
            <given-names>E.</given-names>
            <surname>Miranda</surname>
          </string-name>
          , Portable fast direct threaded code,
          <year>1991</year>
          . URL: https://compilers.iecc.com/ comparch/article/91-03-121.
        </mixed-citation>
      </ref>
      <ref id="ref15">
        <mixed-citation>
          [15]
          <string-name>
            <given-names>E. M.</given-names>
            <surname>Gagnon</surname>
          </string-name>
          ,
          <string-name>
            <given-names>L. J.</given-names>
            <surname>Hendren</surname>
          </string-name>
          ,
          <article-title>SableVM: A research framework for the eficient execution of java bytecode</article-title>
          ,
          <source>in: Java (TM) Virtual Machine Research and Technology Symposium (JVM 01)</source>
          , USENIX Association, Monterey, CA,
          <year>2001</year>
          . URL: https://www.usenix.org/conference/ jvm-01/sablevm-research
          <article-title>-framework-eficient-execution-java-bytecode.</article-title>
        </mixed-citation>
      </ref>
      <ref id="ref16">
        <mixed-citation>
          [16]
          <string-name>
            <given-names>I.</given-names>
            <surname>Piumarta</surname>
          </string-name>
          ,
          <string-name>
            <given-names>F.</given-names>
            <surname>Riccardi</surname>
          </string-name>
          ,
          <article-title>Optimizing direct threaded code by selective inlining</article-title>
          ,
          <source>SIGPLAN Not</source>
          .
          <volume>33</volume>
          (
          <year>1998</year>
          )
          <fpage>291</fpage>
          -
          <lpage>300</lpage>
          . URL: https://doi.org/10.1145/277652.277743. doi:
          <volume>10</volume>
          .1145/277652. 277743.
        </mixed-citation>
      </ref>
      <ref id="ref17">
        <mixed-citation>
          [17]
          <string-name>
            <given-names>R.</given-names>
            <surname>Dewar</surname>
          </string-name>
          , Indirect threaded code,
          <source>Communications of the ACM</source>
          <volume>18</volume>
          (
          <year>1975</year>
          )
          <fpage>330</fpage>
          -
          <lpage>331</lpage>
          .
        </mixed-citation>
      </ref>
      <ref id="ref18">
        <mixed-citation>
          [18]
          <string-name>
            <given-names>L. P.</given-names>
            <surname>Deutsch</surname>
          </string-name>
          ,
          <string-name>
            <given-names>A. M.</given-names>
            <surname>Schifman</surname>
          </string-name>
          ,
          <article-title>Eficient implementation of the smalltalk-80 system</article-title>
          , in: [22],
          <year>1984</year>
          , p.
          <fpage>297</fpage>
          -
          <lpage>302</lpage>
          . URL: https://doi.org/10.1145/800017.800542. doi:
          <volume>10</volume>
          .1145/ 800017.800542.
        </mixed-citation>
      </ref>
      <ref id="ref19">
        <mixed-citation>
          [19]
          <string-name>
            <given-names>E.</given-names>
            <surname>Miranda</surname>
          </string-name>
          ,
          <article-title>Under cover contexts and the big frame-up</article-title>
          ,
          <year>2009</year>
          . URL: http://www. mirandabanda.org/cogblog/2009/01/14/under-cover
          <article-title>-contexts-and-the-big-frame-up/.</article-title>
        </mixed-citation>
      </ref>
      <ref id="ref20">
        <mixed-citation>
          [20]
          <string-name>
            <given-names>N.</given-names>
            <surname>Suzuki</surname>
          </string-name>
          ,
          <string-name>
            <given-names>M.</given-names>
            <surname>Terada</surname>
          </string-name>
          ,
          <article-title>Creating eficient systems for object-oriented languages</article-title>
          , in: [ 22],
        </mixed-citation>
      </ref>
    </ref-list>
  </back>
</article>