Compiling in parallel and deferred threads

There is a lot of parallelism in compilers. I thread tokenizing, parsing and some other stuff. I tried compiling each function in a thread (ie one thread per function) but that overwhelmed my two core system and was slower than a single thread. My next attempt at parallelizing the compile phase was to split the convert-the-parse-tree-to-asm-code and convert-asm-code-to-VM-instructions stages. Functions are the only objects that actually contains runnable code so it was pretty easy to do the split. Another observation is that there isn’t always code that needs assembling so it would be nice to avoid creating a thread in that case (threads aren’t cheap).

Performance: This was good for about a 15% speed up (AMD dual core). The asm thread appears to be easily able to keep up with compiler and probably should be given more work to do.

The first order of business is to create a thread class that will assemble code. And only create that thread if something needs to be assembled. The thread works by sucking in functions (plus some supporting info) to assemble from a inter-thread pipe, calling the assembler and writing the results to another pipe (so they can be consumed by another thread and squished into the object being compiled):

class FcnAsmThread {
   fcn init {     // –> Deferred Thread/Pipe, Pipe
      var [const] in = Thread.Pipe(),
                  out = Thread.Pipe();
        // Only start thread if actually need it
      returnClass(
         T(Deferred(fcn{self.launch(); in}),out));
   }
          // asm a fcn and write it to out in a thread
   fcn liftoff {
      foreach pFcn,vmClass,cblock,defaultArgs,index in
      (in) {
         reg names =
            T(pFcn.name,pFcn.argNames.xplode());
              // .finish() calls Asm.asm()
         func = self.fcn.embryo(names,vmClass,
            cblock.finish(),defaultArgs,
            pFcn.isPrivate);
         // Classes are not thread safe,
         // add fcn in other thread
         out.write(T(vmClass,func,index));
      }
      out.close();
   }
}

Crib sheet:

  • Creating a new instance of this class doesn’t return a reference to itself (self), it returns two pipes. Thus, the new class is “cloaked” and the only way to access to it is via the pipes. returnClass is used to override the normal return mechanism.
  • The input pipe isn’t really a pipe but a Deferred object, which, when first referenced (as in in.write()), causes the contained function to run, which, in this case, launches the thread and returns the pipe, and the original operation (write) is done. Subsequent references will get the pipe.
  • T() creates a read only list.
  • A call to launch() creates the thread and the new thread calls the liftoff function when it starts running.
  • fcn.embryo() creates an “embryonic” function that is ready to be inserted into a Class.
  • A foreach loop on a pipe reads from the pipe until it is closed.
  • Exiting liftoff stops the thread.

________________________________

The thread is readied at the start of a file (or chunk of text) compilation. This is a bit awkward as the compiler is written as reentrant state machine so there needs to be some deduction as to when to fire off the thread. Since a file is encapsulated as a class (a “RootClass”), do it when one (and there can be only one) of those is encountered. If a source code error is encountered, the thread needs to be shut down but care needs to be taken so that, if the thread hasn’t started, attempted to stop it doesn’t start it.

fcn compileClass {
   …
   reg conBlock,in,out;
   if (pClass.isInstanceOf(Parser.RootClass)) {
      // Create a thread to asm the compiled fcns
      // in is a Deferred Thread/Pipe, out is a Pipe
      in,out = FcnAsmThread(); pClass.asmPipe = in;
      onExit(fcn(in,out){
// all done compiling this file
         if (in.BaseClass.toBool()) { // thread was started
                   // all done compiling this file
           
if (vm.xxception) in.clear();
            else { 
// add fcns to their Class
              
in.close(); FcnAsmThread.mashUp(out); }
         }
         // else thread wasn’t started, do nothing
         return(Void);
      },in,out); // onExit
      conBlock = compileBlock(classBlock,vmClass); }
   }
   …
}

Crib sheet:

  • All changes and accesses to the Deferred are made in the compiler thread;  a good thing as Deferred objects are not thread safe.
  • A “block” contains the source for a class; if a class (such as the RootClass/file) contains classes, compileClass is recursive.
  • Asking a Deferred to convert itself to a Bool (eg if (Deferred) … ) is a reference to the underling object (the pipe). We don’t want that. Using .BaseClass.toBool() prevents evaluation and directs the question to the container. The result is True if the Deferred has been evaluated (ie has the thread been created?). I need this check because if I just call in.close(), and the thread hasn’t started, it would start.
  • Closing the input pipe stops the thread (since all it does is read from the pipe).
  • Once the thread has stopped, out contains all the assembled functions.
  • The onExit keyword creates a deferred object (in this case, a function) that is evaluated when the function exits/returns. Using onExit means this code will always be run, I don’t have to worry about exceptions or whatever and puts the thread finish up code close to where the thread is created.
    If an error caused the compilation to stop, clear and close the pipe, which will stop the thread.
    If the thread was started, functions were assembled so close the pipe, stop the thread and put the functions into their class.
    No matter what happens, I know the thread will be stopped.
    Returning Void ensures there in won’t be mistakenly evaluated.

______________________________________

The compileFcn function does a bunch of stuff then sends the function code out to be assembled.

fcn compileFcn(pFcn,lexBlock,vmClass) {
   …
   reg rootClass = lexBlock.findRootClass();
   rootClass.asmPipe.write(  // pass to thread
       T(pFcn,vmClass,cblock,defaultArgs,
         lexBlock.fcnIndex(fcnName)));

   …
}

Crib sheet:

  • The RootClass contains the pipe to the thread. As the compiler is a state machine, the RootClass is located in the passed in context (parse tree).
  • If the thread hasn’t started yet, calling write will cause the Deferred object to evaluate and start the thread and then perform the actual write.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s