Heim > Backend-Entwicklung > Golang > Optimierung der Speichernutzung in Go: Beherrschung der Datenstrukturausrichtung

Optimierung der Speichernutzung in Go: Beherrschung der Datenstrukturausrichtung

Barbara Streisand
Freigeben: 2024-11-16 09:54:03
Original
493 Leute haben es durchsucht

Speicheroptimierung ist entscheidend für das Schreiben leistungsfähiger Softwaresysteme. Wenn eine Software über eine begrenzte Speichermenge verfügt, können viele Probleme auftreten, wenn dieser Speicher nicht effizient genutzt wird. Aus diesem Grund ist die Speicheroptimierung für eine bessere Gesamtleistung von entscheidender Bedeutung.

Go erbt viele der vorteilhaften Funktionen von C, aber was mir auffällt, ist, dass ein großer Teil der Leute, die es verwenden, nicht die volle Leistungsfähigkeit dieser Sprache kennt. Einer der Gründe dafür kann ein Mangel an Wissen darüber sein, wie es auf einem niedrigen Niveau funktioniert, oder ein Mangel an Erfahrung mit Sprachen wie C oder C . Ich erwähne C und C, weil die Grundlagen von Go weitgehend auf den wunderbaren Funktionen von C/C basieren. Es ist kein Zufall, dass ich ein Interview von Ken Thompson auf der Google I/O 2012 zitiere:

Für mich war der Grund, warum ich von Go begeistert war, der, dass ich ungefähr zur gleichen Zeit, als wir mit Go begannen, den von C 0x vorgeschlagenen Standard gelesen (oder versucht habe, ihn zu lesen), und das war überzeugend ich.

Heute werden wir darüber sprechen, wie wir unser Go-Programm optimieren können und insbesondere darüber, wie es sinnvoll wäre, Strukturen in Go zu verwenden. Lassen Sie uns zunächst sagen, was eine Struktur ist:

Eine Struktur ist ein benutzerdefinierter Datentyp, der zusammengehörige Variablen verschiedener Typen unter einem einzigen Namen gruppiert.

Um vollständig zu verstehen, wo das Problem liegt, erwähnen wir, dass moderne Prozessoren nicht jeweils ein Byte aus dem Speicher lesen. Wie ruft die CPU die Daten oder Anweisungen ab, die im Speicher gespeichert sind?

In der Computerarchitektur ist ein Wort eine Dateneinheit, die ein Prozessor in einem einzigen Vorgang verarbeiten kann – im Allgemeinen die kleinste adressierbare Speichereinheit. Es handelt sich um eine Gruppe von Bits (Binärziffern) fester Größe. Die Wortgröße eines Prozessors bestimmt seine Fähigkeit, Daten effizient zu verarbeiten. Zu den gängigen Wortgrößen gehören 8, 16, 32 und 64 Bit. Einige Computerprozessorarchitekturen unterstützen ein Halbwort, also die Hälfte der Bitanzahl in einem Wort, und ein Doppelwort, also zwei aufeinanderfolgende Wörter.

Heutzutage sind die gängigsten Architekturen 32-Bit und 64-Bit. Wenn Sie über einen 32-Bit-Prozessor verfügen, bedeutet dies, dass dieser auf 4 Bytes gleichzeitig zugreifen kann, was bedeutet, dass die Wortgröße 4 Bytes beträgt. Wenn Sie über einen 64-Bit-Prozessor verfügen, kann dieser auf 8 Bytes gleichzeitig zugreifen, was bedeutet, dass die Wortgröße 8 Bytes beträgt.

Wenn wir die Daten im Speicher speichern, hat jedes 32-Bit-Datenwort eine eindeutige Adresse, wie unten gezeigt.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Abbildung. 1 – Wortadressierbarer Speicher

Wir können die Daten im Speicher lesen und sie mit dem Ladewort-Befehl (lw) in ein Register laden.

Nachdem wir die oben genannte Theorie kennengelernt haben, wollen wir sehen, wie die Praxis aussieht. Um die Fälle mit Strukturdatenstrukturen zu beschreiben, werde ich sie mit C-Sprache demonstrieren. Eine Struktur in C ist ein zusammengesetzter Datentyp, mit dem Sie mehrere Variablen gruppieren und im selben Speicherblock speichern können. Wie bereits erwähnt, hängt der CPU-Zugriff auf die Daten von der jeweiligen Architektur ab. Für jeden Datentyp in C gelten Ausrichtungsanforderungen.

Lassen Sie uns also Folgendes als einfache Struktur haben:

// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;


// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Und nun versuchen Sie, die Größe der folgenden Strukturen zu berechnen:

Größe der Struktur 1 = Größe von (char short int) = 1 2 = 3.

Größe der Struktur 2 = Größe von (double int char) = 8 4 1= 13.

