vtk-m2/docs/changelog/array-handle-multiplexer.md
Kenneth Moreland eaf87e4519 Remove special ArrayHandleMultiplexer template case
There was a special case for ArrayHandleMultiplexer where if you gave it
just one type it would treat that as a value type rather than an array
to support and instead provide a default list of types. However, GCC 4.8
is having trouble compiling the code to create the default list, the
semantics are confusing, and the more I think about it the less likely I
think we will need this functionality. So, just getting rid of that.
2019-07-30 14:48:35 -06:00

8.2 KiB

Add ArrayHandleMultiplexer

vtkm::cont::ArrayHandleMultiplexer is a fancy ArrayHandle that can mimic being any one of a list of other ArrayHandles. When declared, a set of a list of ArrayHandles is given to ArrayHandleMultiplexer. To use the ArrayHandleMultiplexer it is set to an instance of one of these other ArrayHandles. Thus, once you compile code to use an ArrayHandleMultiplexer, you can at runtime select any of the types it supports.

The intention is convert the data from a vtkm::cont::VariantArrayHandle to a vtkm::cont::ArrayHandleMultiplexer of some known types. The ArrayHandleMultiplexer can be compiled statically (that is, no virtual methods are needed). Although the compiler must implement all possible implementations of the multiplexer, two or more ArrayHandleMultiplexers can be used together without having to compile every possible combination of all of them.

Motivation

ArrayHandle is a very flexible templated class that allows us to use the compiler to adapt our code to pretty much any type of memory layout or on-line processing. Unfortunately, the template approach requires the code to know the exact type during compile time.

That is a problem when retrieving data from a vtkm::cont::VariantArrayHandle, which is the case, for example, when getting data from a vtkm::cont::DataSet. The actual type of the array stored in a vtkm::cont::VariantArrayHandle is generally not known at compile time at the code location where the data is pulled.

Our first approach to this problem was to use metatemplate programming to iterate over all possible types in the VariantArrayHandle. Although this works, it means that if two or more VariantArrayHandles are dispatched in a function call, the compiler needs to generate all possible combinations of the two. This causes long compile times and large executable sizes. It has lead us to limit the number of types we support, which causes problems with unsupported arrays.

Our second approach to this problem was to create ArrayHandleVirtual to hide the array type behind a virtual method. This works very well, but is causing us significant problems on accelerators. Although virtual methods are supported by CUDA, there are numerous problems that can come up with the compiled code (such as unknown stack depths or virtual methods extending across libraries). It is also unknown what problems we will encounter with other accelerator architectures.

ArrayHandleMultiplexer is meant to be a compromise between these two approaches. Although we are still using metatemplate programming tricks to iterate over multiple implementations, this compiler looping is localized to the code to lookup values in the array. This, it is a small amount of code that needs to be created for each version supported by the ArrayHandle. Also, the code paths can be created independently for each ArrayHandleMultiplexer. Thus, you do not get into the problem of a combinatorial explosion of types that need to be addressed.

Although ArrayHandleMultiplexer still has the problem of being unable to store a type that is not explicitly listed, the localized expression should allow us to support many types. By default, we are adding lots of ArrayHandleCasts to the list of supported types. The intention of this is to allow a filter to specify a value type it operates on and then cast everything to that type. This further allows us to reduce combination of types that we have to support.

Use

The ArrayHandleMultiplexer templated class takes a variable number of template parameters. All the template parameters are expected to be types of ArrayHandles that the ArrayHandleMultiplexer can assume.

For example, let's say we have a use case where we need an array of indices. Normally, the indices are sequential (0, 1, 2,...), but sometimes we need to define a custom set of indices. When the indices are sequential, then an ArrayHandleIndex is the best representation. Normally if you also need to support general arrays you would first have to deep copy the indices into a physical array. However, with an ArrayHandleMultiplexer you can support both.

vtkm::cont::ArrayHandleMultiplexer<vtkm::cont::ArrayHandleIndex,
                                   vtkm::cont::ArrayHandle<vtkm::Id>> indices;
indices = vtkm::cont::ArrayHandleIndex(ARRAY_SIZE);

indices can now be used like any other ArrayHandle, but for now is behaving like an ArrayHandleIndex. That is, it takes (almost) no actual space. But if you need to use explicit indices, you can set the indices array to an actual array of indices

vtkm::cont::ArrayHandle<vtkm::Id> indicesInMemory;
// Fill indicesInMemory...

indices = indicesInMemory;

All the code that uses indices will continue to work.

Variant

To implement ArrayHandleMultiplexer, the class vtkm::internal::Variant was introduced. Although this is an internal class that is not exposed through the array handle, it is worth documenting its addition as it will be useful to implement other multiplexing type of objects (such as for cell sets and locators).

vtkm::internal::Variant is a simplified version of C++17's std::variant or boost's variant. One of the significant differences between VTK-m's Variant and these other versions is that VTK-m's version does not throw exceptions on error. Instead, behavior becomes undefined. This is intentional as not all platforms support exceptions and there could be consequences on just the possibility for those that do.

Like the aforementioned classes that vtkm::internal::Variant is based on, it behaves much like a union of a set of types. Those types are listed as the Variant's template parameters. The Variant can be set to any one of these types either through construction or assignment. You can also use the Emplace method to construct the object in a Variant.

vtkm::internal::Variant<int, float, std::string> variant(5);
// variant is now an int.

variant = 5.0f;
// variant is now a float.

variant.Emplace<std::string>("Hello world");
// variant is now an std::string.

The Variant maintains the index of which type it is holding. It has several helpful items to manage the type and index of contained objects:

  • GetIndex(): A method to retrieve the template parameter index of the type currently held. In the previous example, the index starts at 0, becomes 1, then becomes 2.
  • GetIndexOf<T>(): A static method that returns a constexpr of the index of a given type. In the previous example, variant.GetIndexOf<float>() would return 1.
  • Get<T or I>(): Given a type, returns the contained object as that type. Given a number, returns the contained object as a type of the corresponding index. In the previous example, either variant.Get<1>() or variant.Get<float>() would return the float value. The behavior is undefined if the object is not the requested type.
  • IsValid(): A method that can be used to determine whether the Variant holds an object that can be operated on.
  • Reset(): A method to remove any contained object and restore to an invalid state.

Finally, Variant contains a CastAndCall method. This method takes a functor followed by a list of optional arguments. The contained object is cast to the appropriate type and the functor is called with the cast object followed by the provided arguments. If the functor returns a value, that value is returned by CastAndCall.

CastAndCall is an important functionality that makes it easy to wrap multiplexer objects around a Variant. For example, here is how you could implement executing the Value method in an implicit function multiplexer.

class ImplicitFunctionMultiplexer
{
  vtkm::internal::Variant<Box, Plane, Sphere> ImplicitFunctionVariant;
  
  // ...
  
  struct ValueFunctor
  {
    template <typename ImplicitFunctionType>
	vtkm::FloatDefault operator()(const ImplicitFunctionType& implicitFunction,
	                              const vtkm::Vec<vtkm::FloatDefault, 3>& point)
	{
	  return implicitFunction.Value(point);
	}
  };
  
  vtkm::FloatDefault Value(const vtkm::Vec<vtkm::FloatDefault, 3>& point) const
  {
    return this->ImplicitFunctionVariant.CastAndCall(ValueFunctor{}, point);
  }