1

When I try the following code in compiler explorer with -O3 flag, the code generated is reduced down to basically 3 assembly instructions - mov, imul, mov:

#include <functional>

template <typename T>
int o2 (T f, int x)
{
    return f(x);
}

auto f2 = [](int x) -> int { return x*x; };

int x = 2;
int y = o2(f2, x);

Now, instead of templating the function, if I use a std::function as below, the actual call is still 3 instructions but the code for lambda f2 and o2 are still there:

#include <functional>

int o2(std::function<int(int)> f, int x)
{
    return f(x);
}

auto f2 = [](int x) -> int { return x*x; };

int x = 2;
int y = o2(f2, x);

Why isn't the compiler not optimizing them away? Is there a way to force the compiler so that the code generated for these two snippets is the same?

I was advocating for a stronger checks using std::function and replacing the template style. Sometimes the template error messages are not understandable. But this does not turn out to be equivalent or am I missing something?

Christophe
  • 68,716
  • 7
  • 72
  • 138
Mochan
  • 1,329
  • 1
  • 13
  • 27
  • 4
    `std::function` uses type erasure so it is opaque. You should avoid it if you can. – NathanOliver Mar 29 '19 at 18:41
  • 3
    You didn't even tell us what compiler. I'm not sure what kind of answer you're looking for, but optimizer choices aren't in the standard and vary between compilers and even versions. – xaxxon Mar 29 '19 at 18:45
  • You can add "safety" to the template version by using type traits to examine the return type and parameter types of the object and `static_assert` that they are what you want. – NathanOliver Mar 29 '19 at 18:51
  • Is the question really "which compiler knows about `std::function`?"? – curiousguy Mar 29 '19 at 19:15

3 Answers3

3

Optimizing wrapper classes such as std::function is harder for the compiler to optimize. It require potentially heap elision and mechanism only available on some particular implementation.

Basically, std::function is much more opaque than a template parameter, since the template parameter resolve directly to the right type.

My recommendation would be to avoid using std::function in function parameter. Although sometimes it's unavoidable, for example when exposing virtual function.

Where std::function really shine is as a storage. This is usually where the type of a callable is unknown, whereas it is almost always known in function parameter.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Heap elision is if you treat is as user code and not an intrinsic. Viewing `std::function` as a builtin/intrinsic would make sense, IMHO – curiousguy Mar 29 '19 at 19:16
2

The good

First, as you have remarked, the global optimizer generates the same completely inlined code for both variants:

    mov     DWORD PTR [rsp-8], 2
    mov     eax, DWORD PTR [rsp-8]
    imul    eax, eax
    mov     DWORD PTR [rsp-4], eax

In the templated version you need nothing more, since nothing else can be invoked from another compilation unit.

The bad ;-)

Now your functional version of o2() has external linkage. So it could be called from another translation unit if you would link it with one that would refer to it. This is why the callable code for the function is generated as well under the label o2(std::function<int (int)>, int) and is kept, despite it is not needed by the "inlined" code.

And the ugly

There is still some other additional code in the functional variant, which is less obvious. Looking more closely, I guess that the used implementation of std::function instantiates some classes (probably using CRTP) that expose the lambda as an invokeable class:

  • a member function that implements the lambda in a way to let it be called as a member function (typical approach for creating a callable object), under the label std::_Function_handler<int (int), main::{lambda(int)#1}>::_M_invoke(std::_Any_data const&, int&&):
  • a member function that implements some boiler-plate code to call the previous one under the label std::_Function_base::_Base_manager<main::{lambda(int)#1}>::_M_manager(std::_Any_data&, std::_Function_base::_Base_manager<main::{lambda(int)#1}> const&, std::_Manager_operation):
  • some typeinfo and a virtual table. This shows that one of these classes at least is polymorphic.
  • some exception throwing code (since function can throw)

I suppose that at least one of these classes has external linkage, requiring the compiler to keep the code of the member functions in case it could be referred to from another translation unit.

Christophe
  • 68,716
  • 7
  • 72
  • 138
  • Thank you. That was a great explanation. I got rid of `o2()` by marking it static. Is this a compiler explorer quirk? Is it turning the code into a lib? I used function pointer type and `std::function` stuff went away. – Mochan Mar 29 '19 at 21:14
  • 1
    @Mochan Making it static removes external linkage. So the compiler can determine in the compilation unit if it’s needed or not. No this is not compiler explorer: this is the consequence of the standard. – Christophe Mar 29 '19 at 21:18
0

This did the trick for me.

typedef int (*func_t)(int);

static int o2(func_t f, int x)
{
    return f(x);
}

auto f2 = [](int x) -> int { return x*x; };

int x = 2;
int y = o2(f2, x);

Making o2 static removed it. Does compiler explorer make a lib? I'm not sure what is going on here.

std::function was creating some sort of manager. When I used function pointer type, it also went away.

Mochan
  • 1,329
  • 1
  • 13
  • 27
  • 1
    When the compiler compiles a "translation unit" (a cpp file), it creates an object file with external symbols that can be linked with other object files to create an exe. Or to create a lib. So it's indeed similar principles. – Christophe Mar 29 '19 at 21:42
  • If I given `-flto`, compiler explorer just says `` So, I'll test it out with `perf` and see what happens. – Mochan Mar 29 '19 at 22:13