logo hsb.horse
← Retour à l’index des snippets

Snippets

Taille mémoire des types de données Go

Liste complète des tailles mémoire pour chaque type de données Go et conseils pratiques pour un code économe en mémoire.

Publié: Mis à jour:

Comprendre l’empreinte mémoire des types de données Go est crucial pour écrire des programmes économes en mémoire. Cela devient particulièrement important lors du traitement de grands ensembles de données, dans les systèmes embarqués ou les applications hautes performances où le choix des structures de données impacte directement les performances.

Taille mémoire par type de données

Type de donnéesTaille (octets)Notes
int / uint8 / 88 octets sur les systèmes 64 bits, 4 octets sur 32 bits
int8 / uint81 / 1
int16 / uint162 / 2
int32 / uint324 / 4
int64 / uint648 / 8
float324Flottant simple précision IEEE 754
float648Flottant double précision IEEE 754
complex648float32 × 2 (parties réelle et imaginaire)
complex12816float64 × 2 (parties réelle et imaginaire)
byte (alias pour uint8)1
rune (alias pour int32)4Point de code Unicode
bool1Utilise 1 octet en interne
string16Pointeur(8) + longueur(8), données stockées séparément
Slice (ex: []int)24Pointeur(8) + longueur(8) + capacité(8)
Map (ex: map[string]int)8Pointeur vers structure interne
Channel (ex: chan int)8Pointeur vers structure interne
Struct (ex: struct{})00 sans champs
Interface16Info type(8) + pointeur vers valeur(8)

※ Les tailles sont des valeurs standard pour les systèmes 64 bits. Sur les systèmes 32 bits, les pointeurs font 4 octets.

Exemples d’implémentation

Vérification des tailles

Vous pouvez utiliser unsafe.Sizeof pour vérifier la taille des types de données à l’exécution.

package main
import (
"fmt"
"unsafe"
)
func main() {
// Types de base
var i int
var i8 int8
var f32 float32
var f64 float64
var b bool
var r rune
fmt.Printf("int: %d bytes\n", unsafe.Sizeof(i))
fmt.Printf("int8: %d bytes\n", unsafe.Sizeof(i8))
fmt.Printf("float32: %d bytes\n", unsafe.Sizeof(f32))
fmt.Printf("float64: %d bytes\n", unsafe.Sizeof(f64))
fmt.Printf("bool: %d bytes\n", unsafe.Sizeof(b))
fmt.Printf("rune: %d bytes\n", unsafe.Sizeof(r))
// String, slice, map
var s string
var slice []int
var m map[string]int
fmt.Printf("string: %d bytes\n", unsafe.Sizeof(s))
fmt.Printf("slice: %d bytes\n", unsafe.Sizeof(slice))
fmt.Printf("map: %d bytes\n", unsafe.Sizeof(m))
// Interface
var iface interface{}
fmt.Printf("interface{}: %d bytes\n", unsafe.Sizeof(iface))
}

Vérification du padding des structures

L’efficacité mémoire des structures varie selon l’ordre des champs. Le padding peut causer une consommation mémoire inattendue.

package main
import (
"fmt"
"unsafe"
)
// Disposition inefficace
type BadLayout struct {
a bool // 1 byte + 7 bytes padding
b int64 // 8 bytes
c bool // 1 byte + 7 bytes padding
d int64 // 8 bytes
}
// Disposition efficace
type GoodLayout struct {
b int64 // 8 bytes
d int64 // 8 bytes
a bool // 1 byte
c bool // 1 byte + 6 bytes padding
}
func main() {
fmt.Printf("BadLayout: %d bytes\n", unsafe.Sizeof(BadLayout{})) // 32 bytes
fmt.Printf("GoodLayout: %d bytes\n", unsafe.Sizeof(GoodLayout{})) // 24 bytes
}

Capacité des slices et allocation mémoire

Gérer correctement la capacité des slices évite les réallocations inutiles.

package main
import (
"fmt"
)
func main() {
// Sans capacité spécifiée, des réallocations peuvent survenir à chaque append
s1 := []int{}
for i := 0; i < 1000; i++ {
s1 = append(s1, i)
}
// Pré-allocation de la capacité évite les réallocations
s2 := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s2 = append(s2, i)
}
fmt.Printf("s1 len: %d, cap: %d\n", len(s1), cap(s1))
fmt.Printf("s2 len: %d, cap: %d\n", len(s2), cap(s2))
}

