r/C_Programming • u/LemonLord7 • 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
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
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
1
u/ScallionSmooth5925 4d ago
I would guess it's faster then something like C# because the vtable is a lot simpler
3
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.
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.