Speicherkopierroutinen können weitaus komplizierter und schneller sein als eine einfache Speicherkopie über Zeiger wie:
void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
for (int i = 0; i < bytes; ++i)
*b_dst++ = *b_src++;
}
Verbesserungen
Die erste Verbesserung, die man vornehmen kann, besteht darin, einen der Zeiger an einer Wortgrenze auszurichten (mit Wort meine ich native Ganzzahlgröße, normalerweise 32 Bit / 4 Byte, kann aber bei neueren Architekturen 64 Bit / 8 Byte betragen) und eine Bewegung in Wortgröße verwenden Anweisungen kopieren. Dies erfordert die Verwendung einer Byte-zu-Byte-Kopie, bis ein Zeiger ausgerichtet ist.
void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
unsigned char* b_dst = (unsigned char*)dst;
unsigned char* b_src = (unsigned char*)src;
// Copy bytes to align source pointer
while ((b_src & 0x3) != 0)
{
*b_dst++ = *b_src++;
bytes--;
}
unsigned int* w_dst = (unsigned int*)b_dst;
unsigned int* w_src = (unsigned int*)b_src;
while (bytes >= 4)
{
*w_dst++ = *w_src++;
bytes -= 4;
}
// Copy trailing bytes
if (bytes > 0)
{
b_dst = (unsigned char*)w_dst;
b_src = (unsigned char*)w_src;
while (bytes > 0)
{
*b_dst++ = *b_src++;
bytes--;
}
}
}
Unterschiedliche Architekturen funktionieren unterschiedlich, je nachdem, ob der Quell- oder der Zielzeiger entsprechend ausgerichtet sind. Zum Beispiel auf einem XScale-Prozessor habe ich eine bessere Leistung erzielt, indem ich den Zielzeiger anstelle des Quellzeigers ausgerichtet habe.
Um die Leistung weiter zu verbessern, kann ein gewisses Abrollen der Schleife durchgeführt werden, so dass mehr Register des Prozessors mit Daten geladen werden. Dies bedeutet, dass die Lade- / Speicherbefehle verschachtelt werden können und ihre Latenz durch zusätzliche Anweisungen (wie Schleifenzählen usw.) verborgen bleibt. Der damit verbundene Vorteil variiert je nach Prozessor erheblich, da die Latenzen für Lade- / Speicherbefehle sehr unterschiedlich sein können.
Zu diesem Zeitpunkt wird der Code eher in Assembly als in C (oder C ++) geschrieben, da Sie die Lade- und Speicheranweisungen manuell platzieren müssen, um den maximalen Nutzen aus dem Ausblenden und dem Durchsatz der Latenz zu ziehen.
Im Allgemeinen sollte eine ganze Cache-Datenzeile in einer Iteration der nicht gerollten Schleife kopiert werden.
Das bringt mich zur nächsten Verbesserung, indem ich Pre-Fetching hinzufüge. Dies sind spezielle Anweisungen, die das Cache-System des Prozessors anweisen, bestimmte Teile des Speichers in seinen Cache zu laden. Da es eine Verzögerung zwischen der Ausgabe der Anweisung und dem Füllen der Cache-Zeile gibt, müssen die Anweisungen so platziert werden, dass die Daten verfügbar sind, wenn sie gerade kopiert werden sollen, und nicht früher / später.
Dies bedeutet, dass Prefetch-Anweisungen sowohl zu Beginn der Funktion als auch innerhalb der Hauptkopierschleife eingefügt werden. Mit den Prefetch-Anweisungen in der Mitte der Kopierschleife werden Daten abgerufen, die in mehreren Iterationen kopiert werden.
Ich kann mich nicht erinnern, aber es kann auch nützlich sein, sowohl die Zieladressen als auch die Quelladressen vorab abzurufen.
Faktoren
Die Hauptfaktoren, die beeinflussen, wie schnell Speicher kopiert werden kann, sind:
- Die Latenz zwischen dem Prozessor, seinen Caches und dem Hauptspeicher.
- Die Größe und Struktur der Cache-Zeilen des Prozessors.
- Die Anweisungen zum Verschieben / Kopieren des Arbeitsspeichers des Prozessors (Latenz, Durchsatz, Registergröße usw.).
Wenn Sie also eine effiziente und schnelle Speicherroutine schreiben möchten, müssen Sie viel über den Prozessor und die Architektur wissen, für die Sie schreiben. Es genügt zu sagen, dass es viel einfacher ist, nur die integrierten Speicherkopierroutinen zu verwenden, wenn Sie nicht auf einer eingebetteten Plattform schreiben.