Difference between revisions of "SLICC"

From gem5
Jump to: navigation, search
(SLICC Internals)
(SLICC Internals)
Line 188: Line 188:
'''Variable Assignments'''
'''Variable Assignments'''
* Use the := operator to assign members in class:
* Use the := operator to assign members in class (e.g. a member defined in RubySlicc_Types.sm):
** When using lowercase, an automatic "m_" is added to the name.
** an automatic "m_" is added to the name mentioned in the SLICC file.
** When using uppercase, the variable name is as is.

Revision as of 22:05, 25 May 2011

SLICC is a domain specific language for specifying cache coherence protocols. The SLICC compiler generates C++ code for different controllers, which can work in tandem with other parts of Ruby. The compiler also generates an HTML specification of the protocol.

Input To the Compiler

The SLICC compiler takes as input files that specify the controllers involved in the protocol. The .slicc file specifies the different files used by the particular protocol under consideration. For example, if trying to specify the MI protocol using SLICC, then we may ues MI.slicc as the file that specifies all the files necessary for the protocol. The files necessary for specifying a protocol include the definitions of the state machines for different controllers, and of the network messages that are passed on between these controllers.

The files have a syntax similar to that of C++. The compiler, written using PLY (Python Lex-Yacc), parses these files to create an Abstract Syntax Tree (AST). The AST is then traversed to build some of the internal data structures. Finally the compiler outputs the C++ code by traversing the tree again. The AST represents the hierarchy of different structures present with in a state machine. We describe these structures next.

Protocol State Machines

In this section we take a closer look at what goes in to a file containing specification of a state machine.

Specifying Data Members

Each state machine is described using SLICC's machine datatype. Each machine has several different types of members. Machines for cache and directory controllers include cache memory and directory memory data members respectively. We will use the MI protocol available in src/mem/protocol as our running example. So here is how you might want to start writing a state machine

 machine(L1Cache, "MI Example L1 Cache")
  : Sequencer * sequencer,
    CacheMemory * cacheMemory,
    int cache_response_latency = 12,
    int issue_latency = 2 {
      // Add rest of the stuff

  • In order to let the controller receive messages from different entities in the system, the machine has a number of Message Buffers. These act as input and output ports for the machine. Here is an example specifying the output ports.
 MessageBuffer requestFromCache, network="To", virtual_network="2", ordered="true";
 MessageBuffer responseFromCache, network="To", virtual_network="4", ordered="true";

Note that Message Buffers have some attributes that need to be specified correctly. Another example, this time for specifying the input ports.

 MessageBuffer forwardToCache, network="From", virtual_network="3", ordered="true";
 MessageBuffer responseToCache, network="From", virtual_network="4", ordered="true";

  • Next the machine includes a declaration of the states that machine can possibly reach. In cache coherence protocol, states can be of two types -- stable and transient. A cache block is said to be in a stable state if in the absence of any activity (in coming request for the block from another controller, for example), the cache block would remain in that state for ever. Transient states are required for transitioning between stable states. They are needed when ever the transition between two stable states can not be done in an atomic fashion. Next is an example that shows how states are declared. SLICC has a keyword state_declaration that has to be used for declaring states.
 state_declaration(State, desc="Cache states") {
   I, AccessPermission:Invalid, desc="Not Present/Invalid";
   II, AccessPermission:Busy, desc="Not Present/Invalid, issued PUT";
   M, AccessPermission:Read_Write, desc="Modified";
   MI, AccessPermission:Busy, desc="Modified, issued PUT";
   MII, AccessPermission:Busy, desc="Modified, issued PUTX, received nack";
   IS, AccessPermission:Busy, desc="Issued request for LOAD/IFETCH";
   IM, AccessPermission:Busy, desc="Issued request for STORE/ATOMIC";

The states I and M are the only stable states in this example. Again note that certain attributes have to be specified with the states.

  • The state machine needs to specify the events it can handle and thus transition from one state to another. SLICC provides the keyword enumeration which can be used for specifying the set of possible events. An example to shed more light on this -
 enumeration(Event, desc="Cache events") {
   // From processor
   Load,       desc="Load request from processor";
   Ifetch,     desc="Ifetch request from processor";
   Store,      desc="Store request from processor";
   Data,       desc="Data from network";
   Fwd_GETX,        desc="Forward from network";
   Inv,        desc="Invalidate request from dir";
   Replacement,  desc="Replace a block";
   Writeback_Ack,   desc="Ack from the directory for a writeback";
   Writeback_Nack,   desc="Nack from the directory for a writeback";

  • While developing a protocol machine, we may need to define structures that represent different entities in a memory system. SLICC provides the keyword structure for this purpose. An example follows
 structure(Entry, desc="...", interface="AbstractCacheEntry") {
   State CacheState,        desc="cache state";
   bool Dirty,              desc="Is the data dirty (different than memory)?";
   DataBlock DataBlk,       desc="Data in the block";

The cool thing about using SLICC's structure is that it automatically generates for you the get and set functions on different fields. It also writes a nice print function and overloads the << operator. But in case you would prefer do everything on your own, you can make use of the keyword external in the declaration of the structure. This would prevent SLICC from generating C++ code for this structure.

 structure(TBETable, external="yes") {
   TBE lookup(Address);
   void allocate(Address);
   void deallocate(Address);
   bool isPresent(Address);

In fact many predefined types exist in src/mem/protocol/RubySlicc_*.sm files. You can make use of them, or if you need new types, you can define new ones as well. You can also use the keyword interface to make use of inheritance features available in C++. Note that currently SLICC supports public inheritance only.

  • We can also declare and define functions as we do in C++. There are certain functions that the compiler expects would always be defined by the controller. These include
    • getState()
    • setState()

Input for the Machine

Since protocol is state machine, we need to specify how to machine transitions from one state to another on receiving inputs. As mentioned before, each machine has several input and output ports. For each input port, the in_port keyword is used for specifying the behavior of the machine, when a message is received on that input port. An example follows that shows the syntax for declaring an input port.

 in_port(mandatoryQueue_in, RubyRequest, mandatoryQueue, desc="...") {
   if (mandatoryQueue_in.isReady()) {
     peek(mandatoryQueue_in, RubyRequest, block_on="LineAddress") {
       Entry cache_entry := getCacheEntry(in_msg.LineAddress);
       if (is_invalid(cache_entry) &&
           cacheMemory.cacheAvail(in_msg.LineAddress) == false ) {
         // make room for the block
         trigger(Event:Replacement, cacheMemory.cacheProbe(in_msg.LineAddress),
       else {
         trigger(mandatory_request_type_to_event(in_msg.Type), in_msg.LineAddress,
                 cache_entry, TBEs[in_msg.LineAddress]);

  • As you can see, in_port takes in multiple arguments. The first argument, mandatoryQueue_in, is the identifier for the in_port that is used in the file. The next argument, RubyRequest, is the type of the messages that this input port receives. Each input port uses a queue to store the messages, the name of the queue is the third argument.

  • The keyword peek is used to extract messages from the queue of the input port. The use of this keyword implicitly declares a variable in_msg which is of the same type as specified in the input port's declaration. This variable points to the message at the head of the queue. It can be used for accessing the fields of the message as shown in the code above.

  • Once the incoming message has been analyzed, it is time for using this message for taking some appropriate action and changing the state of the machine. This done using the keyword trigger. The trigger function is actually used only in SLICC code and is not present in the generated code. Instead this call is converted in to a call to the doTransition() function which appears in the generated code. The doTransition() function is automatically generated by SLICC for each of the state machines. The number of arguments to trigger depend on the machine itself. In general, the input arguments for trigger are the type of the message that needs to processed, the address for which this message is meant for, the cache and the transaction buffer entries for that address.

  • trigger also increments a counter that is checked before a transition is made. In one ruby cycle, there is a limit on the number of transitions that can be carried out. This is done to resemble more closely to a hardware based state machine. @TODO: What happens if there are no more transitions left? Does the wakeup abort?


In this section we will go over how the actions that a state machine can carry out are defined. These actions will be called in to action when the state machine receives some input message which is then used to make a transition. Let's go over an example on how the key word action can be made use of.

 action(a_issueRequest, "a", desc="Issue a request") {
   enqueue(requestNetwork_out, RequestMsg, latency=issue_latency) {
   out_msg.Address := address;
     out_msg.Type := CoherenceRequestType:GETX;
     out_msg.Requestor := machineID;
     out_msg.MessageSize := MessageSizeType:Control;

  • The first input argument is the name of the action, the next argument is the abbreviation used for generating the documentation and last one is the description of the action which used in the HTML documentation and as a comment in the C++ code.
  • Each action is converted in to a C++ function of that name. The generated C++ code implicitly includes up to three input parameters in the function header, again depending on the machine. These arguments are the memory address on which the action is being taken, the cache and transaction buffer entries pertaining to this address.
  • Next useful thing to look at is the enqueue keyword. This keyword is used for queuing a message, generated as a result of the action, to an output port. The keyword takes three input arguments, namely, the name of the output port, the type of the message to be queued and the latency after which this message can be dequeued. Note that in case randomization is enabled, the specified latency is ignored. The use of the keyword implicitly declares a variable out_msg which is populated by the follow on statements.


A transition function is a mapping from the cross product of set of states and set of events to the set of states. SLICC provides the keyword transition for specifying the transition function for state machines. An example follows --

 transition(IM, Data, M) {

In this example, the initial state is IM. If an event of type Data occurs in that state, then final state would be M. Before making the transition, the state machine can perform certain actions on the structures that it maintains. In the given example, u_writeDataToCache is an action. All these operations are performed in an atomic fashion, i.e. no other event can occur before the set of actions specified with the transition has been completed.

  • For ease of use, sets of events and states can be provided as input to transition. The cross product of these sets will map to the same final state. Note that the final state cannot be a set. If for a particular event, the final state is same as the initial state, then the final state can be omitted.
 transition({IS, IM, MI, II}, {Load, Ifetch, Store, Replacement}) {

Other Compiler Features

  • SLICC supports conditional statements in form of if and else. Note that SLICC does not support else if.
  • Each function has return type which can be void as well. Returned values cannot be ignored.
  • SLICC has limited support for pointer variables. is_valid() and is_invalid() operations are supported for testing whether a given pointer 'is not NULL' and 'is NULL' respectively. The keyword OOD, which stands for Out of Domain, plays the role of keyword NULL used in C++.
  • Static type casting is supported in SLICC. The keyword static_cast has been provided for this purpose. For example, in the following piece of code, a variable of type AbstractCacheEntry is being casted in to a variable of type Entry.
   Entry L1Dcache_entry := static_cast(Entry, "pointer", L1DcacheMemory[addr]);

SLICC Internals

C++ to Slicc Interface - @note: What do each of these files do/define???

  • src/mem/protocol/RubySlicc_interaces.sm
    • RubySlicc_Exports.sm
    • RubySlicc_Defines.sm
    • RubySlicc_Profiler.sm
    • RubySlicc_Types.sm
    • RubySlicc_MemControl.sm
    • RubySlicc_ComponentMapping.sm

Variable Assignments

  • Use the := operator to assign members in class (e.g. a member defined in RubySlicc_Types.sm):
    • an automatic "m_" is added to the name mentioned in the SLICC file.