Version: SMASH-3.1
smash::ClebschGordan::ThreeSpinHash Struct Reference

This is one of the possible ways to prepare a hashing mechanism to use a custom object in a std::unordered_map container.

It has been preferred here to use a new struct instead of injecting a specialization into the std namespace, because we are here in the smash namespace and the object is going to be possibly better localized. Since the hash is not trivial, we also preferred this approach to using a lambda function to declare the hashing function.

Definition at line 101 of file clebschgordan_lookup.h.

Public Member Functions

std::size_t operator() (const ThreeSpins &in) const noexcept
 The overload of the operator() is the only needed ingredient to make this class ready to be used as hashing algorithm. More...
 

Member Function Documentation

◆ operator()()

std::size_t smash::ClebschGordan::ThreeSpinHash::operator() ( const ThreeSpins in) const
inlinenoexcept

The overload of the operator() is the only needed ingredient to make this class ready to be used as hashing algorithm.

Since there is not (yet) a C++ standard way of combining hashes, to implement functions like this one, it is necessary to choose a hashing algorithm. The algorithm should minimize the hash collision (different input giving the same output), but at the same time be as fast as possible. One could use boost library approach, which defines

template <typename T>
inline void hash_combine(std::size_t &seed, const T &val)
{
seed ^= std::hash<T>{}(val) + 0x9e3779b9 +
(seed << 6) + (seed >> 2);
}

as function to combine hashes. Then this should be used here on each in member to produce the final hash. Although it has been tested to work, there is a simpler approach.

  1. It can be assumed (asserted in the code), that all ThreeSpins members are numbers smaller than 16 (in absolute value). Hence 5 bits are enough to represent them (numbers between -16 and 15). Using bit-shift operations, it is then possible to build a bit-sequence that in its last 6*5=30 bits contains the 6 integer information, appropriately "converted" to 5-bit sequences that are then concatenated.
  2. To get an int as a 5 bits integer into a std::size_t variable, one way is to cast to it first and then get rid of all but 5 rightmost bits. If e.g. the result variable is 64 bits large, this means to bit-shift to the left by 64-5=59 positions and then again to the right to bring the 5 bits back to the least significant positions (this is only an example, since the size of std::size_t is architecture dependent).
  3. Finally, once having the 6 5-bits integer as std::size_t variables, these can be combined shifting them to the left in a way that the 5 relevant bits do not "overlap" (i.e. occupy different positions) and then summing them. In the implementation we use the minimum needed shift, i.e. having N=6 numbers we shift the first number by (N-1)*5, the second by (N-2)*5 and so on till the last one that does not get shifted. Since both the cast to "5-bits" numbers and the preparation of the sum involve bit-shifts, these are combined together.
Note
A couple of remarks a worth:
  • Using std::bitset would probably make the code easier to read, but it has been benchmarked to be some % slower at low energies.
  • Yet another possibility would be to use mathematics/physics to come up with a map between the 6 integers and a super-index. This has been devised in [45], where an algorithm to efficiently store Clebsch-Gordan coefficient is discussed. In it a way to map the three spins information to a single integer is proposed and this can be used here as hashing function, basically offering the guarantee that no hash collision will occur. However, the simpler hand-made hash discussed above and implemented in the following turned out to be more efficient with GNU compiler (equivalent with LLVM).
Parameters
inThe spin information (meant to be used as input to be hashed)
Returns
std::size_t The calculated hash value

Definition at line 163 of file clebschgordan_lookup.h.

163  {
164  assert(in.j1 >= 0 && in.j1 < 16);
165  assert(in.j2 >= 0 && in.j2 < 16);
166  assert(in.j3 >= 0 && in.j3 < 16);
167  assert(std::abs(in.m1) < 16);
168  assert(std::abs(in.m2) < 16);
169  assert(std::abs(in.m3) < 16);
170  /*
171  * Although strictly speaking, this would only generate a very poor hash,
172  * we prefer making compilation fail if size_t has less than 32 bits.
173  * This is after all very unlikely and we'll deal with it only if needed.
174  */
175  static_assert(sizeof(std::size_t) >= 4);
176  // This is the amount to shift to obtain "5-bits numbers"
177  constexpr auto bitshift = sizeof(size_t) * 8 - 5;
178  // The different shift to the right make the 5-bit occupy different bits
179  return (static_cast<std::size_t>(in.j1) << bitshift >> (bitshift - 25)) +
180  (static_cast<std::size_t>(in.j2) << bitshift >> (bitshift - 20)) +
181  (static_cast<std::size_t>(in.j3) << bitshift >> (bitshift - 15)) +
182  (static_cast<std::size_t>(in.m1) << bitshift >> (bitshift - 10)) +
183  (static_cast<std::size_t>(in.m2) << bitshift >> (bitshift - 5)) +
184  (static_cast<std::size_t>(in.m3) << bitshift >> bitshift);
185  }

The documentation for this struct was generated from the following file: