web-dev-qa-db-fra.com

Codage pour ARM NEON: Comment commencer?

BACKGROUND (sauter ceci si vous aimez)

Permettez-moi de commencer par dire que je ne suis pas un programmeur expert. Je suis un jeune ingénieur débutant en vision par ordinateur (CV) et je possède une assez bonne expérience de la programmation C++, principalement à cause de l'utilisation intensive de la formidable API OpenCV2 C++. Tout ce que j'ai appris, c’est la nécessité de réaliser des projets, de résoudre des problèmes et de respecter les délais, comme c’est la réalité dans l’industrie.

Récemment, nous avons commencé à développer un logiciel de CV pour les systèmes intégrés (cartes ARM), et nous le faisons en utilisant un code optimisé en langage C++ simple. Cependant, construire un système de CV en temps réel dans ce type d'architecture est un défi de taille en raison de ses ressources limitées par rapport aux ordinateurs traditionnels.

C'est quand j'ai trouvé à propos de NEON. J'ai lu de nombreux articles à ce sujet, mais il s'agit d'un thème assez récent. Il y a donc peu d'informations à ce sujet. Plus je lis, plus je suis confus.

La question

Je cherche à optimiser le code C++ (principalement quelques pour les boucles ) en utilisant la capacité NEON de calculer 4 ou 8 éléments de tableau à la fois. Existe-t-il une sorte de bibliothèque ou un ensemble de fonctions pouvant être utilisées dans un environnement C++? La principale source de ma confusion est le fait que presque tous les fragments de code que je vois se trouvent dans Assembly, pour lesquels je n’ai absolument aucune connaissance en arrière-plan et que je ne peux absolument pas me permettre d’apprendre pour le moment. J'utilise Eclipse IDE sous Linux Gentoo pour écrire du code C++.

METTRE À JOUR

Après avoir lu les réponses, j'ai fait des tests avec le logiciel. J'ai compilé mon projet avec les drapeaux suivants:

-O3 -mcpu=cortex-a9 -ftree-vectorize -mfloat-abi=hard -mfpu=neon 

Gardez à l'esprit que ce projet comprend des bibliothèques étendues telles que openframeworks, OpenCV et OpenNI, et que tout a été compilé avec ces indicateurs. Pour compiler pour le tableau ARM, nous utilisons un compilateur croisé de chaînes d'outils Linaro. La version de GCC est la 4.8.3. Vous attendriez-vous à ce que cela améliore la performance du projet? Parce que nous n'avons expérimenté aucun changement, ce qui est plutôt étrange compte tenu de toutes les réponses que j'ai lues ici.

Une autre question: tous les pour les cycles ont un nombre apparent d’itérations, mais beaucoup d’entre eux itèrent à travers des types de données personnalisés (structures ou classes). GCC peut-il optimiser ces cycles même s'ils se répètent dans des types de données personnalisés?

14
Pedro Batista

MODIFIER:

À partir de votre mise à jour, vous risquez de mal comprendre le rôle du processeur NEON. C'est un processeur vectoriel SIMD (Single Instruction, Multiple Data). Cela signifie qu’il est très efficace pour exécuter une instruction (par exemple, "multiplier par 4") sur plusieurs données en même temps. Il aime aussi faire des choses comme "additionner tous ces nombres" ou "additionner chaque élément de ces deux listes de nombres pour créer une troisième liste de nombres". Donc, si votre problème ressemble à cela, le processeur NEON sera une aide précieuse.

Pour obtenir cet avantage, vous devez placer vos données dans des formats très spécifiques afin que le processeur vectoriel puisse charger plusieurs données simultanément, les traiter en parallèle, puis les réécrire simultanément. Vous devez organiser les choses de manière à ce que le calcul évite la plupart des conditions (parce que regarder les résultats trop tôt signifie un aller-retour vers le NEON). La programmation vectorielle est une façon différente de penser à votre programme. Tout est question de gestion de pipeline.

