Sur un processeur moderne, vous pourriez utiliser la méthode suivante pour trier de grands tableaux et ne voir aucune différence de vitesse :
void swap (int *a, int *b)
{
for (int i = 1 ; i ; i <<= 1)
{
if ((*a & i) != (*b & i))
{
*a ^= i;
*b ^= i;
}
}
}
La partie vraiment importante de votre question est le "pourquoi". Si l'on revient 20 ans en arrière, à l'époque du 8086, ce qui précède aurait été un véritable tueur de performances, mais sur le dernier Pentium, il serait aussi rapide que les deux que vous avez affichés.
La raison est purement liée à la mémoire et n'a rien à voir avec le CPU.
La vitesse des processeurs par rapport à celle de la mémoire a augmenté de façon astronomique. L'accès à la mémoire est devenu le principal goulot d'étranglement des performances des applications. Tous les algorithmes de swap passent la plupart de leur temps à attendre que les données soient extraites de la mémoire. Les systèmes d'exploitation modernes peuvent avoir jusqu'à 5 niveaux de mémoire :
- Cache de niveau 1 - fonctionne à la même vitesse que l'UC, a un temps d'accès négligeable, mais est de petite taille.
- Cache de niveau 2 - fonctionne un peu plus lentement que L1 mais est plus grand et a une plus grande surcharge pour l'accès (généralement, les données doivent être déplacées vers L1 d'abord).
- Cache de niveau 3 - (pas toujours présent) Souvent externe au CPU, plus lent et plus grand que le L2.
- RAM - la mémoire principale du système, qui met généralement en œuvre un pipeline de sorte qu'il y a une latence dans les demandes de lecture (le CPU demande des données, le message est envoyé à la RAM, la RAM reçoit les données, la RAM envoie les données au CPU).
- Disque dur - lorsqu'il n'y a pas assez de RAM, les données sont transférées sur le disque dur, ce qui est très lent et n'est pas vraiment contrôlé par le CPU.
Les algorithmes de tri détériorent l'accès à la mémoire, car ils accèdent généralement à la mémoire de manière très désordonnée, ce qui entraîne des frais généraux inefficaces liés à l'extraction de données de L2, de la RAM ou du disque dur.
Ainsi, l'optimisation de la méthode d'échange est vraiment inutile - si elle n'est appelée que quelques fois, toute inefficacité est cachée en raison du petit nombre d'appels, si elle est appelée souvent, toute inefficacité est cachée en raison du nombre d'absences de cache (où le CPU doit obtenir des données de L2 (quelques cycles), L3 (quelques dizaines de cycles), RAM (quelques centaines de cycles), HD ( !)).
Ce que vous devez vraiment faire, c'est examiner l'algorithme qui appelle la méthode swap. Ce n'est pas un exercice trivial. Bien que la notation Big-O soit utile, un O(n) peut être significativement plus rapide qu'un O(log n) pour un petit n. (Je suis sûr qu'il y a un article de CodingHorror à ce sujet.) De plus, de nombreux algorithmes ont des cas dégénérés où le code en fait plus que nécessaire (utiliser qsort sur des données presque ordonnées pourrait être plus lent qu'un tri à bulles avec un contrôle de sortie précoce). Vous devez donc analyser votre algorithme et les données qu'il utilise.
Ce qui nous amène à la façon d'analyser le code. Les profileurs sont utiles mais vous devez savoir comment interpréter les résultats. N'utilisez jamais une seule exécution pour rassembler les résultats, faites toujours la moyenne des résultats sur plusieurs exécutions - car votre application de test pourrait avoir été paginée sur le disque dur par le système d'exploitation à mi-chemin. Établissez toujours le profil des versions optimisées, le profilage du code de débogage est inutile.
Quant à la question initiale - lequel est le plus rapide ? - c'est comme essayer de déterminer si une Ferrari est plus rapide qu'une Lambourgini en regardant la taille et la forme du miroir d'aile.
2 votes
XOR est plus lent. Utilisez godbolt pour vérifier le nombre d'instructions assembleur pour les deux fonctions. Note que si vous utilisez la méthode XOR sur les valeurs au lieu des valeurs stockées sous le pointeur, la vitesse est la même (au moins pour le compilateur GCC).
1 votes
godbolt.org/z/nqVb9q
3 votes
Il semble que le premier utilise un registre supplémentaire C'est un peu tard, mais pourquoi quelqu'un penserait-il cela ? La croyance selon laquelle l'échange de bits est plus rapide que l'utilisation d'une variable temporaire ignore la réalité du fonctionnement de la plupart des ordinateurs, avec des processeurs et une mémoire séparés. Un swap utilisant une variable temporaire est probablement implémenté comme "charger A dans le registre 1, charger B dans le registre 2, sauvegarder le registre 1 en B, sauvegarder le registre 2 en A". "Charger les deux variables dans les registres, manipuler quelques bits, puis effectuer deux opérations de sauvegarde" est plus lent. Vous devez charger les deux et sauvegarder les deux, la manipulation des bits en cours de route est superflue. .