Die tatsächlichen Größen mit einem C-Programm könnten Sie überraschen.

#include <stdio.h>


// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;

// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

int main()
{
    printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t));
    printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t));

    return 0;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ausgabe

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Wie wir sehen können, unterscheidet sich die Größe der Strukturen von den von uns berechneten.

Was ist der Grund dafür?

C und Go verwenden eine Technik namens „Struct Padding“, um sicherzustellen, dass Daten im Speicher ordnungsgemäß ausgerichtet sind, was aufgrund von Hardware- und Architekturbeschränkungen die Leistung erheblich beeinträchtigen kann. Das Auffüllen und Ausrichten der Daten entspricht den Anforderungen der Systemarchitektur, hauptsächlich um die CPU-Zugriffszeiten zu optimieren, indem sichergestellt wird, dass die Datengrenzen mit den Wortgrößen übereinstimmen.

Lassen Sie uns ein Beispiel durchgehen, um zu veranschaulichen, wie Go mit Auffüllung und Ausrichtung umgeht. Betrachten Sie die folgende Struktur:

type Employee struct {
  IsAdmin  bool
  Id       int64
  Age      int32
  Salary   float32
}
Nach dem Login kopieren
Nach dem Login kopieren

Ein bool ist 1 Byte, int64 ist 8 Bytes, int32 ist 4 Bytes und float32 ist 4 Bytes = 17 Bytes (insgesamt).

Lassen Sie uns die Strukturgröße validieren, indem wir das kompilierte Go-Programm untersuchen:

package main

import (
    "fmt"
    "unsafe"
)

type Employee struct {
    IsAdmin bool
    Id      int64
    Age     int32
    Salary  float32
}

func main() {

    var emp Employee

    fmt.Printf("Size of Employee: %d\n", unsafe.Sizeof(emp))
}
Nach dem Login kopieren

Ausgabe

Size of Employee: 24
Nach dem Login kopieren

Die gemeldete Größe beträgt 24 Byte, nicht 17. Diese Diskrepanz ist auf die Speicherausrichtung zurückzuführen. Um zu verstehen, wie die Ausrichtung funktioniert, müssen wir die Struktur untersuchen und den Speicher visualisieren, den sie einnimmt.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Abbildung 2 – Nicht optimiertes Speicherlayout

Die Struktur Employee verbraucht 8*3 = 24 Bytes. Sie sehen jetzt das Problem, es gibt viele leere Lücken im Layout von Employee (diese Lücken, die durch die Ausrichtungsregeln entstehen, werden „Padding“ genannt).

Polsterungsoptimierung und Auswirkungen auf die Leistung

Es ist wichtig zu verstehen, wie sich Speicherausrichtung und -auffüllung auf die Leistung einer Anwendung auswirken können. Insbesondere wirkt sich die Datenausrichtung auf die Anzahl der CPU-Zyklen aus, die für den Zugriff auf Felder innerhalb einer Struktur erforderlich sind. Dieser Einfluss entsteht hauptsächlich durch CPU-Cache-Effekte und nicht durch reine Taktzyklen selbst, da das Cache-Verhalten stark von der Datenlokalität und -ausrichtung innerhalb der Speicherblöcke abhängt.

Moderne CPUs rufen Daten aus dem Speicher in einen schnelleren Zwischenspeicher namens Cache ab, der in Blöcken fester Größe (üblicherweise 64 Byte) organisiert ist. Wenn Daten gut ausgerichtet und innerhalb der gleichen oder weniger Cache-Zeilen lokalisiert sind, kann die CPU aufgrund reduzierter Cache-Ladevorgänge schneller darauf zugreifen.

Berücksichtigen Sie die folgenden Go-Strukturen, um schlechte und optimale Ausrichtung zu veranschaulichen:

// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;


// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Wie sich die Ausrichtung auf die Leistung auswirkt

CPU liest Daten in Wortgröße statt in Bytegröße. Wie ich eingangs beschrieben habe, besteht ein Wort in einem 64-Bit-System aus 8 Bytes, während ein Wort in einem 32-Bit-System aus 4 Bytes besteht. Kurz gesagt: Die CPU liest die Adresse im Vielfachen ihrer Wortgröße. Um die Variable PassportId abzurufen, benötigt unsere CPU zwei Zyklen, um auf die Daten zuzugreifen, statt nur einen. Der erste Zyklus ruft die Speicher 0 bis 7 ab und der nachfolgende Zyklus ruft den Rest ab. Und das ist ineffizient – ​​wir benötigen einen Datenstrukturabgleich. Durch einfaches Abgleichen der Daten stellen Computer sicher, dass die Var-Passport-ID in EINEM CPU-Zyklus abgerufen werden kann.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Abbildung 3 – Vergleich der Speicherzugriffseffizienz