Maintenant, pour de nombreux problèmes très courants, le compilateur peut automatiquement résoudre tout cela. Mais il reste à travailler avec des nombres et des nombres dans des formats particuliers. Par exemple, vous devez presque toujours placer tous vos numéros dans un bloc contigu en mémoire. Si vous traitez avec des champs à l'intérieur de structures et de classes, NEON ne peut pas vraiment vous aider. Ce n'est pas un moteur polyvalent "faire des choses en parallèle". C'est un processeur SIMD pour faire des maths en parallèle.

Pour les systèmes très performants, le format des données est primordial. Vous ne prenez pas de formats de données arbitraires (structures, classes, etc.) et essayez de les rendre rapides. Vous déterminez le format de données qui vous permettra de faire le travail le plus parallèle possible, et vous écrivez votre code autour de cela. Vous rendez vos données contiguës. Vous évitez l'allocation de mémoire à tout prix. Mais ce n'est pas vraiment quelque chose qu'une simple question StackOverflow peut adresser. La programmation de haute performance est un ensemble de compétences et une façon de penser différente. Ce n'est pas quelque chose que vous obtenez en trouvant le bon drapeau de compilateur. Comme vous l'avez constaté, les valeurs par défaut sont déjà très bonnes.

La vraie question que vous devriez vous poser est de savoir si vous pouvez réorganiser vos données pour pouvoir utiliser davantage OpenCV. OpenCV a déjà de nombreuses opérations parallèles optimisées qui feront certainement bon usage du NEON. Autant que possible, vous souhaitez conserver vos données dans le format dans lequel OpenCV fonctionne. C'est probablement là que vous obtiendrez vos améliorations les plus importantes.