Pré-allocation des maps

Les maps bénéficient également de la spécification de capacité initiale, réduisant les coûts d’expansion dynamique.

package main
import (
"fmt"
)
func main() {
// Sans capacité spécifiée
m1 := make(map[int]string)
for i := 0; i < 1000; i++ {
m1[i] = fmt.Sprintf("value%d", i)
}
// Avec capacité pré-allouée
m2 := make(map[int]string, 1000)
for i := 0; i < 1000; i++ {
m2[i] = fmt.Sprintf("value%d", i)
}
fmt.Printf("m1 length: %d\n", len(m1))
fmt.Printf("m2 length: %d\n", len(m2))
}

Idées d’application et techniques d’optimisation

1. Optimiser l’ordre des champs de structure

Placer les champs plus grands en premier et les plus petits en dernier minimise le padding.

// Avant optimisation: 32 bytes
type Before struct {
flag1 bool // 1 + 7 padding
num1 int64 // 8
flag2 bool // 1 + 7 padding
num2 int64 // 8
}
// Après optimisation: 24 bytes
type After struct {
num1 int64 // 8
num2 int64 // 8
flag1 bool // 1
flag2 bool // 1 + 6 padding
}

2. Définir une capacité de slice appropriée

Lorsque le nombre final d’éléments est prévisible, spécifiez la capacité avec make pour éviter les réallocations mémoire.

// Lors de l'ajout de 1000 éléments
items := make([]Item, 0, 1000)

3. Choisir entre tableaux et slices

Lorsque le nombre d’éléments est fixe et ne changera pas, les tableaux sont plus économes en mémoire que les slices.

// Slice: 24 bytes (en-tête) + données réelles
s := []int{1, 2, 3}
// Tableau: 24 bytes (int × 3, données uniquement)
a := [3]int{1, 2, 3}

4. Minimiser l’utilisation d’interfaces vides

interface{} consomme 16 octets pour les informations de type et le pointeur. Utilisez des génériques ou des types concrets lorsque le type est connu.

// Utilisant une interface vide (16 bytes)
var v interface{} = 42
// Utilisant un type concret (8 bytes)
var v int = 42

5. Utiliser Builder pour la concaténation de chaînes

Les chaînes sont immuables, donc l’opérateur + alloue de la nouvelle mémoire à chaque fois. Utilisez strings.Builder pour la concaténation en masse.

import "strings"
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("item")
}
result := b.String()

6. Réutiliser les objets avec sync.Pool

Les objets fréquemment créés et détruits peuvent être réutilisés avec sync.Pool pour réduire la pression du garbage collector.

import "sync"
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
// Utilisation
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf)

7. Utiliser des pointeurs pour éviter de copier de grandes structures

Lors du passage de grandes structures aux fonctions, utilisez des pointeurs au lieu du passage par valeur pour réduire les coûts de copie.

// Passage par valeur: toute la structure est copiée
func ProcessValue(data BigStruct) {
// ...
}
// Passage par pointeur: seul le pointeur (8 bytes) est copié
func ProcessPointer(data *BigStruct) {
// ...
}

8. Identifier les zones problématiques avec le profilage mémoire

Utilisez pprof pour visualiser l’utilisation de la mémoire et identifier les zones à optimiser.

import _ "net/http/pprof"
import "net/http"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Logique de l'application
}

Accédez à http://localhost:6060/debug/pprof/heap dans un navigateur ou analysez les profils avec go tool pprof.

Résumé

  • Les types de base ont des tailles explicites; les chaînes, slices, maps et interfaces sont basés sur des pointeurs
  • L’ordre des champs de structure affecte le padding et l’efficacité mémoire
  • Spécifier la capacité initiale pour les slices et maps réduit les coûts de réallocation
  • Passer les grandes structures par pointeur et réutiliser les objets fréquemment créés avec sync.Pool
  • Utiliser pprof pour le profilage mémoire afin d’identifier les opportunités d’optimisation

En appliquant ces connaissances, vous pouvez écrire des programmes Go économes en mémoire.