Maison > développement back-end > Golang > le corps du texte

Optimiser l'utilisation de la mémoire dans Go : maîtriser l'alignement de la structure des données

Barbara Streisand
Libérer: 2024-11-16 09:54:03
original
421 Les gens l'ont consulté

L'optimisation de la mémoire est cruciale pour écrire des systèmes logiciels performants. Lorsqu'un logiciel dispose d'une quantité limitée de mémoire avec laquelle travailler, de nombreux problèmes peuvent survenir lorsque cette mémoire n'est pas utilisée efficacement. C'est pourquoi l'optimisation de la mémoire est essentielle pour de meilleures performances globales.

Go hérite de nombreuses fonctionnalités avantageuses du C, mais ce que je remarque c'est qu'une grande partie des gens qui l'utilisent ne connaissent pas toute la puissance de ce langage. L'une des raisons peut être un manque de connaissances sur son fonctionnement à un bas niveau, ou un manque d'expérience avec des langages comme C ou C . Je mentionne C et C parce que les fondations de Go reposent en grande partie sur les merveilleuses fonctionnalités de C/C . Ce n'est pas un hasard si je cite une interview de Ken Thompson lors de Google I/O 2012 :

Pour moi, la raison pour laquelle j'étais enthousiasmé par Go est parce qu'à peu près au même moment où nous commencions sur Go, j'ai lu (ou essayé de lire) la norme proposée par C 0x, et cela a été convaincant pour moi.

Aujourd'hui, nous allons parler de la façon dont nous pouvons optimiser notre programme Go, et plus précisément, de la façon dont il serait bon d'utiliser les structures dans Go. Disons d'abord ce qu'est une structure :

Une structure est un type de données défini par l'utilisateur qui regroupe des variables associées de différents types sous un seul nom.

Pour bien comprendre où réside le problème, nous mentionnerons que les processeurs modernes ne lisent pas 1 octet à la fois dans la mémoire. Comment le CPU récupère les données ou les instructions stockées dans la mémoire ?

Dans l'architecture informatique, un mot est une unité de données qu'un processeur peut gérer en une seule opération - généralement la plus petite unité de mémoire adressable. C'est un groupe de bits de taille fixe (chiffres binaires). La taille des mots d'un processeur détermine sa capacité à gérer efficacement les données. Les tailles de mots courantes incluent 8, 16, 32 et 64 bits. Certaines architectures de processeurs informatiques prennent en charge un demi-mot, soit la moitié du nombre de bits d'un mot, et un double mot, soit deux mots contigus.

De nos jours, les architectures les plus courantes sont 32 bits et 64 bits. Si vous disposez d'un processeur 32 bits, cela signifie qu'il peut accéder à 4 octets à la fois, ce qui signifie que la taille du mot est de 4 octets. Si vous disposez d'un processeur 64 bits, il peut accéder à 8 octets à la fois, ce qui signifie que la taille du mot est de 8 octets.

Lorsque nous stockons les données en mémoire, chaque mot de données de 32 bits a une adresse unique, comme indiqué ci-dessous.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Figure. 1 ‑ Mémoire adressable par mot

Nous pouvons lire les données en mémoire et les charger dans un registre à l'aide de l'instruction load word (lw).

Après avoir connu la théorie ci-dessus, voyons quelle est la pratique. Pour décrire les cas avec structure de données, je ferai une démonstration en langage C. Une structure en C est un type de données composite qui vous permet de regrouper plusieurs variables et de les stocker dans le même bloc de mémoire. Comme nous l'avons dit plus tôt, l'accès au processeur aux données dépend de l'architecture donnée. Chaque type de données en C aura des exigences d'alignement.

Ayons donc la structure simple suivante :

// 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;
Copier après la connexion
Copier après la connexion
Copier après la connexion

Et maintenant essayez de calculer la taille des structures suivantes :

Taille de la structure 1 = Taille de (char short int) = 1 2 = 3.

Taille de la structure 2 = Taille de (double int char) = 8 4 1= 13.

Les tailles réelles utilisant un programme C pourraient vous surprendre.

#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;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Sortie

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16
Copier après la connexion
Copier après la connexion
Copier après la connexion

Comme nous pouvons le constater, la taille des structures est différente de celles que nous avons calculées.

Quelle en est la raison ?

C et Go utilisent une technique connue sous le nom de « struct padding » pour garantir que les données sont correctement alignées en mémoire, ce qui peut affecter considérablement les performances en raison de contraintes matérielles et architecturales. Le remplissage et l'alignement des données sont conformes aux exigences de l'architecture du système, principalement pour optimiser les temps d'accès au processeur en garantissant que les limites des données s'alignent sur la taille des mots.

Passons en revue un exemple pour illustrer comment Go gère le remplissage et l'alignement, considérons la structure suivante :

type Employee struct {
  IsAdmin  bool
  Id       int64
  Age      int32
  Salary   float32
}
Copier après la connexion
Copier après la connexion

Un bool fait 1 octet, int64 fait 8 octets, int32 fait 4 octets et float32 fait 4 octets = 17 octets (total).

Validons la taille de la structure en examinant le programme Go compilé :

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))
}
Copier après la connexion

Sortie

Size of Employee: 24
Copier après la connexion

La taille signalée est de 24 octets et non de 17. Cet écart est dû à l'alignement de la mémoire. Pour comprendre comment fonctionne l'alignement, il faut inspecter la structure et visualiser la mémoire qu'elle occupe.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Figure 2 - Disposition de la mémoire non optimisée

La structure Employee consommera 8*3 = 24 octets. Vous voyez le problème maintenant, il y a beaucoup de trous vides dans la disposition de l'employé (ces espaces créés par les règles d'alignement sont appelés « remplissage »).

