Historique.
Le package sync/atomic fournit des primitives sans verrou qui se compilent en instructions matérielles. Lorsque Go a été porté sur des systèmes 32 bits (x86-32, ARM32), le runtime a rencontré des processeurs qui n'ont pas de support natif pour l'accès atomique de 64 bits non aligné. Les premières versions permettaient un alignement arbitraire, provoquant des erreurs de bus ou une corruption silencieuse des données. Pour garantir la portabilité, l'équipe de Go a imposé que l'adresse de toute valeur de 64 bits utilisée par les fonctions atomic doit être alignée sur 8 octets sur les architectures 32 bits.
Problème.
Si un programmeur passe un pointeur vers un int64 qui n'est pas aligné sur une limite de 8 octets—par exemple, un champ à un décalage de 4 à l'intérieur d'une structure—l'opération atomique le détecte à l'exécution. Sur les constructions 32 bits, le runtime termine immédiatement le programme avec l'erreur : opération atomique de 64 bits non alignée. Cet échec brutal empêche des lectures ou écritures déchirées qui violeraient les garanties d'atomicité.
Solution.
Le compilateur Go aligne automatiquement les champs de la structure selon leur taille naturelle, mais les développeurs doivent toujours ordonner correctement les champs : placer les champs int64 au début de la structure ou s'assurer qu'ils suivent d'autres types de 8 octets. Alternativement, utilisez atomic.Int64 (disponible depuis Go 1.19), qui encapsule la valeur et garantit l'alignement via le système de types. Pour les variables globales, l'éditeur de liens garantit un alignement approprié.
type Metrics struct { // sum est placé en premier pour garantir un alignement sur 8 octets sur 32 bits. sum int64 count int32 } func (m *Metrics) Add(v int64) { // Sûr sur les architectures 32 bits et 64 bits. atomic.AddInt64(&m.sum, v) }
Scénario.
Un service passerelle IoT fonctionnant sur un ARM Cortex-A7 32 bits a collecté des télémetries. La structure initiale plaçait un DeviceID 32 bits avant un EnergyCounter 64 bits. Des goroutines à haut débit appelaient atomic.AddInt64(&device.EnergyCounter, delta). Immédiatement après le déploiement, le service s'est écrasé avec erreur d'exécution : opération atomique de 64 bits non alignée car EnergyCounter se trouvait à un décalage de 4.
Solutions considérées.
Réorganiser les champs de la structure.
Déplacer les champs int64 en haut de la structure assure un alignement à 0 d'offset. Cette approche ne consomme aucun mémoire supplémentaire et suit la mise en page idiomatique « grands champs d'abord ». Le désavantage est une légère perte de regroupement logique, car DeviceID n'apparaîtrait plus en premier dans le code source.
Insérer un remplissage explicite.
Ajouter un champ de pad int32 de 4 octets avant EnergyCounter force le bon alignement. Cette méthode est explicite et auto-documentée mais gaspille 4 octets par structure. Pour des millions d'enregistrements par appareil, ce surcoût est devenu non négligeable pour le stockage flash embarqué.
Adopter atomic.Int64.
Refactoriser le champ en type d'emballage atomic.Int64 élimine les préoccupations d'alignement car le type lui-même comportait une exigence d'alignement sur 8 octets. Cependant, cela nécessitait de refactoriser chaque site d'appel de atomic.AddInt64(&d.EnergyCounter, v) à d.EnergyCounter.Add(v), introduisant un risque de régressions dans des chemins de code non testés.
Solution choisie.
L'équipe a sélectionné la réorganisation des champs (Solution 1). En plaçant tous les compteurs de 64 bits en haut de la structure, ils ont atteint l'alignement sans surcharge mémoire ni changements d'API. Cela adhère au proverbe Go : « Placez des champs plus grands avant des plus petits. » Ils ont ajouté le linter fieldalignment au CI pour éviter de futures régressions.
Résultat.
Le panic a disparu sur l'ensemble de la flotte ARM32. Le service a fonctionné pendant deux ans sans écrasements liés à l'atomique, et l'optimisation de la mise en page de la structure a réduit l'empreinte mémoire de 8 % grâce à un meilleur emballage des champs restants.
Pourquoi atomic.LoadInt64 réussit-il sur des adresses non alignées sur des architectures 64 bits mais panic sur 32 bits ?
Sur les architectures 64 bits (amd64, arm64), l'unité de gestion de mémoire matérielle prend en charge l'accès non aligné aux valeurs de 64 bits, bien que cela puisse entraîner une pénalité de performance. Les instructions atomiques (par exemple, MOVQ sur x86-64) ne se bloquent pas sur des données non alignées. À l'inverse, les architectures 32 bits utilisent des registres de 32 bits jumelés ou des instructions atomiques spécifiques à 64 bits (comme LDREXD/STREXD sur ARM32) qui nécessitent un alignement sur 8 octets ; sinon, elles provoquent un défaut d'alignement matériel, que le runtime Go traduit en l'erreur fatale « opération atomique de 64 bits non alignée ».
Comment l'inclusion de atomic.Int64 dans une structure définie par l'utilisateur garantit-elle l'alignement sur des systèmes 32 bits sans remplissage manuel ?
Le type atomic.Int64 est défini comme une structure contenant un int64. Le compilateur Go attribue une exigence d'alignement à une structure égale à l'alignement maximum de ses champs. Puisque int64 nécessite un alignement sur 8 octets, atomic.Int64 hérite de cette exigence. Lorsqu'il est intégré comme champ, le compilateur insère des octets de remplissage précédents si nécessaire pour garantir que l'offset du champ soit un multiple de 8. De plus, les allocations sur le tas arrondissent la taille à l'alignement du type, donc un pointeur vers le champ intégré est toujours aligné sur 8 octets.
Pourquoi convertir un []byte en []int64 par un casting unsafe peut-il entraîner des pannes d'alignement sur des architectures 32 bits, même si la longueur du slice est suffisante ?
Un []byte est soutenu par un tableau d'octets. L'adresse de base de ce tableau est garantie d'être alignée pour un accès par octet (alignement de 1 octet), mais pas nécessairement pour un accès par 8 octets. Lorsqu'on utilise unsafe pour convertir le pointeur en *int64 ou redimensionner en []int64, le premier élément peut se trouver à une adresse comme 0x1001, qui n'est pas divisible par 8. Passer &int64Slice[0] à atomic.LoadInt64 déclenche alors la vérification d'alignement. Une conversion sûre nécessite de garantir que le slice de bytes d'origine est alloué à partir d'une source alignée (par exemple, via make([]int64, ...) et casting en []byte pour écriture), ou d'utiliser copy vers un tampon aligné.