Optionale V-Tables

Microsoft führte vor langer Zeit mit __declspec(novtable) eine interessante Erweiterung in den MSVC ein, um dem Problem der aufgeblasenen COM DLLs zu begegnen.

Andere Compiler kennen und brauchen das nicht …
… außer man baut sich seine eigenen COM-artigen Objekte und deren V-Tables zusammen.


Selbst bei bester Optimierung fallen mir in den .map Dateien von Builds viele Funktionen auf, die in einem statische Programm nicht enthalten sein dürften.
Bei genauerem Hinsehen entpuppten sich Aufrufe dieser Funktionen als Teil von C-V-Table-Objekt-Methoden, die selbst nie benutzt wurden.

Besonders in watcom unter DOS ist das lästig, wenn mehrere Kilobytes an Code in der EXE landen, die gar nicht gebraucht werden.

V-Table Initialisierung

C Objekte sahen bei mir (ganz ähnlich zu COM) etwa so aus:

 1static int method_1_impl(void* self, int param);
 2static int method_2_impl(void* self, int param1, int param2);
 3
 4typedef struct 
 5{
 6  int (*method_1)(void* self, int param);
 7  int (*method_2)(void* self, int param1, int param2);
 8
 9} my_vtbl_t;
10
11static my_vtbl_t const my_vtbl = 
12{
13  &method_1_impl,
14  &method_2_impl
15}; /* init V-Table in global scope */
16
17typedef struct 
18{
19  my_vtbl_t const* vtbl;
20
21  int private_member_1;
22  int private_member_2;
23
24} my_object_t;
25
26my_object_t* my_object_constructor()
27{
28  my_object_t* obj = malloc(sizeof(my_object_t));
29
30  obj->vtbl = &my_vtbl;
31  obj->private_member_1 = 0;
32  obj->private_member_2 = 0;
33
34  return obj;
35}

Nachdem die statische my_vtbl Variable mit den beiden Methodenimplementierungen method_1_impl und method_2_impl initialisiert wurde, ist deren Code und Aufruf-Baum vollständig im Programm integriert, auch wenn my_object_constructor nicht aufgerufen wird.

Dieses Problem löste ich nun über einen neuen Ansatz der Initialisierung.

V-Table Konstruktor-Funktion

Anstatt die V-Table statisch initialisieren zu lassen, wird im Konstruktor geprüft, ob der anfangs ausgenullte Speicher schon befüllt ist.
Falls nicht, wird die V-Table auf dem Stack erzeugt und dann in die globale V-Table kopiert:

 1static int method_1_impl(void* self, int param);
 2static int method_2_impl(void* self, int param1, int param2);
 3
 4typedef struct 
 5{
 6  int (*method_1)(void* self, int param);
 7  int (*method_2)(void* self, int param1, int param2);
 8
 9} my_vtbl_t;
10
11static my_vtbl_t const my_vtbl = 
12{
13  &method_1_impl,
14  &method_2_impl
15};
16
17static void init_my_vtbl()
18{
19  if(my_vtbl.method_1 == NULL)
20  {
21    my_vtbl_t local_vtbl = 
22    {
23      &method_1_impl,
24      &method_2_impl
25    };
26    my_vtbl = local_vtbl;
27  }
28}
29
30typedef struct 
31{
32  my_vtbl_t const* vtbl;
33
34  int private_member_1;
35  int private_member_2;
36
37} my_object_t;
38
39my_object_t* my_object_constructor()
40{
41  my_object_t* obj = malloc(sizeof(my_object_t));
42
43  /* init V-Table in first constructor call: */
44  init_my_vtbl(); 
45  obj->vtbl = &my_vtbl;
46  obj->private_member_1 = 0;
47  obj->private_member_2 = 0;
48
49  return obj;
50}

Da die Verknüpfung der V-Table mit den Methoden jetzt in einer Funktion stattfindet und nicht mehr statisch auf globaler Ebene, schaffen es nun alle Compiler, den ganzen Aufrufbaum wegzuoptimieren, wenn my_object_constructor() nie aufgerufen wird.

Optimierungspotential

V-Tables kommen im GATE Projekt vor allem bei Streams und “Runnables” für Threads vor. Jeder Kompressionsalgorithmus ist als Encoder-/Decoder-Stream verfügbar und das bedeutet, dass automatisch immer alle einkompiliert werden, sobald gegen die gateencode Bibliothek gelinkt wird.

Tatsächlich hat sich hier aber keine große Optimierung ergeben, da ich meist immer alle Formen von Features einer Bibliothek nutze. ZLIB, BZIP2 und LZMA treten immer gemeinsam auf, oder gar nicht.

Doch die kleineren Helferchen wie stringstream, memorystream und nullstream fallen jetzt sofort weg, wenn sie nicht benutzt werden und das führte bei DOS-Programmen gleich zu einer Verringerung um 5 bis 10 KByte.

Hätte ich SSL-Streams bisher “optional” im Code drinnen gehabt, würde die Codereduktion im Megabytes-Bereich liegen.

GCC Negativ-Optimierung

Tatsächlich sind mir auch ein paar “Negativ-Optimierungen” mit meiner neuen Initialisierungsstrategie aufgefallen. Denn in einigen Formationen hat der GCC offenbar die V-Tables schon vorher perfekt wegoptimiert.
Mit meiner Umstellung kam also bei den benutzten Objekten jetzt eine Initialisierungsfunktion hinzu, die größer war als die alte statische Initialisierung.

Während also WATCOM und MSVC Binaries zwischen 2 und 10 KB kleiner wurden, wurden einige Outputbinaries des GCC um 1 KByte größer.

Doch da hier der Kosten/Nutzeneffekt bei der DOS Plattform groß und bei Linux vernachlässigbar war, bin ich bei den neuen Funktionen geblieben.

Fazit

Tja … Microsoft hat schon vor über 20 Jahren das V-Table Problem zumindest im C++ Codegenerator angegangen.
Heute komme ich also im C-Nachbau auch dorthin.

Es ist schon spannend zu lernen, wie und vor allem wann Compiler ihre Optimierungen anwenden können und wann nicht.

Ich überlege noch, ob man die Initialisierung mit Makros “besser” abdecken kann, aber vorerst bleibe ich bei der direkten Umsetzung.
Der V-Table Code wird schließlich nur einmal geschrieben bzw. heute eben genau einmal angepasst.