Oui, l'alignement et la disposition de vos données peuvent faire une grande différence dans les performances, pas seulement quelques pour cent mais quelques à plusieurs centaines de pour cent.
Prenez cette boucle, deux instructions comptent si vous exécutez suffisamment de boucles.
.globl ASMDELAY
ASMDELAY:
subs r0,r0,#1
bne ASMDELAY
bx lr
Avec et sans cache, et avec alignement avec et sans lancer de cache dans la prédiction de branche et vous pouvez faire varier considérablement les performances de ces deux instructions (tics du minuteur):
min max difference
00016DDE 003E025D 003C947F
Un test de performance que vous pouvez très facilement faire vous-même. ajouter ou supprimer des nops autour du code sous test et effectuer un travail de synchronisation précis, déplacer les instructions sous test le long d'une plage d'adresses suffisamment large pour toucher les bords des lignes de cache, etc.
Même chose avec les accès aux données. Certaines architectures se plaignent des accès non alignés (effectuant une lecture 32 bits à l'adresse 0x1001 par exemple), en vous donnant un défaut de données. Certains de ceux que vous pouvez désactiver la faute et prendre le coup de performance. D'autres, qui permettent des accès non alignés, vous obtiennent juste les performances.
Ce sont parfois des "instructions" mais la plupart du temps ce sont des cycles horloge / bus.
Regardez les implémentations memcpy dans gcc pour diverses cibles. Supposons que vous copiez une structure de 0x43 octets, vous pouvez trouver une implémentation qui copie un octet en laissant 0x42, puis copie 0x40 octets en gros morceaux efficaces, puis le dernier 0x2, il peut faire deux octets individuels ou un transfert de 16 bits. L'alignement et la cible entrent en jeu si les adresses source et de destination sont sur le même alignement, par exemple 0x1003 et 0x2003, alors vous pouvez faire un octet, puis 0x40 en gros morceaux puis 0x2, mais si l'un est 0x1002 et l'autre 0x1003, alors il obtient vraiment moche et très lent.
La plupart du temps, ce sont des cycles de bus. Ou pire le nombre de transferts. Prenez un processeur avec un bus de données de 64 bits, comme ARM, et effectuez un transfert de quatre mots (lecture ou écriture, LDM ou STM) à l'adresse 0x1004, c'est-à-dire une adresse alignée sur les mots et parfaitement légale, mais si le bus est 64 bits de large, il est probable que l'instruction unique se transforme en trois transferts dans ce cas, un 32 bits à 0x1004, un 64 bits à 0x1008 et un 32 bits à 0x100A. Mais si vous aviez la même instruction mais à l'adresse 0x1008, il pourrait effectuer un seul transfert de quatre mots à l'adresse 0x1008. Chaque transfert est associé à une heure de configuration. Ainsi, la différence d'adresse 0x1004 à 0x1008 en elle-même peut être plusieurs fois plus rapide, même / esp lors de l'utilisation d'un cache et tous sont des hits de cache.
En parlant de cela, même si vous faites une lecture de deux mots à l'adresse 0x1000 vs 0x0FFC, le 0x0FFC avec des échecs de cache va provoquer deux lectures de ligne de cache où 0x1000 est une ligne de cache, vous avez quand même la pénalité d'une ligne de cache lue pour un hasard accès (lecture de plus de données que l'utilisation) mais cela double. La façon dont vos structures sont alignées ou vos données en général et votre fréquence d'accès à ces données, etc., peuvent entraîner un contournement du cache.
Vous pouvez finir par répartir vos données de telle sorte que lorsque vous traitez les données, vous pouvez créer des expulsions, vous pourriez ne pas avoir de chance et finir par n'utiliser qu'une fraction de votre cache et au fur et à mesure que vous la parcourez, la prochaine goutte de données entre en collision avec une goutte précédente . En mélangeant vos données ou en réorganisant les fonctions dans le code source, etc., vous pouvez créer ou supprimer des collisions, car tous les caches ne sont pas créés égaux, le compilateur ne va pas vous aider ici, c'est sur vous. Même la détection du succès ou de l'amélioration des performances vous appartient.
Toutes les choses que nous avons ajoutées pour améliorer les performances, les bus de données plus larges, les pipelines, les caches, la prédiction de branche, les unités / chemins d'exécution multiples, etc. aideront le plus souvent, mais ils ont tous des points faibles, qui peuvent être exploités intentionnellement ou accidentellement. Il y a très peu de choses que le compilateur ou les bibliothèques peuvent faire à ce sujet, si vous êtes intéressé par les performances que vous devez régler et l'un des plus grands facteurs de réglage est l'alignement du code et des données, pas seulement aligné sur 32, 64, 128, 256 les limites de bits, mais aussi lorsque les choses sont relatives les unes aux autres, vous voulez que les boucles fortement utilisées ou les données réutilisées ne se retrouvent pas de la même manière dans le cache, elles veulent chacune la leur. Les compilateurs peuvent aider, par exemple, à ordonner des instructions pour une architecture super scalaire, à réorganiser des instructions qui n'ont pas d'importance les unes par rapport aux autres,
Le plus gros oubli est l'hypothèse que le processeur est le goulot d'étranglement. Cela n'a pas été vrai depuis une décennie ou plus, l'alimentation du processeur est le problème et c'est là que des problèmes tels que les performances d'alignement, le cache du cache, etc. entrent en jeu. Avec un peu de travail même au niveau du code source, réorganiser les données dans une structure, ordonner les déclarations de variable / struct, ordonner les fonctions dans le code source et un peu de code supplémentaire pour aligner les données, peut améliorer les performances plusieurs fois ou plus.