8

Comparing C style struct with 'member' functions to a C++ class in an attempt to model C++ overheads, I suspected that the following implementation would be roughly equivlent by containing the same number of instructions. I see that the C implementation results in extra instuctions when calling the 'member' functions.

int main()
{
    uint32_t a;
    rectangle_t r;
    
    /* struct */
    r.set(2,3, &r);
    /* asm 
       ldr     r3, [r7, #12]
       adds    r2, r7, #4
       movs    r1, #3
       movs    r0, #2
       blx     r3
    */

    a = r.getArea(&r);
    /* asm 
       ldr     r3, [r7, #16]
       adds    r2, r7, #4
       mov     r0, r2
       blx     r3
       str     r0, [r7, #20]
       movs    r3, #0
    */

    /* Class */
    r.set(2, 3);
    /* asm 
       adds    r3, r7, #4
       movs    r2, #3
       movs    r1, #2
       mov     r0, r3
       bl      rectangle_t::set(unsigned int, unsigned int)
    */
    a = r.getArea();
    /* asm 
       adds    r3, r7, #4
       mov     r0, r3
       bl      rectangle_t::getArea()
       str     r0, [r7, #12]
       movs    r3, #0
    */
}

Why is there an extra ldr instruction when calling a stuct 'member' vs a class member?

Compiler: ARM GCC 12.2.0 (linux)

Declarations:

typedef struct rectangle rectangle_t;
struct rectangle
{
    uint32_t w;
    uint32_t l;
    void (*set)(uint32_t L, uint32_t W, rectangle_t *self);
    uint32_t (*getArea)(rectangle_t *self);    
};
void set(uint32_t L, uint32_t W, rectangle_t *self)
{
    self->w = W;
    self->l = L;
}

uint32_t getArea(rectangle_t *self)
{
    return self->l * self->w;
}

class rectangle_t
{
public:   

    uint32_t w;
    uint32_t l;  
    void set(uint32_t L, uint32_t W);
    uint32_t getArea(); 
};

void rectangle_t::set(uint32_t L, uint32_t W)
{
    w = W;
    l = L;
}

uint32_t rectangle_t::getArea()
{
    return l * w;
}
Buoy
  • 307
  • 1
  • 9
  • 4
    Because for the `struct` the compiler actually loaded the function pointer from memory, while the `class` method is not virtual so it was directly referenced by name. Even if it was virtual if the compiler can prove where it should end up, it will not use an indirect call. – Jester Jul 18 '23 at 23:02
  • 2
    GCC makes quite different asm here, whether you initialize the function pointer to `0` or not. https://godbolt.org/z/nsfGhGehE . IDK why you're using a function pointer at all, though; it makes the `struct` larger so it doesn't get passed in registers, and worse it's an extra level of indirection. In C, use free functions whose names include the struct type so readers know they go with it, like `rectangle_set`. That's what C++ non-virtual member functions effectively do: syntactic sugar for a namespace and passing a `this` pointer. – Peter Cordes Jul 18 '23 at 23:12

3 Answers3

7

In C++ we can declare struct and/or class members of type function, and those functions become accessible given an instance of the class.

However, in C, we cannot have fields of type function, so, what you've made then are function pointers.  As such those fields are true variablesinstance variables at that — that anyone could dynamically assign to, and that's where the overhead comes from (the variable's value has to be consulted unless the compiler can see an optimization; like any field variable, could be different values for different instances of the same structure as well).  Note that you have also not shown initialization for the function pointers — somewhere you'd have to do r.set = set; r.getArea = getArea; for each separate structure instance, otherwise you'd get garbage or zeros depending on the storage used for the structure.

To make the C code "sort of" equivalent, make functions, not function pointers, and, they have to be declared outside of the struct, so, effectively unbundled compared with the C++ same.

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53
  • 2
    Also note the function pointers are non-`static` so they're taking up space in each instance of the struct! Totally horrible for objects that should be small, as well as destroying opportunities for inlining + further optimization. – Peter Cordes Jul 18 '23 at 23:54
4

A very common implementation of C++ methods is based on VMT idea in case of dynamic dispatch (virtual methods). In this case a single extra pointer in needed for any class instance and calls require a VMT lookup... i.e.

foo.bar(x, y);

compiles to what in C would be

(foo.vmt->bar_method)(&foo, x, y);

However if there is no dynamic typing (i.e. the exact type is known at compile time) the there's no reason to have anything inside the instances and the compiler will know what code to call thanks to the type. So

foo.bar(x, y);

translates to

foo_bar(&foo, x, y);

No compiler uses pointer to functions for each single method inside the instance itself (this is something that I've personally done in C for quite a while BEFORE c++ was a thing, and it has some advantages like the ability to patch methods on a specific instance; but it's NOT what C++ compilers translate classes/methods to).

6502
  • 112,025
  • 15
  • 165
  • 265
  • 5
    For readers not very familiar with C++, "dynamic dispatch" means using the `virtual` keyword. x86-64 asm shown in [How do objects work in x86 at the assembly level?](https://stackoverflow.com/q/33556511) for normal vs. `virtual` member-function calls, showing how non-virtual functions can inline. – Peter Cordes Jul 18 '23 at 23:59
1

Simple answer:

Because the two pieces of code are not identical:

In C++, you must distinguish between virtual and a non-virtual methods:

class exampleClass {
    ...
    void exampleNonVirtual(int32_t x);
    virtual void exampleVirtual(int32_t x);
};

In your example, the C++ method is non-virtual while you are simulating a virtual method using C. (However, virtual methods do not work exactly like this, so the code of a virtual C++ call would look different than your C code, too.)

A non-virtual method cannot be overwritten. This means: It does not depend on the object r which method is called in the line r.set(2, 3).

The C equivalent of calling a non-virtual method would be calling the function set() directly rather than using a function pointer:

set(2, 3, &r);
Martin Rosenau
  • 17,897
  • 3
  • 19
  • 38