1

There is a class with two member template functions: fun1(), fun2(). I want to call one of the functions decided by a const bool class member : willCallFun1. There is a solution like fun() in the code, but the if-else branch will check every times while fun() is called. I want avoid branch. Since when the Foo object is constructed, const bool willCallFun1 has been set, so which function of fun1 and fun2 will be used can already be determined.

class Foo {
public:

  const bool willCallFun1;

  Foo(bool b) : willCallFun1{b}{
  }

  template<typename T>
  void fun1(T t) {
    std::cout << "fun1 "<< t << "\n";
  }

  template<typename T>
  void fun2(T t) {
    std::cout << "fun2 "<< t << "\n";
  }

  template<typename T>
  void fun(T t){
    if (willCallFun1) fun1<T>(t);
    else fun2<T>(t);
  }

};

int main() {
    Foo f1{true};
    f1.fun(123);
    f1.fun("hi");

    Foo f2{false};
    f2.fun(456);
    f2.fun("hello");
}

If fun1, fun2 are non-template functions, I can add a new member in the class, such as a function pointer or std::function to bind one of them at the constructor. I can't find a way to do it when they are template functions. I would be very grateful if someone could answer.

Wayne Tseng
  • 137
  • 5
  • Does the `bool` have to be set at runtime? Can you make it into a template parameter instead? – Ted Lyngmo Jan 12 '23 at 16:44
  • Yes, in my project the bool is decided by the user, so it need to be set at runtime. – Wayne Tseng Jan 12 '23 at 16:49
  • Do you think calling `std::function` will be faster than one, probably correctly predicted (if you are concerning with this), branch? – sklott Jan 12 '23 at 16:49
  • 1
    *"but the if-else branch will check every times while fun() is called. I want avoid branch"* - Did you (micro-)benchmark this? How are you certain the compiler isn't doing a simple computed goto here? – StoryTeller - Unslander Monica Jan 12 '23 at 16:50
  • I'm not familiar with how compilers handle branching. I may have a misunderstanding that branching is an expensive operation. Since the condition is a constant, will the compiler's branch prediction be quite accurate on the above example? – Wayne Tseng Jan 12 '23 at 16:57
  • @WayneTseng If you call `fun` a couple of times I would assume that the branch prediction will be pretty accurate, yes. That's why I suggest that you _test_ if the branchless approach in my answer is actually faster. I suspect that it will not be. – Ted Lyngmo Jan 12 '23 at 17:00
  • somewhat relevant https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array. As a general rule of thumb you should not be afraid of certain stuff havin negative impact on performance unless you measured and *know* that it actually has impact. Branching is what cpus do all day and they are really good at it (or rather good at not branching when you'd expect one) – 463035818_is_not_an_ai Jan 12 '23 at 17:03
  • The linked post is so cool. I never thought about it. – Wayne Tseng Jan 12 '23 at 17:17

1 Answers1

1

Since you can't make willCallFun1 into a template parameter and use if constexpr to decide which function to call, you could create an array of member function pointers and lookup the correct one using willCallFun1 as an index variable.

template <typename T>
void fun(T t) {
    using fun_sig = void(Foo::*)(T);

    static constexpr fun_sig funs[] = {&Foo::fun2<T>, &Foo::fun1<T>};

    (this->*funs[willCallFun1])(t);
}

Demo

I'd test if this is actually faster than having a simple if though. I've done this before with disappointing results.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • Thank you for your answer! This is a cool approach that I hadn't thought of. – Wayne Tseng Jan 12 '23 at 17:05
  • imho last sentence is important. You are trading a branch vs a indirection. A priori there is not much reason to expect one being better than the other. – 463035818_is_not_an_ai Jan 12 '23 at 17:05
  • @WayneTseng You're welcome! It _can_ be useful in certain situations, but I suspect this is not one of them :-) – Ted Lyngmo Jan 12 '23 at 17:06
  • @463035818_is_not_a_number Indeed! And the `static` _may_ be counted as a branch if one is picky. Initializing it without `static` would make it truly branchless I guess. – Ted Lyngmo Jan 12 '23 at 17:07
  • I'm starting to learn about branch prediction. I was convinced it would be better to keep the original branch. – Wayne Tseng Jan 12 '23 at 17:15
  • I think branch predictor works better than jump predictor. – Jarod42 Jan 12 '23 at 17:17
  • @WayneTseng A branchless approach is probably very good when it doesn't cost even more than leaving it up to the CPU to predict and sometimes branch. – Ted Lyngmo Jan 12 '23 at 17:18
  • @WayneTseng I'd actually also expect the `if` that always takes the same direction to be faster than the array. But don't trust anybody but your code. You really need to measure it to know – 463035818_is_not_an_ai Jan 12 '23 at 17:19
  • It seems like gcc and clang aren't good enough even at `-O3` to optimize this well. Code is generated that looks like `auto* pmf = funs[willCallFun1]; if (is_virtual(pmf)) [[likely]] { pmf = this->vtable[get_offset(pmf)]; } jump_to_function(pmf); }`, so this actually creates an extra branch+jump. – Artyer Jan 12 '23 at 18:02