Skip to content

LatticesDesign

Sebastian Schmieschek edited this page Aug 12, 2015 · 2 revisions

Lattices Design

This page concerns the design of the code implementing multiple lattices (DmQn) to allow for use in HemeLB. Considerations are to minimise the use of virtual function table lookups (i.e. use of runtime polymorphism) in performance critical areas, to minimise the build time following a change in code affecting lattices (relating to the amount of code that is templated over the lattice type) and minimise code complexity.

A Useful List

The current D3Q15 lattice class is referenced in 280 places. Of those references, nearly 180 are to the NUMVECTORS constant (= 15). Outside the lb namespace, there are a few references to the velocity vector set and a few to the inverse vector for each in the set. There are two references in the vis namespace to the function for computing the density and velocity (note that this is a function of the vector set and an array of length NUMVECTORS). It is also a set of values we are considering having cached in the near future; it's possible that the only lattice information required outside the lb namespace could be the number of vectors, the vectors themselves and the indices of their inverses.

Design 1: pure static

All classes needing access to the lattice object are templated over that lattice object. Most of HemeLB will have to be in header files and therefore compilation times will be long. The lack of dynamic polymorphism will avoid any virtual fn table lookups.

class D3Q15{
  static const int NUM_VECTORS = 15;
}

template<typename LatticeType>
class AnotherClass{
  void AFunction()
  {
    printf("%i", LatticeType::NUMVECTORS);
  }
}

Design 2: pure dynamic

The lattice is passed around in the same way as other objects, via a pointer. All accesses are via dynamic function lookup. An abstract base class is defined, effectively an interface, which each lattice type implements. Builds are fast, running is potentially slow though you could consider implementing a kind of local cache of function pointers in the performance-critical areas. There would be no way of getting as-good performance as the pure static way, without such a hack.

class Lattice{
  virtual int GetNumVectors() const = 0;
}

class D3Q15 : public Lattice{
  int GetNumVectors() const
  {
    return 15;
  }
}

class AnotherClass{
  void AFunction(const Lattice* lattice)
  {
    printf("%i", lattice->GetNumVectors());
  }
}

Design 3: Hybrid

Performance-critical bits of the code (realistically, the whole lb namespace) statically access the lattice object via a template parameter, other bits access the lattice object using a pointer and dynamic polymorphism. This will give a more complex hierarchy in the code and require use of the CRTP to convert between function lookups on an abstract instance. Build times will be much better than the pure static case because less code will require recompiling after changes affecting lattice files. Note that classes which take a parameter from the lb namespace would need to be templated too. Currently, I can only find one of these which has a very weak dependency (requiring the value of a single int from the LB class).

class Lattice{
  virtual int GetNumVectors() const = 0;
}

template<typename DmQn>
class LatticeImplementation : public Lattice
{
  int GetNumVectors() const
  {
    return DmQn::GetNumVectors();
  }
}

class D3Q15 : public LatticeImplementation<D3Q15>{
  static int GetNumVectors()
  {
    return 15;
  }
}

template<typename latticeType>
class LbCritical{
  void AFunction()
  {
    printf("%i", latticeType::GetNumVectors());
  }
}

class AnotherClass{
  void AFunction(const Lattice* lattice)
  {
    printf("%i", lattice->GetNumVectors());
  }
}

Design 4: A different hybrid

The final design involves two different classes for the static and dynamic accesses, based on the information which is required outside of the lb namespace. I haven't thought about it as hard as the other 3 designs, but I think it offers all of the benefits of design 3 (fast compilation, fast running) with less code complexity.

// This struct may be used anywhere lattice data is required
struct LatticeInfo{
  const int NumVectors;
  const util::Vector3D<int>* VectorSet;
  const int* InverseVectorIndices;
}

// Derivatives of this class use the CRTP; it is only accessed within the lb namespace.
template<typename DmQn>
class LatticeImplementation
{
  int GetNumVectors() const
  {
    return DmQn::GetNumVectors();
  }

  const LatticeInfo* GetLatticeInfo() const
  {
    return DmQn::GetLatticeInfo();
  }
}

class D3Q15 : public LatticeImplementation<D3Q15>{
  static int GetNumVectors()
  {
    return info.NumVectors;
  }

  static const LatticeInfo* GetLatticeInfo()
  {
    return &info;
  }

  private:
   static LatticeInfo* info; // Initialise elsewhere via the singleton pattern. 
   // Don't use this object from within the D3Q15 class, instead use static constants.
}

template<typename latticeType>
class LbCritical{
  void AFunction()
  {
    printf("%i", latticeType::GetNumVectors());
  }
}

class AnotherClass{
  void AFunction(const LatticeInfo* info)
  {
    printf("%i", info->NumVectors);
  }
}
Clone this wiki locally