Performance Features

One of A4's built-in libraries is called MP, which stands for Maximum Performance.

Some of the features of this library are still under development, but the more basic features can already be used to optimise applications.

Introduction

A4 supports a number of approaches to multithreading.

Lightweight Threads

Firstly, there are lightweight threads (also known as Smalltalk processes, coroutines, etc.). These exist within a single "cell" (virtual machine or database file), and run within the same operating system thread. The cell's built-in process scheduler simply switches between them whenever the current process invokes scheduler yield.

Lightweight threads have performance advantages in many applications - in particular, multimedia and server apps - anything with a lot of small pieces of sequential state (animations, client connections) might be a good target for them. For example, to add some dots to an already-visible GUILabel for a simple "loading" animation, you could use something like this:

[ 100 timesRepeat: [ myLabel text: myLabel text , '.'. gui sleep: 200 ] ] fork

Lightweight threads work the same way they do in other Smalltalk virtual machines, and (at least here) they don't help your program make use of multiple CPU cores. For these reasons, they're part of the base library instead of the Maximum Performance library, and we'll mostly focus on the other approaches here.

Cells

A cell is an execution unit somewhat like an "actor" (or Erlang process), but with some additional features. Each cell has it's own private memory, can be saved to it's own file, and will also be executed in it's own "heavyweight" (or "native") thread. This allows Smalltalk applications to take advantage of multicore processors, and also to divide program/database state into more manageable/maintainable chunks.

Normally, cells operate in a simple load-execute-save pattern: First you load a cell from disk (or create a new cell), then you execute a command within the cell, and then (optionally) it's saved back to disk - either to the same file, or a new one:

<A4> c <- MPCell loadFile: 'App.objects' saveFile: 'App.objects.2' command: 'x <- 1000'.

This will load App.objects, execute the command 'x <- 1000', and then save the cell to App.objects.2.

You can wait for the cell to finish like so (NOTE: The name of the method is unintuitive and will probably change):

<A4> c stop

And you can access the result of the command like this:

<A4> c result
  -> 1000

(Note, the result is always converted to a String.)

You can also run a4 resume App.objects.2 from your system command line to check that the value x has been saved in the file.

There are some other methods to check if the command completed, to check if the file was saved, and other things - but at the moment you'll have to use the help system for more details on that (try help MPCell).

Shared Memory

The load-execute-save pattern is convenient for implementing simple database-like functionality, or for setting up a new cell to do some work, but it isn't very efficient if the cells have a lot of work to do or if they need to run interactively.

That's where memory resources come into play: One cell can create a memory resource, read and write data to it, and then send the address to other cells. These resources can then be used as an underlying buffer for implementing message queues or sending large payloads between threads.

Setting up shared memory without assistance can be a little more difficult, because there are a lot of different ways it can be used. For example, you can make the memory area read-only and executable, or you can set up locks to synchronise access to the memory area.

To allocate a new block of memory with standard (read/write) permissions and without locking, you just have to set a size, and then set allocated to true:

<A4> m <- MPMemory new.
  -> MPMemory
<A4> m size: 100
  -> nil
<A4> m allocated: true
  -> nil

Then it can be used a bit like a ByteArray object:

<A4> m size
  -> 100
<A4> m at: 10
  -> 0
<A4> m at: 10 put: 100
  -> nil
<A4> m at: 10
  -> 100

To access the same piece of memory from somewhere else you need to create another MPMemory object with the same memory pointer (and usually the same size). You can use either an integer or hex-string representation to transfer the pointers (only hex-strings are fully supported right now):

<A4> m2 <- MPMemory new
  -> MPMemory
<A4> m2 size: 100
  -> nil
<A4> m2 pointerAsHex: m pointerAsHex
  -> nil
<A4> m2 at: 10
  -> 100

(To access it from another cell, you just need to transfer the pointerAsHex value somewhere in the command to the other cell and create the other MPMemory object there. Note that the MPMemory object will always be closed when the cell finishes, so they won't persist after the cell is saved and resumed.)

Note

Since MPCell and MPMemory objects are "resources", in a real application they should be closed after use (with the close method).

For MPCell objects, this will wait until the cell finishes before deallocating it's resource.

For MPMemory objects, this will free the memory only if it was allocated for that object.

(There is some support for transferring the [de]allocation responsibility between MPMemory objects, but this shouldn't be considered stable yet.)

Advanced Feature - Executable Memory

Without going into too many details, MPMemory objects allow the application programmer to set and change memory permissions, and also to make low-level function calls into the memory area.

In other words, if you're really crafty, you can use MPMemory objects to implement domain-specific just-in-time compilers within your application.

The basic functionality for this is complete, but hasn't been properly tested or documented yet.

More on this soon...