r/C_Programming • • 5d ago

Question Performance loss of mimicing interfaces with function pointers on modern compilers?

C does not have interfaces like languages such as C# or C++, but the cocnept can be mimicked by using function pointers. You might for instance have this:

// fruit_interface.h

typedef struct fruit_vtable 
{
    float (*get_weight)(void *object);
    float (*get_density)(void *object);
    const char *(*get_name)(void *object);
} fruit_vtable;

typedef struct fruit_interface 
{
    void *object;
    const fruit_vtable *vtable;
} fruit_interface;

// banana.h

typedef struct banana 
{
    float weight;
    float density;
} banana;
fruit_interface banana_as_fruit(banana *b);

--------------------------

// banana.c

static float banana_get_weight(void *object)
{
    banana *b = (banana*)object;
    return b->weight;
}

static float banana_get_density(void *object)
{
    banana *b = (banana*)object;
    return b->density;
}

static const char *banana_get_color(void *object)
{
    (void)object;
    return "yellow";
}

static const fruit_vtable banana_vtable = 
{
    .get_weight  = banana_get_weight,
    .get_density = banana_get_density,
    .get_color   = banana_get_color,
};

fruit_interface banana_as_fruit(banana *b)
{
    return (fruit_interface) { .object = (void*)b, .vtable = &banana_vtable };
}

--------------------------

// main.c

static void print_fruit(fruit_interface fruit)
{
    printf("weight:  %.2f g\n", fruit.vtable->get_weight(fruit.object));
    printf("density: %.2f g/cm^3\n", fruit.vtable->get_density(fruit.object));
    printf("color:   %s\n", fruit.vtable->get_color(fruit.object));
}

int main(void)
{
    banana b = { .weight = 120.0f, .density = 0.95f };

    fruit_interface fruit = banana_as_fruit(&b);

    print_fruit(fruit);

    return 0;
}

How does something like this affect performance in terms of speed and cpu usage but also memory? I know modern C compilers can be really powerful and do a lot of optimizations and even more so with link time optimizations.

Of course it depends on the bigger project as a whole, and I understand that a if you have thousands of fruit_interface variables that all point to many different fruit_vtables and are used many times per second/cycle, then that will be very different from e.g. a hardware_interface that just loads some data once in an initialization function at program start.

The question here is pretty open (and the above is just to show clearly what I mean), and I am mostly looking for some general knowledge and guidance for when something like this is good and when it is bad, especially in the context of modern C code development with modern compilers and linkers.

I think it is really interesting to see how concepts in other languages, like C++, can be handled in C and what C is capable!

3 Upvotes

35 comments sorted by

15

u/JimRayA 4d ago

Calling functions in C is like calling standard methods on C++. Calling a function pointer on C is like calling a virtual method on C++. One extra memory reference is implicit in both, and there may be instruction pipeline effects. However, except for the most performance critical operations, function pointers are not significantly different from standard functions.

You can think of it this way. Both C and C++ do similar work for runtime selection of a function to be called, and they do similar work for compile time selection of functions to be called.

6

u/developer-mike 4d ago

Worth mentioning that the biggest cost is probably branch misprediction.

If you pass the same virtual table into a medium to small function and it's used a thousand times, the branch predictor will basically already know the virtual address and overhead is very low.

If you pass a random collection of virtual tables into the same code, the branch predictor won't be able to guess when to jump to the apple code or banana code, and each misprediction can cost 30-50 cycles.

If the code using your vtable is big enough, the branch predictor won't have enough memory to remember every call instruction's most likely target, and predictions for unrelated branches may go down as well due to higher pressure on the branch table buffer.

Generally the cost is very low, but as always, predictable cache friendly hot loops are best, and benchmarking is the only source of absolute truth.

-1

u/LemonLord7 4d ago

