CIBC:Documentation:SCIRun:DevManual:DynamicCompilation
From SCIRun Documentation Wiki
Contents |
Dynamic Compilation
Overview
The current version of SCIRun supports a feature which we call dynamic compilation. Dynamic Compilation refers to the ability of compiling pieces of code while SCIRun is executing. This feature requires a compiler being available at run time. The reason for dynamically compiling code is the ability to optimize code for a particular model while SCIRun is being used to solve a particular problem.
SCIRun makes extensive use of C++ templates. Templates are a powerful mechanism that allows writing algorithms or data structures once, while allowing them to be usable with many types of data. However, in order to use any particular type of data, the templates need to know about them at compile time. Dynamic compilation allows templates written in the past to be applied to new data types at run time by dynamically compiling code. Making this into a powerful feature.
Creating a dynamically compiled algorithm
The most common use for dynamic compilation is to create an algorithm that operates on the Field class. As the GenericField class is templated using three classes, an algorithm that operates on this class can consist out of a wide range of possible algorithms. Using dynamic compilation SCIRun compiles the proper algorithm when it knows by means of the user defined fields which algorithms to operate on.
In this chapter creating a dynamic compiled algorithm is demonstrated by means of the ConvertToPointCloud algorithm. This algorithm takes a handle to field and extracts all the node locations from the mesh and returns a field that does not contain any elements anymore but only the actual node points that were part of the input field.
The idea of a dynamically compiling class is the following: A general pointer to the Field is given to the algorithm, this is a pointer to the base class of the Field which is the same for all possible Field types. To discover what type it is a virtual function call into the Field is made revealing its true identity. This type information is used to compile a templated algorithm that uses the specific field type as a template argument. When the algorithm is compiled it will execute the algorithm.
To create a dynamically compiling algorithm we start by defining to classes: a base class and a templated class derived from that.
In this example the base class is called ConvertToPointCloudAlgo: This class contains the general access point for the dynamic compiled algorithm. It takes handles (pointers) to the Field base class. It will examine all the input arguments and determine which type the input Fields actually are. When this is known it will tell the dynamic compiler which algorithm needs to be compiled to carry out the operation with high efficiency.
The second class is a template class called ToPointCloudAlgoT: This class is derived from the base class and has a similar call to the algorithm. This templated class contains the actual algorithm. When the dynamic compiler is invoked it will take in a handle to the base algorithm, internally it will overload this with the handle to an instatiation of the templated algorithm. Since the templated class will overload the access function of the base class with the one defined in the templated version using virtual function, one can now invoke the compiled algorithm by calling the access on the base class.
The base class now defined as following:
class ToPointCloudAlgo : public DynamicAlgoBase
{
public:
virtual bool ToPointCloud(ProgressReporter *pr, FieldHandle input, FieldHandle& output);
};
This class is the general access point to the dynamic algorithms in the class. The function ToPointCloud() is the access function to the dynamically compiled algorithm. It is a good practive to define the access functions to the dynamically compiled algorithms as following:
virtual bool MyFunction(ProgressReporter* pr, FieldHandle input1,
FieldHandle input2, ... FieldHandle& output)
Here the algorithm returns false if it fails and true if it succeeds. In order to forward the error message all dynamic algorithms should take in a pointer to the current ProgressReporter. The ProgressReporter reports everything from the progress the algorithm made, to errors, remarks, warnings and is the general access point to forward messages to the user.
Hence this algorithm can be used in a module as following:
ToPointCloudAlgo algo;
if(!(algo->ToPointCloud(this,input,output)))
{
// algorothm failed
}
Note that the module class has been derived from the ProgressReporter and hence the pointer to the module can be used to initialise the ProgressReporter.
The second class called ToPointCloudAlgoT is the actual implementation of the algorithm. Note that the name of the algorithm ends with a 'T' to denote that the algorithm is templated (this is not strictly needed but improves readability if the code). The second class will look like this:
template <class FSRC, class FDST>
class ToPointCloudAlgoT : public ToPointCloudAlgo
{
public:
virtual bool ToPointCloud(ProgressReporter *pr, FieldHandle input, FieldHandle& output);
};
This class is templated according to the input and output field (FSRC, FDST). This means that the specific field class can be accessed by dynamic casting the input and output to the respective types.
The Templated class
Since the algorithm is templated, we need to define its implemenation in the header file.
template <class FSRC, class FDST>
bool ToPointCloudAlgoT<FSRC, FDST>::ToPointCloud(ProgressReporter *pr, FieldHandle input, FieldHandle& output)
{
The first thing the algorithm should do is to cast the pointer contained in the input handle into a pointer of the proper type. Remember this part of the code is dynamically compiled and hence the class FSRC is of the type of the class behind pointer in input, where input only points to the base class. To access the derived class we need to dynamically cast this pointer into its actual type:
FSRC *ifield = dynamic_cast<FSRC *>(input.get_rep());
In this example the function get_rep() gets the pointer contained in the handle. After getting the pointer it is a good habit to insert a safety check, to test whether we actually have a pointer. Although we should get the pointer, if something went wrong in the dynamic casting we get a zero pointer . As we do not want to use a null pointer, we should add a little safety check;
if (ifield == 0)
{ // The object is of a different type
// Error reporting:
// we forward the specific message to the ProgressReporter and return a
// false to indicate that an error has occured.
// The ProgressReporter returns the error message to the module window
pr->error("ToPointCloud: Could not obtain input field");
return (false);
}
So far we got the pointer to the field. It is really useful to obtain the pointer to the mesh as well. To get a pointer to the specific mesh we use the function get_typed_mesh(). In order to get the proper pointer definition we look into the field class where the type mesh_handle_type is defined. As the type consists of a templatename and the actual type defined inside the templated class we use typename to indicate to the compiler that FSRC::mesh_handle_type still needs to be parsed by specifying the actual template class.
typename FSRC::mesh_handle_type imesh = ifield->get_typed_mesh();
if (imesh == 0)
{
pr->error("ToPointCloud: No mesh associated with input field");
return (false);
}
Now we have the pointers to the input mesh, we need to create an output mesh which is of the type of a PointCloud. A mesh is created as following:
typename FDST::mesh_handle_type omesh = scinew typename FDST::mesh_type();
if (omesh == 0)
{
pr->error("ToPointCloud: Could not create output field");
return (false);
}
Note we use the following memory management strategy here: We store the pointer to the new mesh immediately into a mesh handle. Handles are smart pointers that automatically deallocate memory when the handle object is deallocated (unless the handle has been copied). Using handles to refer to new objects will make sure that, if we encounter an error in the code and we exit with a return (false), all memory will be freed automatically as the handle will be desrtoyed. The handle was not copied in that case and thus the object it is pointing to will be automatically destroyed.
Now we need to implement the actual algorithm: We need to define iterators over the mesh, in this case they depend on the type of the mesh and have to be defined through the templated classes:
typename FSRC::mesh_type::Node::iterator bn, en; typename FSRC::mesh_type::Node::size_type numnodes; imesh->begin(bn); // get begin iterator imesh->end(en); // get end iterator imesh->size(numnodes); // get the number of nodes in the mesh
It is always good to preallocate memory if the number of nodes in the output mesh is known.
omesh->node_reserve(numnodes);
In the next loop the algorithm loops over all the nodes in the input mesh. It extracts the exact location of the point and then inserts the node into the output mesh.
while (bn != en)
{
Point point;
imesh->get_center(point, *bn);
omesh->add_point(point);
++bn;
}
Now the new mesh has been created, the output field can be created. This output field can then be attached to the output FieldHandle.
FDST *ofield = scinew FDST(omesh);
output = ofield;
if (ofield == 0)
{
pr->error("ToPointCloud: Could not create output field");
return (false);
}
The algorithm is completed by copying data from the nodes in the input field into the nodes of the output field. This copying is only done when the data is located at the nodes to begin with. In order to check whether the data is located at the nodes the basis_order() of the mesh is checked.
// Make sure Fdata matches the size of the number of nodes
ofield->resize_fdata();
// Is this a linear input field, if so we can copy data from node to node
if (ifield->basis_order() == 1)
{
typename FSRC::fdata_type::iterator bid = ifield->fdata().begin();
typename FSRC::fdata_type::iterator eid = ifield->fdata().end();
typename FDST::fdata_type::iterator bod = ofield->fdata().begin();
while (bid != eid)
{
*bod = *bid;
++bod; ++bid;
}
}
The algorithm is finished by copying the properties from the input field to the output field.
output->copy_properties(input.get_rep()); // Success: return (true); }
The Base class
The base class is used to compile and access the templated algorithm. The base class contains the same function definition as the template class, however it is this function that is accessed from the rest of SCIRun. Internally it does generate the actual algorithm.
The base class algorithm is defined in a .cc file and reads as follows:
bool ToPointCloudAlgo::ToPointCloud(ProgressReporter *pr, FieldHandle input, FieldHandle& output)
{
The first step is to test whether we received a valid field. A handle can point to no object and hence we need to discard this possibility. Using a null handle will cause the program to crash. Hence it is a good policy to check all incoming handles and to see whether they point to actual objects.
if (input.get_rep() == 0)
{
// If we encounter a null pointer we return an error message and return to
// the program to deal with this error.
pr->error("ToPointCloud: No input field");
return (false);
}
The first thing this algorithm needs to do is to determine the type of the input fields and determine what type the output field should be. The FieldInformation is a helper class that will store all the names of all the components a field is made of. It takes a handle to a field and then determines what the actual type is of the field. As the current Field class has a variety of functions to query for its type the FieldInformation object will do this for you and will contain a summary of all the type information.
// As the output field will be a variation on the input field we initialize // both with the input handle. FieldInformation fi(input); FieldInformation fo(input);
Recent updates to the software allow for quadratic and cubic hermitian representations. However these methods have not fully been exposed yet. Hence the iterators in the field will not consider the information needed to define these non-linear elements. And hence although the algorithm may provide output for these cases and may not fail, the output is mathematically improper and hence for a proper implementation we have to wait until the mesh and field classes are fully completed.
Using the FieldInformation class we can test whether the class is part of any of these newly defined non-linear classes. If so we return an error.
if (fi.is_nonlinear())
{
pr->error("ToPointCloud: This function has not yet been defined for non-linear elements yet");
return (false);
}
The other thing which we need to check is that in case the mesh is already a PointCloud, we only need to copy the input to the output. No algorithm is needed in this case.
if (fi.get_mesh_type() == "PointCloudMesh")
{
output = input;
return (true);
}
The algorithm now needs to define the output field type. This is done by modifying the FieldInformation. By starting with the input field we can maintain properties like the datatype in which the data stored and only change the field class:
fo.set_mesh_type("PointCloudMesh");
fo.set_mesh_basis_type("ConstantBasis");
if (fi.get_basis_type() == "ConstantBasis")
{
// Inform the user of some possible unintented consequence:
pr->remark("Data is defined at the elements: hence removing the data from the field");
// Though we can still perform the operation
fo.set_basis_type("NoDataBasis");
}
The next step is to build the information structure for the dynamic compilation. The only object we need to build to perform a dynamic compilation is the CompileInfo. This object is created and we use the handle to the object to pass the data structure around.
The CompileInfo object contains the following information: The constructor needs the following information:
- An unique filename descriptor which can used in the on-the-fly-libs directory. The FieldInformation object has a function that renders an unique name for each field type. The latter is combined with the name of the algorithm into a unique name for the dynamically compiled file.
- The name of the base class
- The name of the templated class without template descriptors
- The template descriptors separated by commas
The following code generates the CompileInfo object:
SCIRun::CompileInfoHandle ci = scinew CompileInfo( "ALGOToPointCloud."+fi.get_field_filename()+"."+fo.get_field_filename()+".", "ToPointCloudAlgo","ToPointCloudAlgoT", fi.get_field_name() + "," + fo.get_field_name());
The dynamic algorithm will be created by writing a small piece of code in a .cc file. This file needs to know which file to include for the definitions of this algorithm. Using the function add_include includes can be added to the dynamically compiled code:
ci->add_include(TypeDescription::cc_to_h(__FILE__));
This function is defined in the namespace SCIRunAlgo, add a statement 'using namespace SCIRunAlgo' to the dynamic file to be created:
ci->add_namespace("SCIRunAlgo");
ci->add_namespace("SCIRun");
In order to be able to compile the dynamic code it needs to include the descriptions of the mesh/field classes. The following two statements will add the proper include files for both the input and output field types :
fi.fill_compile_info(ci); fo.fill_compile_info(ci);
The next step is to invoke the compiler to compile a piece of code. For the new algorithm we need to create an access point to the dynamically compiled algorithm. In order to do so we create a handle to the basis algorithm class. Note: this is currently a handle to the base class algorithm.
SCIRun::Handle<ToPointCloudAlgo> algo;
The next step is to call the compile() function using the CompileInfo to create the dynamically compiled piece of code and the Handle to the base algorithm, which will be overloaded with the actual algorithm. If the function is a success, the handle algo will point to the dynamically compiled algorithm. Since the access function is virtual, executing it will invoke the dynamic version.
if(!(SCIRun::DynamicCompilation::compile(ci,algo,pr)))
{
// In case we detect an error: we forward the error to the user
// The current system will take the filename of the file that failed to compile
// It will display the error and dynamic file to the user, in the hope it
// will tell something on what went wrong
pr->compile_error(ci->filename_);
// If compilation failed: remove file from on-the-fly-libs directory
SCIRun::DynamicLoader::scirun_loader().cleanup_failed_compile(ci);
return(false);
}
The final step is to invoke dynamic algorithm.
return(algo->ToPointCloud(pr,input,output));
}
Depending on whether dynamic algorithm fails or succeeds, false or true is returned. As error messages are reportered to the ProgressReporter we do not need to handle any error messages here, they automatically are forwarded to the user.
Go back to Documentation:SCIRun:DevManual