Padding ist der Schlüssel zur Datenausrichtung. Das Auffüllen erfolgt, weil moderne CPUs dafür optimiert sind, Daten an ausgerichteten Adressen aus dem Speicher zu lesen. Durch diese Ausrichtung kann die CPU die Daten in einem einzigen Vorgang lesen.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Abbildung 4 – Einfaches Abgleichen der Daten

Ohne Auffüllung können Daten falsch ausgerichtet sein, was zu mehreren Speicherzugriffen und einer langsameren Leistung führt. Daher verschwendet das Auffüllen möglicherweise etwas Speicher, stellt aber sicher, dass Ihr Programm effizient ausgeführt wird.

Strategien zur Padding-Optimierung

Eine ausgerichtete Struktur verbraucht weniger Speicher, einfach weil sie im Vergleich zu einer fehlausgerichteten Struktur eine bessere Reihenfolge der Strukturfelder aufweist. Aufgrund des Auffüllens ergeben sich aus zwei 13-Byte-Datenstrukturen jeweils 16 Byte und 24 Byte. Daher sparen Sie zusätzlichen Speicher, indem Sie einfach Ihre Strukturfelder neu anordnen.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Abbildung 5 – Feldreihenfolge optimieren

Falsch ausgerichtete Daten können die Leistung verlangsamen, da die CPU möglicherweise mehrere Zyklen benötigt, um auf falsch ausgerichtete Felder zuzugreifen. Umgekehrt minimieren korrekt ausgerichtete Daten die Cache-Zeilenbelastung, was für die Leistung entscheidend ist, insbesondere in Systemen, in denen die Speichergeschwindigkeit einen Engpass darstellt.

Lassen Sie uns einen einfachen Benchmark durchführen, um es zu beweisen:

#include <stdio.h>


// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;

// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

int main()
{
    printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t));
    printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t));

    return 0;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ausgabe

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Wie Sie sehen können, dauert das Durchqueren des Aligned tatsächlich weniger Zeit als sein Gegenstück.

Padding wurde hinzugefügt, um sicherzustellen, dass jedes Strukturfeld entsprechend seinen Anforderungen richtig im Speicher ausgerichtet ist, wie wir zuvor gesehen haben. Aber obwohl es einen effizienten Zugriff ermöglicht, kann die Polsterung auch Platz verschwenden, wenn die Felder nicht richtig angeordnet sind.

Für eine effiziente Speichernutzung, insbesondere in leistungskritischen Anwendungen, ist es wichtig zu verstehen, wie Strukturfelder richtig ausgerichtet werden, um Speicherverschwendung durch Padding zu minimieren. Im Folgenden werde ich ein Beispiel mit einer schlecht ausgerichteten Struktur bereitstellen und dann eine optimierte Version derselben Struktur zeigen.

In einer schlecht ausgerichteten Struktur werden Felder ohne Berücksichtigung ihrer Größe und Ausrichtungsanforderungen angeordnet, was zu zusätzlichem Abstand und erhöhter Speichernutzung führen kann:

// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;


// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Der Gesamtspeicher könnte daher 1 (bool) 7 (padding) 8 (float64) 4 (int32) 4 (padding) 16 (string) = 40 Bytes betragen.

Eine optimierte Struktur ordnet die Felder von der größten zur kleinsten Größe an, wodurch die Notwendigkeit einer zusätzlichen Polsterung deutlich reduziert oder ganz überflüssig wird:

#include <stdio.h>


// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;

// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

int main()
{
    printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t));
    printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t));

    return 0;
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Der Gesamtspeicher würde dann ordentlich 8 (float64) 16 (string) 4 (int32) 1 (bool) 3 (padding) = 32 Bytes umfassen.

Lassen Sie uns das oben Gesagte beweisen:

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ausgabe

type Employee struct {
  IsAdmin  bool
  Id       int64
  Age      int32
  Salary   float32
}
Nach dem Login kopieren
Nach dem Login kopieren

Die Reduzierung der Strukturgröße von 40 Byte auf 32 Byte bedeutet eine Reduzierung der Speichernutzung pro Person-Instanz um 20 %. Dies kann zu erheblichen Einsparungen in Anwendungen führen, in denen viele solcher Instanzen erstellt oder gespeichert werden, wodurch die Cache-Effizienz verbessert und möglicherweise die Anzahl der Cache-Fehler verringert wird.

Abschluss

Die Datenausrichtung ist ein entscheidender Faktor bei der Optimierung der Speichernutzung und der Verbesserung der Systemleistung. Durch die korrekte Anordnung der Strukturdaten wird die Speichernutzung nicht nur effizienter, sondern auch schneller im Hinblick auf die CPU-Lesezeiten, was erheblich zur Gesamtsystemeffizienz beiträgt.

Das obige ist der detaillierte Inhalt vonOptimierung der Speichernutzung in Go: Beherrschung der Datenstrukturausrichtung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage