Continuing my journey to show what 32bit arm instructions devkitARM generates for particular C++ source code snippets, I thought it’s a good idea to compare classes and structs.
It’s not about C++ classes and C structs, but C++ classes and C++ structs!
I always read in several Nintendo DS homebrew related forums, using classes rather than structs in C++ has a dramatic impact on performance and people recommend not to do this.
Is there really a performance problem? Let’s find it out!
The following C++ code was compiled with devkitARM r25 using optimization level -O1, no runtime type information and no exceptions.
What is the difference between a class and struct in C++?
Actually, the only difference is the default access of members:
- Default access of members in a class is private.
- Default access of members in a structure or union is public.
- Default access of a base class is private for classes and public for structures. Unions cannot have base classes.
When you don’t specify any access specifier (public, protected, private), members of a class have private access by default, while members of a struct have public access.
Both, classes and structs, allow to have member functions, virtual member functions, special member functions (ctor, dtor, …), operators, feature inheritance and so on.
Member variable access of a class vs struct
Let’s take a look what 32bit arm instructions are generated for the following C++ code. Since classes and structs are technically the same, the compiler should output identical code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| struct MyStruct
{
int x;
};
void SetMyStruct(MyStruct *p)
{
p->x = 2;
}
class MyClass
{
public:
int x;
};
void SetMyClass(MyClass *p)
{
p->x = 2;
} |
Instructions for SetMyStruct:
1
2
3
4
5
| ; the parameter "p" is stored in r0
_Z11SetMyStructP8MyStruct:
mov r3, #2 ; r3 = 2
str r3, [r0, #0] ; *(int*)&((char*)r0)[0] = r3
bx lr ; return |
Instructions for SetMyClass:
1
2
3
4
5
| ; the parameter "p" is stored in r0
__Z10SetMyClassP7MyClass:
mov r3, #2 ; r3 = 2
str r3, [r0, #0] ; *(int*)&((char*)r0)[0] = r3
bx lr ; return |
The function names went through name mangling, that’s why they look so weird. Name mangling is a technique used to solve various problems caused by the need to resolve unique names for programming entities.
Alright, accessing a member variable of a class or struct produces identical code.
non-virtual member function vs function
Let’s take a look at the generated code for member functions of a class vs passing a struct to a function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| struct MyStruct
{
int x;
};
void InitMyStruct(MyStruct *p)
{
p->x = 2;
}
class MyClass
{
public:
void Init();
int x;
};
void MyClass::Init()
{
this->x = 2;
} |
Instructions for InitMyStruct:
1
2
3
4
5
| ; the parameter "p" is stored in r0
__Z12InitMyStructP8MyStruct:
mov r3, #2 ; r3 = 2
str r3, [r0, #0] ; *(int*)&((char*)r0)[0] = r3
bx lr ; return |
Instructions for MyClass::Init:
1
2
3
4
5
| ; "this" is stored in r0
___ZN7MyClass4InitEv:
mov r3, #2 ; r3 = 2
str r3, [r0, #0] ; *(int*)&((char*)r0)[0] = r3
bx lr ; return |
The member function code is identical to the code of InitMyStruct! Now you should go like “Why is it identical, why has MyClass::Init also a parameter?”
Because the compiler substitudes a hidden parameter to every member function. The parameter is what you know as this keyword. It is passed as first parameter, thus located in register r0 for the arm instruction set.
The same applies to InitMyStruct, it has one parameter that expects a pointer (to a MyStruct object), so it’s the same.
We also see the member function code is not attached to the object as many people claim! Non-virtual member functions are resolved statically. That is, the member function is selected statically (at compile-time) based on the type of the pointer (or reference) to the object.
How a non-virtual member function gets called
Until now, we only analysed what code is generated for the particular functions, but we don’t know how they get called. Let’s take a look at main, where both functions get called.
1
2
3
4
5
6
7
8
9
10
| int main(void)
{
MyStruct myStruct;
InitMyStruct(&myStruct);
MyClass myClass;
myClass.Init();
return 0;
} |
Instructions generated for main:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| main:
; store link-register to stack
str lr, [sp, #-4]!
; move stack pointer by 12 bytes
; 4 bytes for the link-register store above
; 4 bytes for storage of MyStruct object
; 4 bytes for storage of MyClass object
sub sp, sp, #12
; set r0 to start of MyStruct object
add r0, sp, #4
; call InitMyStruct
; "myStruct" pointer is stored r0
bl _Z12InitMyStructP8MyStruct
; set r0 to start of MyClass object
mov r0, sp
; call member function Init of MyClass
; "myClass" pointer (which becomes "this") is stored in r0
bl _ZN7MyClass4InitEv |
The code that calls InitMyStruct and myClass.Init is the same. There is no difference, no difference, no …!
How a virtual member function gets called
I’ll show you what’s going on with virtual member function in an upcoming article, stay tuned for that!
Conclusion
- C++ classes and C++ structs are technically the same.
- There is no performance impact when calling a non-virtual member function over a function with one parameter.
Follow up material
I recommend to read the topics about classes and inheritance at C++ FAQ Lite in case you want to know more details about classes.