VSs structures, types, and constructs

Types and structs

VSsTaskFnPtr is the type for variables that hold a pointer to a function that encodes the behavior of a task. Such functions have a fixed signature. PtrToAtomicFn is the type for functions that are executed via the VSs__animate_short_fn_in_isolation construct (below)

IN, OUT, and INOUT are used to specify the way a task uses each of its shared variables

VSsTaskType is the structure that holds information about a VSs task. One of these must be created and filled in for each kind of task created within a VSs (StarSs) program. It says how many arguments the task needs passed to it, how many of those are shared variables involved in dependencies with other tasks, the type of each, the size of each, and the size of the structure passed to the VSs__submit_task. Here is the full structure, with detailed comments on each entry:

 typedef struct
  {
    VSsTaskFnPtr fn;
    int32  numTotalArgs;//the number of inputs to function
    int32  numCtldArgs;//how many of those inputs have dependencies
    int32 *argTypes;   //says IN, OUT, INOUT, or not involved in dependencies, for each argument (is an array)
    int32 *argSizes;   //size of the data structure of each argument.. used for detecting overlap  (is an array)
    int32  sizeOfArgs; //used when copying the array of arguments, says the size, in bytes, of the structure passed to VSs__submit_task() (below)
  }
 VSsTaskType;

VSs constructs

This is used inside the "border crossing" function, when transition from normal C into VSs. It creates the first virtual processor that exists within the VSs program. This seed VP then creates task-stubs and possibly other VPs, by using the creation constructs (below).

 void
 VSs__create_seed_slave_and_do_work( TopLevelFnPtr fn, void *initData );

Creation constructs

This creates a new slave virtual processor, and gives the code to execute. Which core the VP is animated by is indeterminate and may change. The TopLevelFnPtr specifies that the top level function receive a SlaveVP pointer as one of its arguments. This pointer is generated by the VSs runtime and passed in to the function. It points to the actual virtual processor that is animating that function call. This pointer is required in all other VSs calls, so it must be passed around.

 SlaveVP *
 VSs__create_slave( TopLevelFnPtr fnPtr, void *initData,
                    SlaveVP *creatingSlv );

This also creates a slave VP, but pins it to a specific core.

 SlaveVP *
 VSs__create_slave_with_affinity( TopLevelFnPtr fnPtr,    void *initData,
                                  SlaveVP *creatingSlv, int32 coreToAssignOnto);

This is a required call that must be made from within the code given during creation of a slave VP. It can be called from anyplace, but will immediately stop execution of the VP's code and make the VP disappear. The slaveToDissipate must be the slave that is animating the code that makes this call (IE, the slave that was passed in to the top level function).

 void
 VSs__dissipate_slave( SlaveVP *slaveToDissipate );

This creates a task, rather than a virtual processor. This difference is one of context and expected longevity. A task has no pre-existing stack context, and normally does not suspend. Rather, a task is seen as a self-contained unit of work that runs to completion. However, in order to increase the flexibility of VSs, to capture a wider variety of applications, VSs tasks are able to execute sends and receives, as well as a variety of other synchronization constructs seen below. The function executed as the behavior of a task must have the same signature as a VP top level function (and is passed into this task creator via the task type structure). The arguments to the task are passed inside a structure, that the task must cast to an application-defined structure. The arguments are copied inside the runtime, so the arguments structure can be reused to create many tasks, assigning new values for each one.

 int32
 VSs__submit_task( VSsTaskType *taskType, void *args, SlaveVP *animSlv);

This is mandatory, and must be executed at some point within the task code. Every path that ends task code must have this as the last statement. IE, "return" or simply encountering the last } must never be reached without executing this first.

 void
 VSs__end_task( SlaveVP *animSlv );

Malloc and Free

Due to a "feature" of Linux's threads, VMS, and thus VSs is forced to implement its own malloc and free system. Direct calls to malloc and free must be replaced with VSs__malloc and VSs__free in order to prevent segfaults inside Linux's implementation.

 void *
 VSs__malloc( int32 numBytes, SlaveVP *callingSlave )

 VSs__free( void *ptrToFree, SlaveVP *callingSlave )

Concurrency Constructs

These affect the virtual processor that is currently animating a task. They are all used to synchronize the use of shared variables that is not controlled by the IN, OUT, and INOUT method. These allow more flexibility in the pattern of communication among tasks and virtual processors that are not animating tasks.

VSs__start_fn_singleton and VSs__end_fn_singleton are used when many tasks or virtual processors need a section of code to be executed by the time they start, but it must only be executed by one of them. This construct allows the first to get that point in the code to be the one to execute it, and all the others just skip that section of code. Without this singleton construct, a barrier would have to be used to be sure all the tasks and/or virtual processors had reached this point, or else some custom use of locks. However, in practice, the coder often wishes to group executions by some code-specific thing, and creates an ID for each group:

 void
 VSs__start_fn_singleton( int32 singletonID, SlaveVP *animSlv );

 void
 VSs__end_fn_singleton( int32 singletonID, SlaveVP *animSlv );

VSs__start_data_singleton and VSs__end_data_singleton are also placed in-line in the code, except they protect a particular data structure rather than a section of code. The address of the data should only have the protected code executed on it once. In use, the address of a target data structure is given to the singleton construct, and then the protected code is only executed on that address one time:

 void
 VSs__start_data_singleton( VSsSingleton **singeltonAddr, SlaveVP *animSlv );

 void
 VSs__end_data_singleton( VSsSingleton **singletonAddr, SlaveVP *animSlv );

This executes a function atomically (PtrToAtomicFn was mentioned above):

 void
 VSs__animate_short_fn_in_isolation( PtrToAtomicFn ptrToFnToExecInMaster,
                                    void *data, SlaveVP *animSlv );

VSs__start_transaction and VSs__end_transaction are classic nested transactions. However, the implementation is not speculative, so it doesn't have all the performance advantages of a transactional memory implementation. Rather, it is provided in order to express dependencies among sections of code that access shared variables. They increase the variety of dependencies that can be expressed:

 void
 VSs__start_transaction( int32 transactionID, SlaveVP *animSlv );

 void
 VSs__end_transaction( int32 transactionID, SlaveVP *animSlv );

Send-receive constructs

inline int32 * VSs__create_taskID_of_size( int32 numInts, SlaveVP *animSlv );

void VSs__submit_task_with_ID( VSsTaskType *taskType, void *args, int32 *taskID,

                          SlaveVP     *animSlv);

inline int32 * VSs__give_self_taskID( SlaveVP *animSlv );

void VSs__send_of_type_to( void *msg, const int32 type, int32 *receiverID,

                      SlaveVP *senderSlv );

void VSs__send_from_to( void *msg, int32 *senderID, int32 *receiverID, SlaveVP *senderSlv );

void * VSs__receive_type_to( const int32 type, int32* receiverID, SlaveVP *receiverSlv );

void * VSs__receive_from_to( int32 *senderID, int32 *receiverID, SlaveVP *receiverSlv );