Project

General

Profile

Task #2221

Avoid preprocessor for SIMD functions

Added by Roland Schulz over 2 years ago. Updated over 1 year ago.

Status:
New
Priority:
Normal
Assignee:
-
Category:
-
Target version:
-
Difficulty:
uncategorized
Close

Description

To further avoid using the preprocessor for SIMD function, we need to be able to use enable_if on functions which should only work for certain SIMD features. Because two-phase lookup will give an error on non-dependent names and because enable_if only works if it depends on the SIMD template argument, I suggest to have the SIMD functions/types in a wrapper class (one for each implementation).

The resulting code (this is simplified code, e.g. the load function is missing its argument) would look similar to:

#include <cstdio>
#include <type_traits>

struct AVXSimd {  //defined in each implementation                                                                                                                                                                                            
    using haveFloat = std::true_type; //why GMX_SIMD_HAVE_FLOAT not HAS?                                                                                                                                                                      
    struct Real { Real operator+(Real) {return Real();} };
    static Real load() {return Real();}
};
struct NoSimd {
    using haveFloat = std::false_type;
};

using DefaultSimd=AVXSimd;
//using DefaultSimd=NoSimd;                                                                                                                                                                                                                   

template<typename T=DefaultSimd>
typename std::enable_if<T::haveFloat::value>::type f() {
    printf("simd\n");
    using Real = typename T::Real; //avoiding typename T:: on each one                                                                                                                                                                        
    Real x;
    auto y = T::load(); //freestanding functions require the T:: prefix                                                                                                                                                                       
    x+y;
}

template<typename T=DefaultSimd>
typename std::enable_if<!T::haveFloat::value>::type f() {
    printf("fallback\n");
}

int main() {
    f();
    return 1;
}

The wrapper class could be made the only interface used by all functions or it could be a wrapper around the existing freestanding functions/types only used by code wanting to use templates with enable_if.

The advantages of enable_if over #if are:
- Both code paths are checked for syntax errors.
- One can execute a function with a different SIMD implementation than the default.
- Potentially better error messages when getting the conditional logic wrong (e.g. #if errors are confusing when nesting of them is wrong)

Disadvantages:
- possible slower compile time.
- maybe harder to read for some people.
- typename keyword is required for simd types

Should we have those wrapper classes and if so as the only interface or as an optional interface?


Related issues

Related to GROMACS - Task #2216: GROMACS SIMD acceleration: generation 3New

History

#1 Updated by Roland Schulz over 2 years ago

  • Related to Task #2216: GROMACS SIMD acceleration: generation 3 added

#2 Updated by Mark Abraham over 2 years ago

Looks like a great idea to me.

The need to use typename for SIMD types can probably be circumvented with using Real = typename T::real.

I'm always happy to trade compiler time for the time of developers and maintainers. For a somewhat OT example, I've spent a lot of time going back and fixing ifndef NDEBUG paths that weren't tested for compilation / functionality when we made the first implementation. Enough that I would consider something like

constexpr bool debugMode_g =
#if defined NDEBUG
false
#else
true
#endif
;

template <bool debugMode = debugMode_g>
void func()
{
  if (debugMode) { /* check */ }
  /* real code */
}

#3 Updated by Roland Schulz over 2 years ago

  • Description updated (diff)

The using works like you suggested. I think this is nicer and I updated the example in the description.

It would be possible to have the SIMD class only contain the definitions (e.g. haveFloat) and keep the types (e.g. SimdFloat) and functions (e.g. load) outside (with dummy implementations to avoid two-phase namelookup errors). This class could still be used as a template argument for enable_if. This would have the advantage that the T:: and the using/typename wouldn't be needed. But we would loose that we can call the same function with different SIMD implementations. I think having the option two call multiple versions might be useful in the future, thus I think the proposed solution is probably better.

Whether the wrapper class is going to be the only interface or only a wrapper around the free standing version can be decided later. We can first make it a wrapper and then see whether we want to move all usages over and if so retire the original interface.

Also available in: Atom PDF