1
votes

Pourquoi memcpy () et d'autres fonctions similaires utilisent l'assembly?

J'ai jeté un coup d'œil aux parties du code derrière memcpy et d'autres fonctions (memset, memmove, ...) et cela semble être beaucoup, et beaucoup de code d'assemblage.

D'autres questions de stackoverflow sur ce sujet mentionnent que cela peut être dû au fait qu'il contient un code différent pour différentes architectures de CPU.

J'ai personnellement écrit mes propres fonctions memcpy / memset avec très peu de lignes de code C ++ et en 1 million d'itérations avec le temps mesuré avec chrono, j'obtiens constamment de meilleurs temps.

La question est donc la suivante: pourquoi les programmeurs n'ont-ils pas simplement écrit le code en C / C ++ et laissé le compilateur l'interpréter et l'optimiser comme il le pense le mieux? Pourquoi tant de code d'assemblage?


16 commentaires

Vous parlez de la mise en œuvre. Veuillez nommer le système d'exploitation et la bibliothèque avec la version.


Cela dépend de la plate-forme. Sur certaines plates-formes, l'assemblage optimisé à la main est meilleur que le code généré par le compilateur pour des fonctions spéciales telles que memcpy , memcmp , strcpy et d'autres fonctions similaires.


Fondamentalement, parce que les processeurs modernes ont des instructions qui ne sont pas reflétées avec précision par la machine abstraite en C ++ (ou C).


J'ai du mal à croire qu'une implémentation triviale de memcpy fonctionne plus rapidement qu'une implémentation personnalisée optimisée et testée par des experts.


Notez qu'il est très difficile d'obtenir des micro-benchmarks corrects. Souvent, un compilateur verra que vous faites un memcpy et vous optimisera simplement pour cela.


J'ai utilisé VS 2019 sur la dernière mise à jour de Windows 10. la fonction provient de string.h


La clé est que les implémentations de bibliothèques standard ne sont pas nécessairement écrites pour être portables. Ils peuvent donc s'appuyer sur des astuces basées sur l'architecture spécifique pour laquelle ils sont implémentés. À moins que le compilateur ne soit programmé pour optimiser avec ces astuces spécifiques, un bon moyen de les implémenter est l'assemblage.


Re «J'obtiens toujours des temps meilleurs»: avez-vous mesuré avec des tampons d'entrée et de sortie alignés? Avez-vous mesuré avec une entrée alignée et une sortie non alignée? Avez-vous mesuré avec une entrée non alignée et une sortie non alignée? Avez-vous mesuré avec une entrée et une sortie non alignées? Avez-vous mesuré diverses combinaisons de décalages par rapport à l'alignement? Avez-vous mesuré des copies courtes? Avez-vous mesuré de longues copies? Avez-vous mesuré sur des machines avec AVX-512? Avez-vous mesuré sur des machines sans AVX-512? Avec AVX-2? D'autres modèles de processeurs?


@NathanOliver tu veux dire qu'il remplacera mon code par un simple appel à memcpy? Je ne pensais pas qu'une telle chose était possible, l'optimisation en changeant un morceau de code en une fonction entièrement différente. Je me suis enregistré à IDA et il n'y a pas eu de changement de ce genre. Également désactivé toutes les optimisations et la différence est encore de quelques millisecondes en faveur du C ++ brut


@EricPostpischil Non, frérot


L'optimisation @Hjkl peut littéralement tout faire tant que le comportement observé (tel que défini par les spécifications du langage) reste inchangé. Si le compilateur voit que ce que vous faites est équivalent à un memcpy , il peut très bien changer votre code pour utiliser memcpy . Voir La règle as-if .


Pour développer les questions de @ EricPostpischil: Avez-vous pensé à activer les optimisations du compilateur? Avez-vous mesuré la variance des mesures? La différence mesurée est-elle significative par rapport à la variance? Êtes-vous sûr de ne rien inclure de plus dans la mesure de manière inégale?


@Hjkl C ++ a la règle as-if. Fondamentalement, tant que l'optimisation fait le même effet observable, le compilateur est autorisé à faire ce qu'il veut. Notez également qu'un benchmark avec des optimisations désactivées n'a généralement pas de sens. Vous ne devriez vraiment comparer que le code optimisé en raison de la règle as-if.


gcc et clang turing une boucle dans un appel à memcpy: godbolt.org/z/2SoKCr


Eh bien, je suppose que si un compilateur faisait cela à mon morceau de code, ce ne serait pas plus rapide puisque je comparerais 2 appels correspondants. Je suppose que cela pourrait être un résultat isolé sur ma machine spécifique avec sa charge actuelle, la version actuelle du système d'exploitation et qu'il pourrait simplement fonctionner bien pire sur un autre ou tous les autres ordinateurs pour autant que je sache.


