Blueprint Help Send comments on this topic.
State and Workspace Objects

Glossary Item Box

Description

CDL provides a number of specific mechanisms for handling data in distributed parallel environments.  Active objects (threads, call-backs, methods and circuits) can all own 'workspace' data objects which are also persistent, and in many ways provide an equivalent of thread specific data.  Methods can also own 'state' objects, which are similar to workspace objects, but in the specific case of 'slave-able' methods, which can be scheduled to execute on any CPU, the state data object is automatically moved (and cached) to whichever machine executes the method instance.  Because state data needs to be moved each time the target processor changes, it should only be used when necessary.  Constants and tables that are calculated once and do not change (e.g. FFT twiddle coefficients) should always be stored in workspace.

In all cases, data objects have two components.  The first of these is the 'data' component itself (see Data Components) which is referenced and managed by the second 'reference' component.  The data component can contain native pointers and references but these will not be valid when data objects are moved between machines (only state data is moved), and are unlikely to be valid in asymmetric memory environments.  They can be used however to reference objects within the data component but will need to be 're-linked' each time their owning store is opened for read.

State and Workspace Objects

The translator will create a skeletal class definition for each state and workspace data type, but will assume a fixed size data component (see Data Component section above).

State and Workspace reference object type names are derived from their data component type with "Obj" appended.  In the case of state and workspace objects the reference component's type is automatically generated using the following schemes;

Method Workspace

MthdType +"MthdWorkspaceObj".  If a method is explicitly defined then MthdType is specified by the definition.  If the method is defined 'in-line' then it's type is constructed from parent circuit type, followed by "_" followed by its instance name.  So if a method named "M" is defined in a circuit of type "MyCct" then its workspace data component's type would be 'MyCct_MMthdWorkspaceObj'.

Call-Back Workspace

CbfType +"CbfWorkspaceObj".  If a call-back is explicitly defined then CbfType is specified by the definition.  If the call-back is defined 'in-line' then it's type is constructed from parent circuit type, followed by "_" followed by its instance name.  So if a call-back named "C" is defined in a circuit of type "MyCct" then its workspace data component's type would be 'MyCct_CCbfWorkspaceObj'.

Circuit Workspace

CctType +"CctWorkspaceObj".  So if a circuit has type "MyCct" then its workspace data component's type would be 'MyCct_WorkspaceObj'.

Method State

MthdType +"MthdStateObj".  If a method is explicitly defined then MthdType is specified by the definition.  If the method is defined 'in-line' then it's type is constructed from parent circuit type, followed by "_" followed by its instance name.  So if a method named "M" is defined in a circuit of type "MyCct" then its state data component's type would be 'MyCct_MMthdStateObj'.

The translator will produce the following workspace definition for a method of type 'Mtype';

   class MtypeMthdWorkspaceObj : public ClpMthdWorkspaceObject<MtypeMthdWorkspace>
   {
   public:
    /// <summary>
    /// Workspace data class access method
    /// </summary>
    /// <returns>A reference to the workspace data class for this workspace object</returns>
      MtypeMthdWorkspace& Data()
      {
         return Ref();
      }
    /// <summary>
    /// Determines workspace data class actual size
    /// </summary>
    /// <returns>The actual size of the workspace data class for this workspace object</returns>
      Uns Size()
      {
        if ( sizeof( MtypeMthdWorkspace ) == 1 )
           return 0;
        else
           return sizeof( MtypeMthdWorkspace );
      }
    /// <summary>
    /// Initializes the workspace data class
    /// </summary>
    /// <returns>TRUE if successful, FALSE if failed</returns>
      Uns Initialise()
      {
         return Data().Initialise();
      }
   };
 

By default the translator will also produce the following initialization code for methods which will be called once during initialization.  The other active objects will have similar code generated for them, but only methods support state.

The 'Definitive()' function will only return TRUE if the executing process owns the single definitive copy of the method's state.  Unless the method is explicitly 'pinned' to execute in a particular slave process this will be the master.  The 'Executable()' function will only return TRUE if the executing process is able to execute the method.  If it is not slaved this will be the master, if it is pinned it will be a particular slave, otherwise it will return TRUE for all slaves.  It should be noted therefore that when required, methods must be 'pinned' before state and workspace objects are constructed.

   Uns MtypeMthdBaseElem::Initialise()
   {
      Uns failed = FALSE;
      
      if ( Definitive() )
      {
         // Only methods can own state
         failed |= ! mState.Construct();
         failed |= ! mState.Initialise();
      }
      
      if ( Executable() )
      {
         failed |= ! mWorkspace.Construct();
         failed |= ! mWorkspace.Initialise();
      }

      return ! failed;
   }

State and workspace data can then be accessed from user code using mState.Data() and mWorkspace.Data() respectively.

Variable Sized Data

