The PUP framework is a generic way to describe the data in an object and to use that description for any task requiring serialization. The CHARM++ system can use this description to pack the object into a message, and unpack the message into a new object on another processor. The name thus is a contraction of the words Pack and UnPack (PUP).
Like many C++ concepts, the PUP framework is easier to use than describe:
This class's pup routine describes the fields of a foo to CHARM++. This allows CHARM++ to: marshall parameters of type foo across processors, translate foos across processor architectures, read and write foos to disk files, inspect and modify foo objects in the debugger, and checkpoint and restart calculations involving foos.
Your object's pup routine must save and restore all your object's data. As shown, you save and restore a class's contents by writing a routine c alled ``pup'' which passes all the parts of the class to an object of type PUP::er, which does the saving or restoring. We often use ``pup'' as a verb, meaning ``to save/restore the value of'' or equivalently, ``to call the pup routine of''.
Pup routines for complicated objects normally call the pup routines for their simpler parts. Since all objects depend on their immediate superclass, the first line of every pup routine is a call to the superclass's pup routine--the only time you shouldn't call your superclass's pup routine is when you don't have a superclass. If your superclass has no pup routine, you must pup the values in the superclass yourself.
The recommended way to pup any object a is to use p|a;.
This syntax is an operator | applied to the PUP::er p
and the user variable a.
The p|a; syntax works wherever a is:
p|a; copies the data in-place.
This is equivalent to passing the type directly to the PUP::er
using p(a).
p|a; calls the object's pup routine.
This is equivalent to the statement a.pup(p);.
p|a; allocates and copies the appropriate subclass.
p|a; copies the object as plain bytes, like memcpy.
operator | defined.
In this case, p|a; calls the custom operator |.
For container types, you must simply pup each element of the container. For arrays, you can use the utility routine PUParray, which takes the PUP::er, the array base pointer, and the array length. This utility routine is defined for user-defined types T as:
If the variable is from the C++ Standard Template Library, you can include
operator|'s for STL vector, map, list, pair, and string, templated
on anything, by including the header ``pup_stl.h''.
As usual in C++, pointers and allocatable objects usually require special handling. Typically this only requires a p.isUnpacking() conditional block, where you perform the appropriate allocation. See Section 3.16.3 for more information and examples.
If the object does not have a pup routine, and you cannot add one or use
PUPbytes, you can define an operator| to pup the object.
For example, if myClass contains two fields a and b, the
operator| might look like:
For classes and structs with many fields, it can be tedious and error-prone to list all the fields in the pup routine. You can avoid this listing in two ways, as long as the object can be safely copied as raw bytes--this is normally the case for simple structs and classes without pointers.
PUPbytes(myClass) macro in your header file.
This lets you use the p|*myPtr; syntax
to pup the entire class as sizeof(myClass) raw bytes.
p((void *)myPtr,sizeof(myClass)); in the pup
routine. This is a direct call to pup a set of bytes.
p((char *)myCharArray,arraySize); in the pup
routine. This is a direct call to pup a set of bytes.
Other primitive types may also be used.
Note that pupping as bytes is just like using `memcpy': it does nothing to the data but copy it whole. For example, if the class contains any pointers, you must make sure to do any allocation needed, and pup the referenced data yourself.
Pupping as bytes will prevent your pup routine from ever being able to work across different machine architectures. We don't do this very often yet, but eventually may, so pupping as bytes is currently discouraged.
The PUP::er overhead is very small--one virtual function call for each item or array to be packed/unpacked. The actual packing/unpacking is normally a simple memory-to-memory binary copy.
For arrays of builtin types like ``int" and ``double", or arrays of a type with the ``PUPbytes'' declaration, PUParray uses an even faster block transfer, with one virtual function call per array.
Please note that if your object contains Structured Dagger code (see section ``Structured Dagger'') you must call the generated routine __sdag_pup to correctly pup the Structured Dagger state:
CHARM++ uses your pup routine to both pack and unpack, by passing different types of PUP::ers to it. The routine p.isUnpacking() returns true if your object is being unpacked--that is, your object's values are being restored. Your pup routine must work properly in sizing, packing, and unpacking modes; and to save and restore properly, the same fields must be passed to the PUP::er, in the exact same order, in all modes. This means most pup routines can ignore the pup mode.
Three modes are used, with three separate types of PUP::er: sizing, which only computes the size of your data without modifying it; packing, which reads/saves values out of your data; and unpacking, which writes/restores values into your data. You can determine exactly which type of PUP::er was passed to you using the p.isSizing(), p.isPacking(), and p.isUnpacking() routines. However, sizing and packing should almost always be handled identically, so most programs should use p.isUnpacking() and !p.isUnpacking(). Any program that calls p.isPacking() and does not also call p.isSizing() is probably buggy, because sizing and packing must see exactly the same data.
The p.isDeleting() flag indicates the object will be deleted after calling the pup routine. This is normally only needed for pup routines called via the C or f90 interface, as provided by AMPI or the FEM framework. Other CHARM++ array elements, marshalled parameters, and other C++ interface objects have their destructor called when they are deleted, so the p.isDeleting() call is not normally required--instead, memory should be deallocated in the destructor as usual.
The life cycle of an object with a pup routine is shown in Figure 1. As usual in C++, objects are constructed, do some processing, and are then destroyed.
Objects can be created in one of two ways: they can
be created using a normal constructor as usual; or they
can be created using their pup constructor. The pup constructor
for CHARM++ array elements and PUP::able objects
is a ``migration constructor'' that takes a single ``CkMigrateMessage *";
for other objects, such as parameter marshalled objects,
the pup constructor has no parameters. The pup constructor
is always followed by a call to the object's pup routine in
isUnpacking mode.
Once objects are created, they respond to regular user methods
and remote entry methods as usual. At any time, the object
pup routine can be called in isSizing or isPacking
mode. User methods and sizing or packing pup routines can be called
repeatedly over the object lifetime.
Finally, objects are destroyed by calling their destructor as usual.
If your class has fields that are dynamically allocated, when unpacking these need to be allocated (in the usual way) before you pup them. Deallocation should be left to the class destructor as usual.
The simplest case is when there is no dynamic allocation.
The next simplest case is when we contain a class that is always allocated during our constructor, and deallocated during our destructor. Then no allocation is needed within the pup routine.
If we need values obtained during the pup routine before we can allocate the class, we must allocate the class inside the pup routine. Be sure to protect the allocation with ``if (p.isUnpacking())''.
For example, if we keep an array of doubles, we need to know how many doubles there are before we can allocate the array. Hence we must first pup the array length, do our allocation, and then pup the array data. We could allocate memory using malloc/free or other allocators in exactly the same way.
If our allocated object may be NULL, our allocation becomes much more complicated. We must first check and pup a flag to indicate whether the object exists, then depending on the flag, pup the object.
This sort of code is normally much longer and more error-prone if split into the various packing/unpacking cases.
An array of actual classes can be treated exactly the same way
as an array of basic types. PUParray will pup each
element of the array properly, calling the appropriate operator|.
An array of pointers to classes must handle each element separately, since the PUParray routine does not work with pointers. An ``allocate'' routine to set up the array could simplify this code. More ambitious is to construct a ``smart pointer'' class that includes a pup routine.
Note that this will not properly handle the case where some elements of the array are actually subclasses of foo, with virtual methods. The PUP::able framework described in the next section can be helpful in this case.
If the class foo above might have been a subclass, instead of simply using new foo above we would have had to allocate an object of the appropriate subclass. Since determining the proper subclass and calling the appropriate constructor yourself can be difficult, the PUP framework provides a scheme for automatically determining and dynamically allocating subobjects of the appropriate type.
Your superclass must inherit from PUP::able, which provides the basic machinery used to move the class. A concrete superclass and all its concrete subclasses require these four features:
An abstract superclass--a superclass that will never actually be packed--only needs to inherit from PUP::able and include a PUPable_abstract(className) macro in their body. For these abstract classes, the .ci file, PUPable_decl macro, and constructor are not needed.
For example, if parent is a concrete superclass and child its subclass,
With these declarations, then, we can automatically allocate and pup a pointer to a parent or child using the vertical bar PUP::er syntax, which on the receive side will create a new object of the appropriate type:
This will properly pack, allocate, and unpack obj whether it is actually a parent or child object. The child class can use all the usual C++ features, such as virtual functions and extra private data.
If obj is NULL when packed, it will be restored to NULL when unpacked. For example, if the nodes of a binary tree are PUP::able, one may write a recursive pup routine for the tree quite easily:
This same implementation will also work properly even if the tree's internal nodes are actually subclasses of treeNode.
You may prefer to use the macros PUPable_def(className) and PUPable_reg(className) rather than using PUPable in the .ci file. PUPable_def provides routine definitions used by the PUP::able machinery, and should be included in exactly one source file at file scope. PUPable_reg registers this class with the runtime system, and should be executed exactly once per node during program startup.
Finally, a PUP::able superclass like parent above must normally be passed around via a pointer or reference, because the object might actually be some subclass like child. Because pointers and references cannot be passed across processors, for parameter marshalling you must use the special templated smart pointer classes CkPointer and CkReference, which only need to be listed in the .ci file.
A CkReference is a read-only reference to a PUP::able object--it is only valid for the duration of the method call. A CkPointer transfers ownership of the unmarshalled PUP::able to the method, so the pointer can be kept and the object used indefinitely.
For example, if the entry method bar needs a PUP::able parent object for in-call processing, you would use a CkReference like this:
If the entry method needs to keep its parameter, use a CkPointer like this:
Both CkReference and CkPointer are read-only from the send side--unlike messages, which are consumed when sent, the same object can be passed to several parameter marshalled entry methods. In the example above, we could do:
C and Fortran programmers can use a limited subset of the PUP::er capability. The routines all take a handle named pup_er. The routines have the prototype:
The first call is for use with a single element; the second call is for use with an array. The supported types are char, short, int, long, uchar, ushort, uint, ulong, float, and double, which all have the usual C meanings.A byte-packing routine
pup_isSizing, pup_isPacking, pup_isUnpacking, and pup_isDeleting calls are also available. Since C and Fortran have no destructors, you should actually deallocate all data when passed a deleting pup_er.
C and Fortran users cannot use PUP::able objects, seeking, or write custom PUP::ers. Using the C++ interface is recommended.
The most common PUP::ers used are PUP::sizer, PUP::toMem, and PUP::fromMem. These are sizing, packing, and unpacking PUP::ers, respectively.
PUP::sizer simply sums up the sizes of the native binary representation of the objects it is passed. PUP::toMem copies the binary representation of the objects passed into a preallocated contiguous memory buffer. PUP::fromMem copies binary data from a contiguous memory buffer into the objects passed. All three support the size method, which returns the number of bytes used by the objects seen so far.
Other common PUP::ers are PUP::toDisk, PUP::fromDisk, and PUP::xlater. The first two are simple filesystem variants of the PUP::toMem and PUP::fromMem classes; PUP::xlater translates binary data from an unpacking PUP::er into the machine's native binary format, based on a machineInfo structure that describes the format used by the source machine.
It may rarely occur that you require items to be unpacked in a different order than they are packed. That is, you want a seek capability. PUP::ers support a limited form of seeking.
To begin a seek block, create a PUP::seekBlock object with your current PUP::er and the number of ``sections'' to create. Seek to a (0-based) section number with the seek method, and end the seeking with the endBlock method. For example, if we have two objects A and B, where A's pup depends on and affects some object B, we can pup the two with:
Note that without the seek block, A's fields would be unpacked over B's memory, with disasterous consequences. The packing or sizing path must traverse the seek sections in numerical order; the unpack path may traverse them in any order. There is currently a small fixed limit of 3 on the maximum number of seek sections.
System-level programmers may occasionally find it useful to define their own PUP::er objects. The system PUP::er class is an abstract base class that funnels all incoming pup requests to a single subroutine:
The parameters are, in order, the field address, the number of items, the size of each item, and the type of the items. The PUP::er is allowed to use these fields in any way. However, an isSizing or isPacking PUP::er may not modify the referenced user data; while an isUnpacking PUP::er may not read the original values of the user data. If your PUP::er is not clearly packing (saving values to some format) or unpacking (restoring values), declare it as sizing PUP::er.
November 23, 2009
Charm Homepage