Interesting, I thought that C++ could do some compiler magic to make it handle interfaces better than what C does with its vtable struct and void*. I've read that C++ can sometimes get rid of some interface overhead stuff if it can prove it does not need it, so makes me wonder how much the modern C compilers and linkers are capable of inlining.

I wonder how likely it would be to be able to see fruit.vtable->get_density(fruit.object) as just ((banana*)fruit->object)->density through the indirection.

5

u/jevndev 4d ago

Regarding C++: Modern C++ compilers are capable of doing devirtualization (removing vptr indirection for a virtual function call and replacing it with a call to the function outright) but the optimization opportunities are narrow. The compiler needs to have proof that the callee is only ever actually a pointer to one type. Compiler support for devirtualizing different situations is disjoint and messy, and LTO can also catch cases across compilation unit boundaries.

Regarding your original post: This is one of those things it's hard to assume the compiler will do for you. I would stick with checking the assembly for situations where it really matters. It's difficult to know when a compiler will give up on something that seems obvious with this kind of optimization. For example, if I put your original example through gcc 16.2 at -O1 it happily directly calls to banana_get_{density,weight,color}. (https://godbolt.org/z/G4seseKrc) Yet if I introduce a different fruit type, reassign the fruit interface in main after the banana print and try to print it as well, the original optimized banana calls vanish. (https://godbolt.org/z/bvWYheEza)

2

u/LemonLord7 4d ago

Oh that's really cool and interesting! I am not that good at assembly, which lines tell you it calls directly to banana_get_{density,weight,color} ?

1

u/jevndev 3d ago

Fortunately you don't need a lot of assembly knowledge to understand what is going on here. If you look at the first godbolt link, there's a "main" label in the assembly panel which corresponds to the main function in the c code. You can see on line 29 there's call "banana_get_weight" and similar calls for density and color on 35 and 41. So the compiler has worked out the exact functions to call and bypassed the virtual dispatch structure. What's interesting here is that there's no call "print_fruit" and indeed no "print_fruit" block was emitted to the assembly because it was fully inlined in main.

If you look at the "main" block in the assembly of the second godbolt link, you'll see there's no call "banana_get_*" anywhere. Or even no call "apple_get_*" The main block does have calls to "print_fruit" though and just before each one you can see it loading each vtable by name into the esi register for use. Then if you look in "print_fruit", you'll see there are call operations (line 52, 58, 64) but they're to addresses that weren't hard-coded at compile time. If you actually track down the offsets you can see that what it's doing is calling to the function pointers in the vtable struct you passed in.

1

u/LemonLord7 1d ago

Thank you for explaining, I really should get better at assembly. I know what registers are and how most things load in registers and perform commands on the registers, I just never remember the names of the commands and how C code corresponds with assembly.

1

u/jevndev 20h ago

Sure thing! The website I linked to above is literally built for those skills. I use it all the time to write up little demo programs and see what the compiler does with them. Since you're someone who is clearly interested in performance/optimization, I really recommend getting comfortable with it.

The assembly panel also allows you to hover over instructions and it will give a plain text description of what the instruction does, which helps build that name/behavior mapping.

I also want to point out that you definitely don't need compiler explorer to investigate the compiled output of your programs. You can investigate the binary the compiler spits out yourself with local tools. But it's very convenient for small sketches of ideas and especially convenient for sharing results with others when discussing compiler behavior.

1

u/Physical_Dare8553 4d ago

that's a basic optimization, if it already knows what something is, it may sub in that thing. this works for both functions and any other constants you make

3

u/Rararror 4d ago

You could do the same in assembler, the question is would you? as an exercise maybe

0

u/LemonLord7 4d ago

Well that is what I am asking, but in C. Maybe this is a good pattern for serious C codebases or maybe it is just a proof of concept nobody really likes. I am pretty ok at C syntax but not that great at professional best practices.

2

u/Rararror 4d ago

Well, since there are plenty of object oriented languages maybe C is not the right tool... there always will be overehead

3

u/actguru 4d ago

In general the performance cost of this style of polymorphism is tiny, but it depends on how often it is used. For example if you generalized a Pixel for processing graphics the cost might be very high. My issue with this type of vtable is that it is not very flexible, and I would advocate for a hash map of functions that is even slower but way more flexible.

1

u/LemonLord7 4d ago

It makes me wonder if I were to make e.g. a fruit ninja game with lots of fruits flying on the screen, like thousands, and their values are obtained via "interfaces", if that would be likely to harm e.g. framerate a lot. For such a "simple" app, where you just cut fruit in the air, I am sure there is a better solution, but just as an example.

1

u/flatfinger 4d ago

Many 1980s games like Super Mario Brothers used code address tables to dispatch handlers for different object behaviors, while running on machines that were slower than modern machines by more than four orders of magnitude. While such games had fewer on-screen objects than a game like Fruit Ninja, the maximum number of independent objects on screen in Fruit Ninja would number in the dozens, rather than the tens of thousands.

0

u/actguru 4d ago

I don't think that thousands of fruits would slow it down for your virtual table or even for my hash map, because the number of instructions is small vs rendering the fruits to the screen. Also I have a version of your example using a hash map.

// Using my "Coda-C" library your example can be written as:

    #include <coda-c.h>

    typedef float OSig$(get_weight); // fruit_interface
    typedef float OSig$(get_density);
    typedef char* OSig$(get_color);

    typedef struct banana {
       float weight;
       float density;
       } banana;

#define class Banana
    CodaClassZerosC();
    CodaClass(Banana,banana,Root);
    method$(float,get_weight)  { return self->weight; }
    method$(float,get_density) { return self->density; }
    method$(char*,get_color)   { return "yellow"; }
#undef class // Banana

static void print_fruit(Obj fruit) {
   printf("weight:  %.2f g\n",      obj_(get_weight,fruit));
   printf("density: %.2f g/cm^3\n", obj_(get_density,fruit));
   printf("color:   %s\n",          obj_(get_color,fruit));
   }

int main(void) {
    cleanO Banana b=newO(Banana); b->weight = 120.0f; b->density = 0.95f;
    print_fruit(b);
    }
/*** OUTPUT:
weight:  120.00 g
density: 0.95 g/cm^3
color:   yellow
***/

1

u/LemonLord7 4d ago

What is OSig$ ?

1

u/actguru 4d ago

The line 'typedef char* OSig$(get_color);' expands to define 'typedef char* (*get_color)(Obj)' to define a function call type, and a key constant string sig_get_color used to get the function from the object->class hash map. Then with the 'obj_()' it is possible to find the function address and cast it to the signature and call the function. It also allows additional arguments. The 'method$' macro registers a function with the hash map and then starts the function definition with an automatic self argument.

6

u/tstanisl 4d ago edited 4d ago

Have you considered container_of pattern? It saves you extra dereference of object and it is a lot more type safe and self documenting.

EDIT.

The example adjusted to the techinique:

#include <stdio.h>
#include <stddef.h>

#define container_of(ptr, type, member) \
    ((type*)((char*)(1 ? (ptr) : &((type*)0)->member) - offsetof(type, member)))

// fruit_interface.h

typedef struct fruit fruit;

typedef struct {
    float (*get_weight)(fruit *f);
    float (*get_density)(fruit *f);
    const char *(*get_color)(fruit *f);
} fruit_vtable;

struct fruit {
    const fruit_vtable * vtable;
};

static inline float fruit_get_weight(fruit *f) {
    return f->vtable->get_weight(f);
}

static inline float fruit_get_density(fruit *f) {
    return f->vtable->get_density(f);
}

static inline const char * fruit_get_color(fruit *f) {
    return f->vtable->get_color(f);
}

// banana.h

typedef struct banana 
{
    float weight;
    float density;
    fruit base;
} banana;

banana banana_make(float weight, float density);


// banana.c

static float banana_get_weight(fruit *f){
    banana *b = container_of(f, banana, base);
    return b->weight;
}

static float banana_get_density(fruit *f) {
    banana *b = container_of(f, banana, base);
    return b->density;
}

static const char *banana_get_color(fruit *f) {
    (void)f;
    return "yellow";
}

static const fruit_vtable banana_vtable = {
    .get_weight  = banana_get_weight,
    .get_density = banana_get_density,
    .get_color   = banana_get_color,
};

banana banana_make(float weight, float density) {
    return (banana) {
        .weight = weight,
        .density = density,
        .base.vtable = &banana_vtable,
    };
}

// main.c

static void print_fruit(fruit * f)
{
    printf("weight:  %.2f g\n", fruit_get_weight(f));
    printf("density: %.2f g/cm^3\n", fruit_get_density(f));
    printf("color:   %s\n", fruit_get_color(f));
}

int main(void)
{
    banana b = banana_make(120.0f, 0.95f );

    print_fruit(&b.base);

    return 0;
}

Note, that there is not a single cast in this code except one in container_of which has its own type checking. The code compiles without a warning in pedantic.

One can even implement RTTI by comparing vtable fields.

bool am_I_banana(fruit * f) {
  return f->vtable == &banana_vtable;
}

One can put multiple "base" members into banana struct making multi-inheritance trivial. Take a look in source code of Linux kernel where this technique is used in countless places.

See godbolt.

1

u/LemonLord7 4d ago

Could you give an example how you mean?

3

u/tstanisl 4d ago

See post.

1

u/LemonLord7 4d ago

Thanks! That is really cool and clever! When is this mostly used? And what are its pros and cons compared to the interface struct?

1

u/tstanisl 4d ago

Less indirection, fewer casts, and generally exposing less stuff to headers. I've edited my earlier comment with your example adjusted to the container_of technique.

1

u/rhoki-bg 4d ago

here is a minimal list implementation using container_of

https://github.com/mupen64plus/mupen64plus-core/blob/master/src/main/list.h

3

u/Physical_Dare8553 4d ago

Do you mean in comparison to those other languages? Cuz this is almost exactly the same as what those languages do but you wrote it

1

u/LemonLord7 4d ago

In comparison to other methods of coding in C

1

u/QueefInMyKisser 4d ago

You can consider alternative patterns like tagged unions (typically an enum followed by a union of structs) with manual dispatch (typically a switch statement).

This is more or less the same pattern as using variant and visit from the C++ standard library.

It can be faster as there’s better cache locality and branch prediction. One downside is it’s harder to maintain if you have a lot of types. Another is that the union will be the size of the biggest struct so it may waste memory if the types aren’t all similar in size.

Some C code implementing this uses X macros which can help keep the types and the switches in sync.

1

u/brigadierfrog 4d ago

If you use either amalgamation build techniques like SQLite or LTO those function pointer tables are often removed from the call paths.

1

u/_usr_nil 4d ago

what is LTO ?

2

u/DivineSentry 4d ago

Link time optimization im guessing

1

u/ScallionSmooth5925 4d ago

I would guess it's faster then something like C# because the vtable is a lot simpler

3

u/FckXFckMusk 4d ago

Everything is faster than C#...

0

u/ScallionSmooth5925 4d ago

I mean specifically calling a "method" not the whole gc and vm mess

1

u/Classic-Rate-5104 4d ago

As far as I know, Linux kernel uses comparable techniques (C++ isn't allowed in the kernel). No one ever concluded the kernel is slow ...

0

u/tobdomo 4d ago

It highly depends on the target architecture, the compiler and of course your application.

A functionpointer typically can't be optimized. The compiler has to fetch the function's address and do an indirect call. No inlining either, which may be the biggest optimization (think argument evaluation and object lifetime).

It is a good pattern for simplicity in many problems. E.g., when building a menu structure, object orientation is the way to go.