As with records, if the data is fixed sized then the generated code above will suffice, but variable sized data components will require additional user code.  As with records, the user needs to provide a 'SetDims', a 'Size', and a 'Construct'; all of which require data component sizes as parameters.

The examples that follow assume that the data object has the following form, where N and M are assumed to be dynamic (unspecified at compile time).  See Data Components example.

class Mtype
{
   Float f;
   Uns   N;
   Uns   M;
   Int   X[N];
   Int   Y[N][M];
};

If we assume that the data component follows the same convention as that outlined in the data components topic, then the method's workspace data component will be the following;

   class MtypeMthdWorkspace : public ClpNewObject
   {
   public:
   
      Uns SetDims( Uns n, Uns m )
      {
         // Store current dimensions
         N = n;
         M = m;
         return TRUE;
      }

      // Calculate object size for n,m
      Uns Size( Uns n, Uns m )
      {
         return sizeof( MtypeMthdWorkspace ) + (m+1)*n*sizeof( Int );
      }

      // Calculate current object size
      Uns Size()
      {
         return Size( N, M );
      }

      // Initialize object
      Uns Initialise()
      {
         // Perform object specific initialization here
         return TRUE;
      }

      // Return current value of 'N'
      Uns N_val()
      {
         return N;
      }

      // Return current value of 'M'
      Uns M_val()
      {
         return M;
      }

      // Set current value of 'N'
      void N_set( Uns n )
      {
         N = n;
      }

      // Set current value of 'M'
      void M_set( Uns m )
      {
         M = m;
      }

      // Set X array element
      void X_set( Uns idx, Int x )
      {
         // Assume 'X' follows the 'MtypeMthdWorkspace' header contiguously
         ((Int*)(this+1))[ idx ] = x;
      }

      // Set Y array element
      void Y_val( Uns idx1, Uns idx2, Int y )
      {
         // Assume Y follows X contiguously
         Int *Y_start = ((Int*)(this+1))+N;
         Y_start[ idx1*M+idx2 ] = y;
      }

      // Return X array element
      Int X_val( Uns idx )
      {
         // Assume it follows the 'MtypeMthdWorkspace' header contiguously
         return ((Int*)(this+1))[ idx ];
      }

      // Return Y array element
      Int Y_val( Uns idx1, Uns idx2 )
      {
         // Assume Y follows X contiguously
         Int *Y_start = ((Int*)(this+1))+N;
         return Y_start[ idx1*M+idx2 ];
      }

   private:
      // Fixed size components
      Float f;
      Uns N;
      Uns M;
   };

The generated reference object would then need to be updated as follows.  The additional user code is highlighted in red.

   class MtypeMthdWorkspaceObj : public ClpMthdWorkspaceObject<MtypeMthdWorkspace>
   {
   public:
    /// <summary>
    /// Workspace data class access method
    /// </summary>
    /// <returns>A reference to the workspace data class for this workspace object</returns>
      MtypeMthdWorkspace& Data()
      {
         return Ref();
      }
    /// <summary>
    /// Determines workspace data class actual size
    /// </summary>
    /// <returns>The actual size of the workspace data class for this workspace object</returns>
      Uns Size()
      {
         return Data().Size();
      }

    /// <summary>
    /// Initializes the workspace data class
    /// </summary>
    /// <returns>TRUE if successful, FALSE if failed</returns>
      Uns Initialise()
      {
         return Data().Initialise();
      }

      // Two parameter SetDims function (generic);
      Uns SetDims( Uns n, Uns m )
      {
         return Data().SetDims( n, m );
      }

      // Two parameter Size function (generic);
      static Uns Size( Uns n, Uns m )
      {
         return MtypeMthdWorkspace::Size( n, m );
      }

      // Two parameter Construct function (generic);
      Uns Construct( Uns n, Uns m )
      {
         Uns totalSize = Size( n, m );
         SetTsSize( totalSize );
         if ( Construct() )
         {
            return Data().SetDims( n, m );
         }
         return FALSE;
      }

   };

All three user modified functions above are generic and can therefore be used with any data component that has 2 size parameters and adopts the required convention.  In fact the 3 or more size parameter cases are all identical but with the appropriate number of sizing parameters.

Now finally the method's initialise call needs to be re-coded so that it calls the user defined construct and initialise;

The generated harness's initialise will call the base initialise which is no longer required and should be replaced by the following generic code;

Uns MtypeMthdElem::Initialise()
{
   Uns failed = FALSE;

   // Retrieve dynamic sizes from file, command line, or similar;
   Uns N_size = GetN_size();
   Uns M_size = GetM_size();

   if ( Definitive() )
   {
      failed |= ! mState.Construct( N_size, M_size );
      failed |= ! mState.Initialise();
   }
   if ( Executable() )
   {
      failed |= ! mWorkspace.Construct( N_size, M_size );
      failed |= ! mWorkspace.Initialise();
   }
  
   return ! failed;
}

The generic code above will also generalize to multidimensional dynamic data with 3 or more size parameters.