Optionale V-Tables
« | 07 May 2023 | »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.