Search code examples
c++static-methodsfunctorc++23

Efficiency advantage of C++ 23 static operator()


I'm looking at the motivation for static operator() laid out in this article

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1169r2.html

where they explain that function objects, frequently used in STL algorithms incur the extra cost of storing their this pointer in a register, even if the operator() does not use it.

To this end they give the following code example: https://godbolt.org/z/ajTZo2

Where running the algorithm to use the function object's static function rather than the non static operator() does indeed produce shorter assembly code. (They run it once with -DSTATIC and once without )

struct X {
    bool operator()(int) const;
    static bool f(int);
};

inline constexpr X x;

int count_x(std::vector<int> const& xs) {
    return std::count_if(xs.begin(), xs.end(),
#ifdef STATIC
    X::f
#else
    x
#endif
    );
}   

However, when I change the functions to actually do something, whether it is to just return true, or compare the parameter to a hard coded value, the differences seem to disappear, and it appears the produced assembly is identical, leaving me still searching for the advantage of a static operator().

#include <vector>
#include <algorithm>
using namespace std;

struct X {
    bool operator()(int data ) { return data > 5; } ;
    static bool f(int data) { return data > 5; } ;
};

inline constexpr X x;

int count_x(std::vector<int> const& xs) {
    return std::count_if(xs.begin(), xs.end(),
#ifdef STATIC
    X::f
#else
    x
#endif
    );
}

can anyone explain?

Edit: After posting this question and realizing from the comments that inlining may be the reason for the identical assembly, I changed the code to the following which again makes the assembly for the static version more efficent.

struct X {
    bool operator()(int data ) ;
    static bool f(int data);
};

 __attribute__ ((noinline))  bool X::operator() (int data) {
     { return data > 5; } 
}

 __attribute__ ((noinline)) bool X::f(int data )
     { return data > 5; } 

inline constexpr X x;

int count_x(std::vector<int> const& xs) {
    return std::count_if(xs.begin(), xs.end(),
#ifdef STATIC
    X::f
#else
    x
#endif
    );
}

Solution

  • If the function gets inlined, then of course it doesn't matter what the actual calling convention is going to be - it got inlined. But the function doesn't get inlined, then you have to emit a call to the function - which is going to end up doing unnecessary work.

    That's basically the point of being able to annotate call operators as static. If you don't need an object parameter, just make it clear from the signature that you don't need an object parameter. You can already do this on named functions, it's just that you couldn't do it if your member function happened to be named (). This was an arbitrary restriction that prevented you from clearly expressing your intent.

    Plus it makes the compiler's job easier - don't have to optimize out the object parameter if there was never one to begin with. Inlining failures are a death-by-a-thousand-cuts kind of ordeal, so every little bit helps.

    (Source: this was my proposal)