Search code examples
c++rcpp

Conditional operations inside C++ functions without loss of speed or code duplication: use macros, inline functions, templates or otherwise?


I'm developing some high-performance statistical functions and trying to make them perform different operations based on some function arguments. The present problem is to develop a general code that flexibly performs differencing, partial / generalized differencing, growth rates and log-differencing based on function arguments. To give a basic example:

// General code to first-difference a vector:
std::vector<double> diff(std::vector<double> x, int ret = 1, double rho = 1, ...) { // ... = more arguments               int l = x.size();
                         std::vector<double> res(l);
                         for (int i = 1; i < l; ++i) res[i] = FUN(x[i], x[i - 1]); 
                         // rest of code ...
}

Now FUN(y, x) is the thing I want to vary efficiently based on arguments to ret and rho. For simple differences it is y-x, for generalized differences it is y-rho*x, for log differences it is log(y/x), for growth rates (y-x)*(100/x), and more options could be added. The code is applied to large datasets and needs to be fast, so optimally I would use something like a conditionally created macro for FUN, i.e. something like:

std::vector<double> diff(std::vector<double> x, int ret = 1, double rho = 1, ...) {
                         int l = x.size();
                         std::vector<double> res(l);
                         #define FUN(y, x) (ret==1 && rho==1) ? ((y)-(x)) : \
                                           (ret==1) ? ((y)-rho*(x)) :      \
                                           (ret==2) ? (log((y)/(x))) :     \
                                           (ret==3) ? ((y)-(x))*(100/(x))                           
                         for (int i = 1; i < l; ++i) res[i] = FUN(x[i], x[i - 1]); 
}

This works, but judging from the reduced code speed it seems to me that I am not conditionally creating a macro but just one macro and each time FUN is called, all the conditions are evaluated to perform the right operation. I have looked a bit into these preprocessor commands (#if, #elif, #else, #endif, #define and #undef), but it seems to me you cannot use them to conditionally create a macro based on function arguments. A second approach could be using inline functions, i.e.:

inline double do_diff(double y, double x, double r) {
  return y-x; 
}
inline double do_gdiff(double y, double x, double r) {
  return y-r*x;
}
inline double do_logdiff(double y, double x, double r) {
  return log(y/x); 
}
inline double do_growth(double y, double x, double r) {
  return (y-x)*(100/x); 
}

std::vector<double> diff(std::vector<double> x, int ret = 1, double rho = 1, ...) {
                         int l = x.size();
                         std::vector<double> res(l);
                         auto FUN = (ret==1 && rho==1) ? do_diff : 
                                    (ret==1) ? do_gdiff :       
                                    (ret==2) ? do_logdiff :      
                                    (ret==3) ? do_growth;
                         for (int i = 1; i < l; ++i) res[i] = FUN(x[i], x[i - 1], rho); 
}

The problem here is just that it reduces the code speed around 1.5 times. Given that these are really simple operations and this code is supposed to be as fast as possible, I would rather avoid this. So my question is: Is there any way to vary the operation performed by FUN that comes at a negligible performance cost?

Note: Code duplication here is undesirable as the actual code I'm working on is far more complex, i.e. it can perform iterated differences on unordered panel-data etc, about 700 lines in which FUN enters in multiple places.


Solution

  • Template seem more appropriate:

    template <typename F>
    std::vector<double> diff(std::vector<double> x, F f, double rho = 1, ...) {
         int l = x.size();
         std::vector<double> res(l);
         for (int i = 1; i < l; ++i) res[i] = f(x[i], x[i - 1], rho); 
         // ...
    }
    
    std::vector<double> diff(const std::vector<double>& x, ret = 1, double rho = 1, ...) {
        switch (ret)
        {
            case 1: return (rho == 1) ?
                           diff(x, []{double y, double x, double r) { return y-x; }, rho) :
                           diff(x, []{double y, double x, double r) { return y-r*x; }, rho);
            case 2: return diff(x, []{double y, double x, double r) { return log(y/x); }, rho);
            case 3: return diff(x, []{double y, double x, double r) { return (y-x)*(100/x); }, rho);
        }
        throw std::runtime_error("Invalid argument");
    }
    

    It seems even that rho could only be captured by (one) lambda, allowing to get rid of one parameter.