Je m'attendrais à ce que memcpy et les autres fonctions soient écrites en assemblage pour tirer parti des instructions de processeur spécialisées, en particulier le bloc de lecture et d'écriture de la mémoire.


5 Réponses :


2
votes

Il est techniquement impossible d'écrire memcpy en C ++ et C standard car vous devez vous fier à des constructions non définies. La même chose est vraie pour les autres fonctions de bibliothèque standard; memset et malloc sont deux autres exemples.

Mais ce n'est pas seulement la raison: une implémentation de bibliothèque standard C et C ++ est, de nos jours, si étroitement associée à un compilateur particulier que les rédacteurs de la bibliothèque peuvent prendre toutes sortes de libertés que vous, en tant que consommateur, ne pouvez pas. isupper , toupper , etc. se démarquent comme de bons exemples où un encodage de caractères particulier peut être supposé.

Une autre bonne raison est que l'assemblage artisanal peut être difficile à battre pour la performance.


2 commentaires

Comment l'implémentation de memcpy à l'aide de unsigned char repose-t-elle sur des constructions non définies?


@EricPostpischil: voir stackoverflow.com/questions/62329008/... . (Pour les autres lecteurs, les règles diffèrent pour C.)



2
votes

Ce "Il est inutile de réécrire en assemblage" est un mythe. Une manière plus précise de l'exprimer est que peu de programmeurs ont les compétences nécessaires pour battre le compilateur. Mais ils existent, et en particulier parmi ceux qui développent des compilateurs.


4 commentaires

Je n'ai jamais dit que c'était inutile, je suis d'accord que certains pourraient optimiser leur code mieux qu'un compilateur, ce n'est simplement pas ce que mes résultats indiquent


@Hjkl Oui, mais les gens peuvent penser que c'est inutile. C'est un mythe courant.


@Hjkl Il est extrêmement probable qu'il y ait un problème avec le benchmark que vous avez utilisé que l'implémentation optimisée à la main de memcpy fournie avec votre bibliothèque standard était plus lente que votre brassage maison. Veuillez partager comment vous en êtes arrivé à ces résultats.


Ce n'est certainement pas inutile, cela demande juste beaucoup d'expertise. Et les gens avec ce genre d'expertise sont ceux qui font que l'optimiseur du compilateur fasse des choses incroyables. Battre le compilateur pour l'optimisation est tout un défi, mais pas impossible. Dans mon projet, le coût de développement de routines hautement optimisées prend des mois, comparé à l'écriture de la même routine en C ++ simple qui prend des heures. Cela vaut-il le coût? Oui, parfois.



0
votes

Pourquoi les programmeurs n'ont-ils pas simplement écrit le code en C / C ++

Nous ne pensons pas aux lecteurs. Nous ne savons même pas ce qu'ils ont écrit. Si vous avez besoin d'une réponse faisant autorité, vous devriez demander aux programmeurs qui ont écrit le code.

Mais nous pouvons faire l'hypothèse, qu'ils ont écrit ce qu'ils ont fait parce que c'était rapide, et ont fait la bonne chose.


0 commentaires

1
votes
  1. Le compilateur génère généralement du code inutile (par rapport à l'assemblage écrit à la main) même au niveau d'optimisation complet. Cela gaspille de l'espace mémoire, ce qui n'est pas bon spécialement sur les systèmes embarqués et réduit les performances.

  2. Êtes-vous sûr que vos codes personnalisés sont complets et sans défaut? Je ne pense pas; car lorsque vous écrivez un assembly, vous avez un contrôle total sur tout, mais lorsque vous compilez un code, il est possible que le compilateur génère quelque chose que vous ne voulez pas (et c'est de votre faute, pas du compilateur).

  3. Il est presque impossible pour le compilateur de générer du code aussi complet qu'un assemblage manuscrit et plus petit que lui en même temps.

  4. Comme mentionné dans certains commentaires, cela dépend également de la plate-forme.


0 commentaires

1
votes

Les memcpy et memset ainsi que d'autres fonctions, sont écrits en assembly pour profiter des instructions spécifiques au processeur .

Par exemple, le processeur ARM a une fonction qui peut charger plusieurs registres à partir d'emplacements successifs avec une seule instruction. Il existe également l'instruction de stockage multiple qui stocke plusieurs registres dans des emplacements successifs. L'Intel x86 a des instructions de lecture et d'écriture de bloc.

Le langage d'assemblage permet de copier 4 octets de 8 bits en utilisant un seul registre 32 bits.

Certains processeurs permettent l'exécution conditionnelle des instructions, ce qui facilite le déploiement des boucles.

J'ai écrit des fonctions memcpy et memset optimisées pour différents processeurs. J'ai également passé beaucoup de temps à discuter (discuter) des «meilleures» implémentations C et C ++ avec des compilateurs. Il est un peu difficile d'utiliser C ou C ++ pour essayer de faire en sorte que le compilateur utilise les instructions du processeur que vous souhaitez.


0 commentaires