Optimisation du rembourrage et impact sur les performances

Comprendre comment l'alignement et le remplissage de la mémoire peuvent affecter les performances d'une application est crucial. Plus précisément, l'alignement des données a un impact sur le nombre de cycles CPU requis pour accéder aux champs d'une structure. Cette influence provient principalement des effets du cache du processeur, plutôt que des cycles d'horloge bruts eux-mêmes, car le comportement du cache dépend fortement de la localisation des données et de leur alignement dans les blocs de mémoire.

Les processeurs modernes récupèrent les données de la mémoire vers un intermédiaire plus rapide appelé cache, organisé en blocs de taille fixe (généralement 64 octets). Lorsque les données sont bien alignées et localisées dans le même nombre de lignes de cache ou moins, le processeur peut y accéder plus rapidement en raison de la réduction des opérations de chargement du cache.

Considérez les structures Go suivantes pour illustrer un alignement médiocre par rapport à un alignement optimal :

// 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;
Copier après la connexion
Copier après la connexion
Copier après la connexion

Comment l'alignement affecte les performances

Le processeur lit les données en taille de mot plutôt qu'en octet. Comme je l'ai décrit au début, un mot dans un système 64 bits fait 8 octets, tandis qu'un mot dans un système 32 bits fait 4 octets. En bref, le processeur lit l'adresse dans un multiple de la taille du mot. Pour récupérer la variable passeportId, notre CPU prend deux cycles pour accéder aux données au lieu d'un. Le premier cycle récupérera la mémoire 0 à 7 et le cycle suivant récupérera le reste. Et cela est inefficace - nous avons besoin d'un alignement de la structure des données. En alignant simplement les données, les ordinateurs garantissent que le var passeportId peut être récupéré en UN CPU cycle.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Figure 3 - Comparaison de l'efficacité de l'accès à la mémoire

Le remplissage est la clé pour parvenir à l'alignement des données. Le remplissage se produit parce que les processeurs modernes sont optimisés pour lire les données de la mémoire à des adresses alignées. Cet alignement permet au CPU de lire les données en une seule opération.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Figure 4 - Aligner simplement les données

Sans remplissage, les données peuvent être mal alignées, entraînant de multiples accès à la mémoire et un ralentissement des performances. Par conséquent, même si le remplissage peut gaspiller de la mémoire, il garantit que votre programme s'exécute efficacement.

Stratégies d'optimisation du remplissage

La structure alignée consomme moins de mémoire simplement parce qu'elle possède un meilleur ordre des champs de structure par rapport à Misaligned. En raison du remplissage, deux structures de données de 13 octets se révèlent respectivement de 16 octets et 24 octets. Par conséquent, vous économisez de la mémoire supplémentaire en réorganisant simplement vos champs de structure.

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

Figure 5 - Optimisation de l'ordre des champs

Des données mal alignées peuvent ralentir les performances, car le processeur peut avoir besoin de plusieurs cycles pour accéder aux champs mal alignés. À l'inverse, des données correctement alignées minimisent la charge des lignes de cache, ce qui est crucial pour les performances, en particulier dans les systèmes où la vitesse de la mémoire constitue un goulot d'étranglement.

Faisons un benchmark simple pour le prouver :

#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;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Sortie

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16
Copier après la connexion
Copier après la connexion
Copier après la connexion

Comme vous pouvez le constater, la traversée de l'Aligné prend en effet moins de temps que son homologue.

Un remplissage est ajouté pour garantir que chaque champ de structure s'aligne correctement en mémoire en fonction de ses besoins, comme nous l'avons vu plus tôt. Mais s’il permet un accès efficace, le rembourrage peut également gaspiller de l’espace si les champs ne sont pas bien ordonnés.

Comprendre comment aligner correctement les champs de structure pour minimiser le gaspillage de mémoire dû au remplissage est important pour une utilisation efficace de la mémoire, en particulier dans les applications critiques en termes de performances. Ci-dessous, je vais fournir un exemple avec une structure mal alignée, puis montrer une version optimisée de la même structure.

Dans une structure mal alignée, les champs sont ordonnés sans tenir compte de leurs tailles et des exigences d'alignement, ce qui peut entraîner un remplissage supplémentaire et une utilisation accrue de la mémoire :

// 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;
Copier après la connexion
Copier après la connexion
Copier après la connexion

La mémoire totale pourrait donc être 1 (bool) 7 (padding) 8 (float64) 4 (int32) 4 (padding) 16 (string) = 40 octets.

Une structure optimisée organise les champs de la plus grande à la plus petite, réduisant ou éliminant considérablement le besoin de remplissage supplémentaire :

#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;
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

La mémoire totale comprendrait alors parfaitement 8 (float64) 16 (string) 4 (int32) 1 (bool) 3 (padding) = 32 octets.

Prouvons ce qui précède :

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16
Copier après la connexion
Copier après la connexion
Copier après la connexion

Sortie

type Employee struct {
  IsAdmin  bool
  Id       int64
  Age      int32
  Salary   float32
}
Copier après la connexion
Copier après la connexion

Réduire la taille de la structure de 40 octets à 32 octets signifie une réduction de 20 % de l'utilisation de la mémoire par instance de Person. Cela peut conduire à des économies considérables dans les applications où de nombreuses instances de ce type sont créées ou stockées, améliorant ainsi l'efficacité du cache et réduisant potentiellement le nombre d'échecs de cache.

Conclusion

L'alignement des données est un facteur essentiel pour optimiser l'utilisation de la mémoire et améliorer les performances du système. En organisant correctement les données de structure, l'utilisation de la mémoire devient non seulement plus efficace, mais également plus rapide en termes de temps de lecture du processeur, contribuant ainsi de manière significative à l'efficacité globale du système.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal