A message encapsulates all the parameters sent to an entry method. Since the parameters are already encapsulated, sending messages is often more efficient than parameter marshalling. In addition, messages are easier to queue and store on the receive side.
The largest difference between parameter marshalling and messages is that entry methods keep the messages passed to them. Thus each entry method must be passed a new message. On the receiving side, the entry method must either store the passed message or explicitly delete it, or else the message will never be destroyed, wasting memory.
Several kinds of message are available. Regular CHARM++ messages are objects of fixed size. One can have messages that contain pointers or variable length arrays (arrays with sizes specified at runtime) and still have these pointers to be valid when messages are sent across processors, with some additional coding. Also available is a mechanism for assigning priorities to messages that applies all kinds of messages. A detailed discussion of priorities appears later in this section.
Like all other entities involved in asynchronous method invocation, messages need to be declared in the .ci file. In the .ci file (the interface file), a message is declared as:
A message that contains variable length arrays is declared as:
If the name of the message class is MessageType, the class must inherit publicly from a class whose name is CMessage_MessageType. This class is generated by the charm translator. Then message definition has the form:
Messages are allocated using the C++ new operator:
The optional arguments to the new operator are used when allocating messages with variable length arrays or prioritized messages. sz1, sz2, ... denote the size (in appropriate units) of the memory blocks that need to be allocated and assigned to the pointers that the message contains. The priobits argument denotes the size of a bitfield (number of bits) that will be used to store the message priority.
For example, to allocate a message whose class declaration is:
do the following:
To allocate a message whose class declaration is:
do the following:
This allocates a VarsizeMessage, in which firstArray points to an array of 10 ints and secondArray points to an array of 20 doubles. This is explained in detail in later sections.
To add a priority bitfield to this message,
Note, you must provide number of bits which is used to store the priority as the priobits parameter. The section on prioritized execution describes how this bitfield is used.
In Section 3.4.3 we explain how messages can contain arbitrary pointers, and how the validity of such pointers can be maintained across processors in a distributed memory machine.
When a message is sent to a chare, the programmer relinquishes control of it; the space allocated to the message is freed by the system. When a message is received at an entry point it is not freed by the runtime system. It may be reused or deleted by the programmer. Messages can be deleted using the standard C++ delete operator.
There are no limitations of the methods of message classes except that the message class may not redefine operators new or delete.
An ordinary message in CHARM++ is a fixed size message that is allocated internally with an envelope which encodes the size of the message. Very often, the size of the data contained in a message is not known until runtime. One can use packed messages to alleviate this problem. However, it requires multiple memory allocations (one for the message, and another for the buffer.) This can be avoided by making use of a varsize message. In varsize messages, the space required for these variable length arrays is allocated with the message such that it is contiguous to the message.
Such a message is declared as
in CHARM++ interface file. The class mtype has to inherit from CMessage_mtype. In addition, it has to contain variables of corresponding names pointing to appropriate types. If any of these variables (data members) are private or protected, it should declare class CMessage_mtype to be a ``friend'' class. Thus the mtype class declaration should be similar to:
An Example
Suppose a CHARM++ message contains two variable length arrays of types int and double:
Then in the .ci file, this has to be declared as:
We specify the types and actual names of the fields that contain variable length arrays. The dimensions of these arrays are NOT specified in the interface file, since they will be specified in the constructor of the message when message is created. In the .h or .C file, this class is declared as:
The interface translator generates the CMessage_VarsizeMessage class, which contains code to properly allocate, pack and unpack the VarsizeMessage.
One can allocate messages of the VarsizeMessage class as follows:
Another way of allocating a varsize message is to pass a sizes in an array instead of the parameter list. For example,
No special handling is needed for deleting varsize messages.
The CHARM++ interface translator generates implementation for three static methods for the message class CMessage_mtype. These methods have the prototypes:
One may choose not to use the translator-generated methods and may override these implementations with their own alloc, pack and unpack static methods of the mtype class. The alloc method will be called when the message is allocated using the C++ new operator. The programmer never needs to explicitly call it. Note that all elements of the message are allocated when the message is created with new. There is no need to call new to allocate any of the fields of the message. This differs from a packed message where each field requires individual allocation. The alloc method should actually allocate the message using CkAllocMsg, whose signature is given below:
For varsize messages, these static methods alloc, pack, and unpack are generated by the interface translator. For example, these methods for the VarsizeMessage class above would be similar to:
The pointers in a varsize message can exist in two states. At creation, they are valid C++ pointers to the start of the arrays. After packing, they become offsets from the address of the pointer variable to the start of the pointed-to data. Unpacking restores them to pointers.
In many cases, a message must store a non-linear data structure using pointers. Examples of these are binary trees, hash tables etc. Thus, the message itself contains only a pointer to the actual data. When the message is sent to the same processor, these pointers point to the original locations, which are within the address space of the same processor. However, when such a message is sent to other processors, these pointers will point to invalid locations.
Thus, the programmer needs a way to ``serialize'' these messages only if the message crosses the address-space boundary. CHARM++ provides a way to do this serialization by allowing the developer to override the default serialization methods generated by the CHARM++ interface translator. Note that this low-level serialization has nothing to do with parameter marshalling or the PUP framework described later.
Packed messages are declared in the .ci file the same way as ordinary messages:
Like all messages, the class PMessage needs to inherit from CMessage_PMessage and should provide two static methods: pack and unpack. These methods are called by the CHARM++ runtime system, when the message is determined to be crossing address-space boundary. The prototypes for these methods are as follows:
Typically, the following tasks are done in pack method:
On the receiving processor, the unpack method is called. Typically, the following tasks are done in the unpack method:
Here is an example of a packed-message implementation:
While serializing an arbitrary data structure into a flat buffer, one must be very wary of any possible alignment problems. Thus, if possible, the buffer itself should be declared to be a flat struct. This will allow the C++ compiler to ensure proper alignment of all its member fields.
By default, CHARM++ will process the messages you send in roughly FIFO order. For most programs, this behavior is fine. However, some programs need more explicit control over the order in which messages are processed. CHARM++ allows you to control queueing behavior on a per-message basis.
The simplest call available to change the order in which messages are processed is CkSetQueueing.
void CkSetQueueing(MsgType message, int queueingtype)
where queueingtype is one of the following constants:
The first two options, CK_QUEUEING_FIFO and CK_QUEUEING_LIFO, are used as follows:
When message msg1 arrives at its destination, it will be pushed onto the end of the message queue as usual. However, when msg2 arrives, it will be pushed onto the front of the message queue.
The other four options involve the use of priorities. To attach a priority field to a message, one needs to set aside space in the message's buffer while allocating the message. To achieve this, the size of the priority field in bits should be specified as a placement argument to the new operator, as described in Section 3.4.1. Although the size of the priority field is specified in bits, it is always padded to an integral number of ints. A pointer to the priority part of the message buffer can be obtained with this call:
unsigned int *CkPriorityPtr(MsgType msg)
There are two kinds of priorities which can be attached to a message: integer priorities and bitvector priorities. Integer priorities are quite straightforward. One allocates a message, setting aside enough space (in bits) in the message to hold the priority, which is an integer. One then stores the priority in the message. Finally, one informs the system that the message contains an integer priority using CkSetQueueing:
The predefined constant CK_QUEUEING_IFIFO indicates that the message contains an integer priority, and that if there are other messages of the same priority, they should be sequenced in FIFO order (relative to each other). Similarly, a CK_QUEUEING_ILIFO is available. Note that MAXINT is the lowest priority, and NEGATIVE_MAXINT is the highest priority.
Bitvector priorities are somewhat more complicated. Bitvector priorities are arbitrary-length bit-strings representing fixed-point numbers in the range 0 to 1. For example, the bit-string ``001001'' represents the number .001001binary. As with the simpler kind of priority, higher numbers represent lower priorities. Unlike the simpler kind of priority, bitvectors can be of arbitrary length, therefore, the priority numbers they represent can be of arbitrary precision.
Arbitrary-precision priorities are often useful in AI search-tree applications. Suppose we have a heuristic suggesting that tree node should be searched before tree node . We therefore designate that node and its descendants will use high priorities, and that node and its descendants will use lower priorities. We have effectively split the range of possible priorities in two. If several such heuristics fire in sequence, we can easily split the priority range in two enough times that no significant bits remain, and the search begins to fail for lack of meaningful priorities to assign. The solution is to use arbitrary-precision priorities, i.e. bitvector priorities.
To assign a bitvector priority, two methods are available. The first is to obtain a pointer to the priority field using CkPriorityPtr, and to then manually set the bits using the bit-setting operations inherent to C. To achieve this, one must know the format of the bitvector, which is as follows: the bitvector is represented as an array of unsigned integers. The most significant bit of the first integer contains the first bit of the bitvector. The remaining bits of the first integer contain the next 31 bits of the bitvector. Subsequent integers contain 32 bits each. If the size of the bitvector is not a multiple of 32, then the last integer contains 0 bits for padding in the least-significant bits of the integer.
The second way to assign priorities is only useful for those who are using the priority range-splitting described above. The root of the tree is assigned the null priority-string. Each child is assigned its parent's priority with some number of bits concatenated. The net effect is that the entire priority of a branch is within a small epsilon of the priority of its root.
It is possible to utilize unprioritized messages, integer priorities, and bitvector priorities in the same program. The messages will be processed in roughly the following order:
A final warning about prioritized execution: CHARM++ always processes messages in roughly the order you specify; it never guarantees to deliver the messages in precisely the order you specify. However, it makes a serious attempt to be ``close'', so priorities can strongly affect the efficiency of your program.
Immediate messages are specical messages that skip the Charm scheduler, they can be executed in an ``immediate'' fashion even in the middle of a normal running entry method. They are supported only in nodegroup. Also see Section 3.2.1 and example in charm/pgms/charm++/megatest/immediatering.C.
October 08, 2008
Charm Homepage