Zero Copy Architektur

Warum Daten unnötig zu kopieren immer eine schlechte Idee ist

Mittwoch, 04. Juli 2012

Systemwachstum

Wenn Architekturen »wachsen« kommen immer neue Schichten an Funktionalitäten hinzu mit ihren eigenen Konventionen der Datenrepräsentation. Konvertierungen sind daher unvermeidlich. Transformation von Darstellungen erfordern auch immer das Lesen und Schreiben von Daten aus und in den Speicher.

Speicheroperationen sind mit den heutigen Rechnerarchitekturen im schlimmsten Fall zwischen 2 und 3 Größenordnungen langsamer. Werden von der Architektur keine Wege und Mittel vorgesehen, unnötige Kopiervorgänge und Konvertierungen zu vermeiden, so werden darauf basierende Programme — ausreichendes Wachstum vorausgesetzt — sich mehr und mehr gefühlt »träge» verhalten, weil die Speicherbandbreite zum limitieren Faktor wird.

Mittel und Wege

Zur Vermeidung von unnötigen Konvertierungen ist ein möglichst universelles Datenformat zu erfinden. Dieser Prozess ist, betrachtet man die aktuelle SQL- und Objekt-Schicht »Konvertierungs«-Problematik, noch lange nicht abgeschlossen.

Unnötiges Kopieren lässt sich am besten vermeiden, indem direkt auf der untersten Implementierungsebene Mechanismen angeboten werden. Die Zero-copy Mechanismen der Betriebssysteme sind hier nur rudimentär entwickelt. Anstatt jedoch Log-Buffer zu reservieren und dann per »write« in die Datei zu kopieren, ist es per »memory mapped Files« möglich, den Log-Buffer direkt als Teil der Datei auszuzeichnen.

Obgleich interne Datenstrukturen, ein simpler Stack etwa, mit einem angepassten API dem Zero-copy Rechnung tragen müssen, verhindert eine durchgängige Berücksichtigung dieses Prinzips jedoch das »Trägheitsprinzip« von Software, bei dem die Speicherbandbreite zum limitierenden Faktor wird.

// old API
/* function: push_binarystack
 * Diese Funktion kopiert initialisierte Daten auf den Stack.
 * Zum Initialisieren kommt noch ein Kopiervorgang hinzu. */
int push_binarystack(binarystack_t * stack,
                     size_t size, const uint8_t data[size]) ;

// Datenflussskizze
╭──────────────────────────╮    ╭───────────────────────╮
│ Thread 1                 │    │ binarystack_t stack   │
├──────────────────────────┤    ├───────────────────────┤
 treepos_t  iter ;               stack-top ╮
 ... depth first search ...                │
 push_binarystack(... &iter); ─→      ╭────┴────────────╮
                            copies▸   │ saved treepos_t │
                            data      ╰────┬────────────╯
                                 stack-top ╯ after push

// new API
/* function: push_binarystack
 * Der Pointer auf einen neu reservierten Speicherbereich
 * auf dem Stack wird zurückgegeben. Der Client muss die
 * Daten direkt auf dem Stack initialisieren. */
int push_binarystack(binarystack_t * stack,
                     size_t size, /*out*/uint8_t ** data) ;

// Datenflussskizze
╭──────────────────────────╮    ╭───────────────────────╮
│ Thread 1                 │    │ binarystack_t stack   │
├──────────────────────────┤    ├───────────────────────┤
 treepos_t  * iter ;             stack-top ╮
 push_binarystack(... &iter); ─→      ╭────┴────────────╮
                            ←─returns │ empty memory    │
                              address ╰─∆──┬────────────╯
      *iter ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┘  ╰─ stack-top
 ... depth first search ...                   after push
 ... uses iterator ref ...
Abbildung 1: Zero-copy Stack API