D'après mon expérience, il est certainement possible d'écrire à la main NEON Assembly pour battre clang et gcc (du moins il y a quelques années, même si le compilateur continue de s'améliorer). Avoir une excellente optimisation ARM n'est pas la même chose que l'optimisation NEON. Comme @Mats le note, le compilateur fera généralement un excellent travail dans des cas évidents, mais ne gérera pas toujours tous les cas de manière idéale, et il est certainement possible, même pour un développeur peu expérimenté, de le vaincre parfois de façon spectaculaire. (@wallyk a également raison de dire que le réglage manuel de Assembly est mieux sauvegardé pour la fin; il peut néanmoins être très puissant.)

Cela dit, compte tenu de votre déclaration "Assemblée, pour laquelle je n'ai absolument aucune expérience en la matière et que je ne peux absolument pas me permettre d'apprendre à ce stade", alors non, vous ne devriez même pas vous déranger. Sans au moins d'abord comprendre les bases (et quelques notions non essentielles) de Assembly (et plus spécifiquement NEON Assembly vectorisée), il est inutile de deviner le compilateur. La première étape consiste à connaître la cible.

Si vous souhaitez connaître la cible, mon introduction préférée est/ Tour Whirlwind de ARM Assembly . Cela, plus quelques autres références (ci-dessous), était suffisant pour me permettre de battre le compilateur de 2-3 fois dans mes problèmes particuliers. En revanche, ils étaient insuffisants pour que, lorsque j'ai montré mon code à un développeur expérimenté de NEON, il l'ait examiné pendant environ trois secondes et a déclaré "vous vous arrêtez là." Un très bon assemblage est difficile, mais un assemblage à peu près correct peut toujours être meilleur que le C++ optimisé. (Encore une fois, cela devient moins vrai à mesure que les rédacteurs du compilateur s'améliorent, mais cela peut toujours être vrai.)

Une note de côté, mon expérience avec les composants intrinsèques de NEON est qu’ils en valent rarement la peine. Si vous voulez battre le compilateur, vous devrez écrire réellement Assembly. La plupart du temps, quelle que soit la nature intrinsèque que vous auriez utilisée, le compilateur était déjà au courant. Vous avez plus souvent le pouvoir de restructurer vos boucles pour gérer au mieux votre pipeline (et les éléments intrinsèques ne vous aident pas là-bas). Il est possible que cela se soit amélioré au cours des deux dernières années, mais je m'attendrais à ce que l'optimiseur de vecteurs supérieur améliore la valeur des éléments intrinsèques plus que l'inverse.

12
Rob Napier

Voici un "moi aussi" avec quelques articles de blog d'ARM.FIRST, commencez par ce qui suit pour obtenir les informations d’arrière-plan, y compris le ARM32 bits (ARMV7 et versions antérieures), Aarch32 (ARMv8 ARMv8 ) et Aarch64 (ARMv8 64-bit ARM):

Second, vérifiez la série Coding for NEON. C’est une belle introduction avec des images pour que les choses telles que les charges entrelacées aient un sens avec un coup d’œil.

Je suis aussi allé sur Amazon à la recherche de livres sur ARM Assembly avec un traitement de NEON. Je ne pouvais en trouver que deux et le traitement de NEON dans aucun livre n'était impressionnant. Ils ont réduit à un seul chapitre avec l'exemple obligatoire de Matrix.


Je crois que ARM Les systèmes intrinsèques sont une très bonne idée. Les instructions vous permettent d’écrire du code pour les compilateurs GCC, Clang et Visual C/C++. Nous avons une base de code qui fonctionne pour ARM distributions Linux (comme Linaro), certains appareils iOS (utilisant -Arch armv7) et les gadgets Microsoft (comme Windows Phone et Windows Store Apps).

6
jww

Si vous avez accès à un GCC raisonnablement moderne (GCC 4.8 et supérieur), je vous conseillerais de tenter votre chance avec les produits intrinsèques. Les composants intrinsèques de NEON sont un ensemble de fonctions connues du compilateur, qui peuvent être utilisées à partir de programmes C ou C++ pour générer des instructions NEON/Advanced SIMD. Pour y accéder dans votre programme, il est nécessaire de #include <arm_neon.h>. La documentation détaillée de tous les éléments intrinsèques disponibles est disponible à l'adresse http://infocenter.arm.com/help/topic/com.arm.doc.ihi0073a/IHI0073A_arm_neon_intrinsics_ref.pdf , mais vous pouvez trouver des didacticiels plus conviviaux ailleurs en ligne.

Les conseils sur ce site sont généralement contre les éléments intrinsèques de NEON, et il y a certainement des versions de GCC qui ont mal fonctionné pour les implémenter, mais les versions récentes s'en tirent plutôt bien (et si vous remarquez une mauvaise génération de code, signalez-le comme un bogue - https://gcc.gnu.org/bugzilla/

C’est un moyen facile de programmer le jeu d’instructions NEON/Advanced SIMD et les performances que vous pouvez obtenir sont souvent plutôt satisfaisantes. Ils sont également "portables", en ce sens que lorsque vous passez à un système AArch64, un sur-ensemble des composants intrinsèques que vous pouvez utiliser à partir de ARMv7-A est disponible. Ils sont également portables pour toutes les implémentations de l'architecture ARM, dont les caractéristiques de performances peuvent varier, mais que le compilateur va modéliser pour l'optimisation des performances.

Le principal avantage des éléments intrinsèques NEON par rapport à un assemblage écrit à la main est que le compilateur peut les comprendre lorsqu’il effectue ses différentes passes d’optimisation. En revanche, assembleur écrit à la main est un bloc opaque pour GCC et ne sera pas optimisé. D'autre part, les programmeurs experts en assemblage peuvent souvent déjouer les règles d'allocation de registres du compilateur, en particulier lors de l'utilisation d'instructions permettant d'écrire ou de lire dans plusieurs registres consécutifs.

5
James Greenhalgh

En plus de la réponse de Wally - et devrait probablement faire l'objet d'un commentaire, je ne pourrais toutefois pas être assez bref: ARM dispose d'une équipe de développeurs de compilateurs dont le rôle consiste à améliorer les éléments de GCC et de Clang/llvm. la génération de code pour les processeurs ARM, y compris des fonctionnalités offrant une "vectorisation automatique" - je ne l'ai pas analysée en détail, mais d'après mon expérience de la génération de code x86, je m'attendrais à tout ce qui est relativement facile à vectoriser, le compilateur devrait faire un travail décevant. Certains codes sont difficiles à comprendre pour le compilateur quand ils peuvent vectoriser ou non, et peuvent nécessiter des "encouragements" - tels que des boucles de déroulement ou des conditions de marquage "probables" ou "improbables", etc.

Clause de non-responsabilité: je travaille pour ARM, mais je n’ai que très peu à voir avec les compilateurs, ni même avec les processeurs, car je travaille pour le groupe qui fait des graphiques (où j’ai une certaine implication dans les compilateurs pour les GPU dans la partie OpenCL du pilote GPU).

Modifier:

Les performances et l’utilisation de diverses extensions d’instructions dépendent vraiment EXACTEMENT de ce que fait le code. Je m'attendrais à ce que des bibliothèques telles qu'OpenCV intègrent déjà pas mal de choses intelligentes dans leur code (comme les assembleurs manuscrits en tant qu'intrinsèques du compilateur et le code généralement conçu pour permettre au compilateur de déjà faire du bon travail), peut ne pas vraiment vous donner beaucoup d'amélioration. Je ne suis pas un expert en vision par ordinateur, je ne peux donc pas vraiment dire exactement combien de travail est fait sur OpenCV, mais je m'attendrais certainement à ce que les points les plus "chauds" du code aient déjà été assez bien optimisés. 

En outre, profilez votre application. Ne vous contentez pas de manipuler les indicateurs d'optimisation, mesurez ses performances et utilisez un outil de profilage (l'outil "perform" de Linux, par exemple) pour mesurer O votre code prend du temps. Ensuite, voyez ce qui peut être fait pour ce code particulier. Est-il possible d’en écrire une version plus parallèle? Le compilateur peut-il aider, avez-vous besoin d'écrire assembleur? Y a-t-il un algorithme différent qui fait la même chose mais d'une meilleure façon, etc, etc ... 

Bien que modifier les options du compilateur PEUT aider, et souvent, cela peut donner des dizaines de pour cent, où un changement d’algorithme peut souvent conduire à un code 10 fois ou 100 fois plus rapide - en supposant bien sûr que votre algorithme puisse être amélioré!

Il est cependant essentiel de comprendre quelle partie de votre application prend le temps nécessaire. Il est inutile de changer les choses pour rendre le code qui prend 5% du temps 10% plus rapide, alors qu'un changement quelque part ailleurs pourrait rendre un morceau de code plus rapide de 30 ou 60% du temps total. Ou encore, optimisez une routine mathématique, lorsque 80% du temps est consacré à la lecture d'un fichier, où une mémoire tampon deux fois plus volumineuse serait deux fois plus rapide ...

5
Mats Petersson

Si vous ne voulez pas du tout gâcher le code d'assemblage, modifiez les indicateurs du compilateur pour optimiser la vitesse. gcc étant donné la bonne cible ARM devrait le faire, à condition que le nombre d'itérations de boucle soit évident.

Pour vérifier la génération de code gcc, demandez la sortie de Assembly en ajoutant l'indicateur -S.

Si, après plusieurs tentatives (de lecture de la documentation de gcc et de modification des indicateurs), vous ne parvenez toujours pas à obtenir le code que vous souhaitez, utilisez la sortie Assembly et modifiez-la à votre satisfaction.


Attention à optimisation prématurée . Le bon ordre de développement consiste à rendre le code fonctionnel, puis à voir s'il a besoin d'optimisation. Cela n’a de sens que si le code est stable.

4
wallyk

Bien que beaucoup de temps se soit écoulé depuis que j'ai soumis cette question, je me rends compte qu'elle suscite un certain intérêt et j'ai décidé de raconter ce que j'ai finalement fait à ce sujet. 

Mon objectif principal était d'optimiser une boucle for, qui constituait le goulot d'étranglement du projet. Donc, comme je ne connais rien à l’Assemblée, j’ai décidé d’essayer NEON intrinsics. J'ai fini par avoir un gain de performance de 40-50% (dans cette boucle uniquement), et une amélioration globale significative de la performance de l'ensemble du projet.

Le code fait des calculs pour transformer un ensemble de données de distance brutes en distance par rapport à un avion en millimètres. J'utilise des constantes (comme _constant05, _fXtoZ) qui ne sont pas définies ici, mais ce ne sont que des valeurs constantes définies ailleurs. Comme vous pouvez le voir, je fais le calcul pour 4 éléments à la fois, parle de la parallélisation réelle :)

unsigned short* frameData = frame.ptr<unsigned short>(_depthLimits.y, _depthLimits.x);

unsigned short step = _runWidth - _actWidth; //because a ROI being processed, not the whole image

cv::Mat distToPlaneMat = cv::Mat::zeros(_runHeight, _runWidth, CV_32F);

float* fltPtr = distToPlaneMat.ptr<float>(_depthLimits.y, _depthLimits.x); //A pointer to the start of the data

for(unsigned short y = _depthLimits.y; y < _depthLimits.y + _depthLimits.height; y++)
{
    for (unsigned short x = _depthLimits.x; x < _depthLimits.x + _depthLimits.width - 1; x +=4)
    {
        float32x4_t projX = {(float)x, (float)(x + 1), (float)(x + 2), (float)(x + 3)};
        float32x4_t projY = {(float)y, (float)y, (float)y, (float)y};

        framePixels = vld1_u16(frameData);

        float32x4_t floatFramePixels = {(float)framePixels[0], (float)framePixels[1], (float)framePixels[2], (float)framePixels[3]};

        float32x4_t fNormalizedY = vmlsq_f32(_constant05, projY, _yResInv);

        float32x4_t auxfNormalizedX = vmulq_f32(projX, _xResInv);
        float32x4_t fNormalizedX = vsubq_f32(auxfNormalizedX, _constant05);

        float32x4_t realWorldX = vmulq_f32(fNormalizedX, floatFramePixels);

        realWorldX = vmulq_f32(realWorldX, _fXtoZ);

        float32x4_t realWorldY = vmulq_f32(fNormalizedY, floatFramePixels);
        realWorldY = vmulq_f32(realWorldY, _fYtoZ);

        float32x4_t realWorldZ = floatFramePixels;

        realWorldX = vsubq_f32(realWorldX, _tlVecX);
        realWorldY = vsubq_f32(realWorldY, _tlVecY);
        realWorldZ = vsubq_f32(realWorldZ, _tlVecZ);

        float32x4_t distAuxX, distAuxY, distAuxZ;

        distAuxX = vmulq_f32(realWorldX, _xPlane);
        distAuxY = vmulq_f32(realWorldY, _yPlane);
        distAuxZ = vmulq_f32(realWorldZ, _zPlane);

        float32x4_t distToPlane = vaddq_f32(distAuxX, distAuxY);
        distToPlane = vaddq_f32(distToPlane, distAuxZ);

        *fltPtr = (float) distToPlane[0];
        *(fltPtr + 1) = (float) distToPlane[1];
        *(fltPtr + 2) = (float) distToPlane[2];
        *(fltPtr + 3) = (float) distToPlane[3];

        frameData += 4;
        fltPtr += 4;
    }
    frameData += step;
    fltPtr += step;
}
3
Pedro Batista

Jouez avec quelques exemples d'assemblage minimes sur QEMU pour comprendre les instructions

La configuration suivante n'a pas encore beaucoup d'exemples, mais elle sert de terrain de jeu soigné:

Les exemples fonctionnent en mode utilisateur QEMU, qui distribue du matériel supplémentaire, et la GDB fonctionne parfaitement.

Les assertions sont effectuées via la bibliothèque standard C.

Vous devriez être en mesure d'étendre facilement cette configuration avec de nouvelles instructions au fur et à mesure de votre apprentissage.

Les intrinsèques ARM ont notamment été posées à l’adresse suivante: Existe-t-il une bonne référence pour ARM Neon intrinsics?

0
Ciro Santilli