31 votes

Pourquoi ARM NEON n'est pas plus rapide que le simple C ++?

Voici un code C++:

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    for ( register int i = 0; i < ARR_SIZE_TEST; ++i )
    {
        x[ i ] = x[ i ] + y[ i ];
    }
}

Voici un néon version:

void neon_assm_tst_add( unsigned* x, unsigned* y )
{
    register unsigned i = ARR_SIZE_TEST >> 2;

    __asm__ __volatile__
    (
        ".loop1:                            \n\t"

        "vld1.32   {q0}, [%[x]]             \n\t"
        "vld1.32   {q1}, [%[y]]!            \n\t"

        "vadd.i32  q0 ,q0, q1               \n\t"
        "vst1.32   {q0}, [%[x]]!            \n\t"

        "subs     %[i], %[i], $1            \n\t"
        "bne      .loop1                    \n\t"

        : [x]"+r"(x), [y]"+r"(y), [i]"+r"(i)
        :
        : "memory"
    );
}

Fonction de Test:

void bench_simple_types_test( )
{
    unsigned* a = new unsigned [ ARR_SIZE_TEST ];
    unsigned* b = new unsigned [ ARR_SIZE_TEST ];

    neon_tst_add( a, b );
    neon_assm_tst_add( a, b );
}

J'ai testé les deux variantes et voici le rapport:

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 185 ms // SLOW!!!

J'ai aussi testé d'autres types:

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms // FASTER X3!

LA QUESTION: Pourquoi le néon est plus lent avec l'entier de 32 bits types?

J'ai utilisé la dernière version de GCC pour Android NDK. NEON options d'optimisation qui ont été activées. Voici une démonté C++ version:

                 MOVS            R3, #0
                 PUSH            {R4}

 loc_8
                 LDR             R4, [R0,R3]
                 LDR             R2, [R1,R3]
                 ADDS            R2, R4, R2
                 STR             R2, [R0,R3]
                 ADDS            R3, #4
                 CMP.W           R3, #0x2000000
                 BNE             loc_8
                 POP             {R4}
                 BX              LR

Ici est démonté version du néon:

                 MOV.W           R3, #0x200000
.loop1
                 VLD1.32         {D0-D1}, [R0]
                 VLD1.32         {D2-D3}, [R1]!
                 VADD.I32        Q0, Q0, Q1
                 VST1.32         {D0-D1}, [R0]!
                 SUBS            R3, #1
                 BNE             .loop1
                 BX              LR

Voici tous les bancs de tests:

add, char,     C++       : 83  ms
add, char,     neon asm  : 46  ms FASTER x2

add, short,    C++       : 114 ms
add, short,    neon asm  : 92  ms FASTER x1.25

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 184 ms SLOWER!!!

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms FASTER x3

add, double,   C++       : 533 ms
add, double,   neon asm  : 420 ms FASTER x1.25

LA QUESTION: Pourquoi le néon est plus lent avec l'entier de 32 bits types?

48voto

John Ripley Points 2922

Le NÉON de pipeline sur le Cortex-A8 est dans l'ordre de l'exécution, et a peu de hit-sous-miss (aucun changement), de sorte que vous êtes limité par la latence de la mémoire (que vous êtes en utilisant plus de L1/L2 taille du cache). Votre code est immédiate des dépendances sur les valeurs chargées à partir de la mémoire, de sorte qu'il va bloquer constamment en attente pour la mémoire. Cela expliquerait pourquoi le NÉON code est légèrement (un petit peu) plus lent que les non-NÉON.

Vous avez besoin de dérouler l'assemblée des boucles et l'augmentation de la distance entre la charge et de l'utilisation, de l'e.g:

vld1.32   {q0}, [%[x]]!
vld1.32   {q1}, [%[y]]!
vld1.32   {q2}, [%[x]]!
vld1.32   {q3}, [%[y]]!
vadd.i32  q0 ,q0, q1
vadd.i32  q2 ,q2, q3
...

Il y a beaucoup de néon registres, de sorte que vous pouvez dérouler beaucoup. Code entier subira le même problème, dans une moindre mesure, A8 entier a un meilleur hit-sous-miss au lieu de s'enliser. Le goulot d'étranglement est va être la mémoire de la bande passante et la latence pour les repères de manière importante par rapport à L1/L2 cache. Vous pouvez également exécuter la valeur de référence à de plus petites tailles (moins de 4 ko..256 KO) pour voir les effets lorsque les données sont mises en cache entièrement en L1 et/ou L2.

17voto

Exophase Points 191

Bien que vous êtes limité par le temps de latence de la mémoire principale dans ce cas, il n'est pas évident que le NEON version serait plus lent que l'ASM version.

En utilisant le cycle de calculatrice ici:

http://pulsar.webshaker.net/ccc/result.php?lng=en

Votre code doit prendre 7 cycles avant que le cache miss sanctions. Il est plus lent que vous pouvez vous attendre parce que vous êtes en utilisant non alignés et de charges en raison de la latence entre le complément et le magasin.

Pendant ce temps, le compilateur a généré boucle prend 6 cycles (c'est pas très bien planifiée ou optimisé en général). Mais elle fait un quart autant de travail.

Le cycle des chiffres de script peut être pas parfait, mais je ne vois rien qui ressemble manifestement mal avec elle donc je pense qu'ils aurait au moins proche. Il y a du potentiel pour prendre un cycle sur la branche si vous le max de récupérer la bande passante (même si les boucles ne sont pas 64 bits alignés), mais dans ce cas, il y a beaucoup de stands de le cacher.

La réponse n'est pas entier sur Cortex-A8 a plus de possibilités pour masquer la latence. En fait, il a normalement moins, à cause des NÉONS décalés pipeline et le problème de la file d'attente. Bien sûr, ceci n'est vrai que sur Cortex-A8 - sur Cortex-A9, la situation peut être inversée (le NÉON est distribué dans l'ordre et en parallèle avec entier, tout entier a de capacités). Depuis que vous avez marqués ce Cortex-A8, je suppose que c'est ce que vous êtes en utilisant.

Cela soulève plus d'enquête. Voici quelques idées pour lesquelles cela pourrait se produire:

  • Vous n'êtes pas en précisant tout type d'alignement sur vos baies, et alors que j'attends des nouvelles pour l'aligner sur 8 octets, il peut ne pas être en alignant à 16 octets. Disons que vous êtes vraiment obtenir des tableaux qui ne sont pas de 16 octets alignés. Alors vous seriez le fractionnement entre les lignes, sur les accès au cache qui pourrait avoir de la peine complémentaire (surtout sur les accidents)
  • Un cache miss qui se passe juste après une magasin; je ne crois pas que le Cortex-A8 a la mémoire de désambiguïsation et doit donc supposer que la charge pouvait être de la même ligne que le magasin, donc nécessitant de la mémoire tampon d'écriture à la fuite avant de la L2 charge manquante peut arriver. Parce qu'il y a un beaucoup plus grand pipeline distance entre le NÉON des charges (qui sont initiés dans le pipeline entier) et les commerces (initiée à la fin du NÉON pipeline) qu'un entier, il y aurait potentiellement une plus étal de.
  • Parce que vous êtes en train de charger 16 octets par l'accès au lieu de 4 octets de la critique-parole de la taille est plus grande et, par conséquent, l'efficacité de la latence pour une critique-mot-première ligne de remplissage de la mémoire principale va être plus élevé (L2 à L1 est censé être sur un cryptage de 128-bit bus et donc ne devrait pas avoir le même problème)

Vous avez demandé à quoi bon le NÉON est dans des cas comme celui - ci, en réalité, le NÉON est particulièrement bon pour ces cas où vous êtes en streaming vers/à partir de la mémoire. Le truc, c'est que vous devez utiliser le préchargement dans le but de cacher la mémoire principale de latence autant que possible. Précharge obtenir la mémoire dans L2 (pas de L1) cache à l'avance. Ici, le NÉON a un gros avantage par rapport entier, car il peut cacher beaucoup de la mémoire cache L2 de latence, en raison de sa décalés pipeline et le problème de la file d'attente, mais aussi parce qu'il a un accès direct à elle. Je m'attends à vous voir efficace L2 latence vers le bas pour les 0 à 6 cycles et moins si vous avez moins de dépendances et de ne pas épuiser la charge de la file d'attente, alors que sur entier, vous pouvez être coincé avec une bonne ~16 cycles que vous ne pouvez pas éviter (dépend probablement sur le Cortex-A8, quoique).

Donc, je vous recommande d'aligner vos tableaux à cache-taille de la ligne (64 octets), déroulez vos boucles d'en faire au moins un cache-ligne à la fois, utilisez aligné des charges ou des points de vente (put :128 après l'adresse) et d'ajouter un pld instruction de chargement de plusieurs cache-lignes. Comme pour combien de lignes: commencer petit et de continuer à augmenter jusqu'à ce que vous ne voyez plus aucun avantage.

15voto

Votre code C++ n'est pas optimisé non plus.

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    unsigned int i = ARR_SIZE_TEST;
    do
    {
        *x++ += *y++;
    } (while --i);
}

cette version consomme moins 2 cycles/itération.

Par ailleurs, les résultats d'un benchmark ne m'étonne pas du tout.

32 bits :

Cette fonction est trop simple pour le NÉON. Il n'y a pas assez d'opérations arithmétiques en laissant de l'espace pour les optimisations.

Oui, c'est tellement simple que C++ et NEON version souffrent de pipeline dangers presque à chaque fois, sans réelle chance de bénéficier de la double question des capacités.

Tout en NÉON version peut bénéficier de traitement de 4 entiers à la fois, il souffre beaucoup plus de tous les dangers ainsi. C'est tout.

8bit :

Le BRAS est TRÈS lent à la lecture de chaque octet de la mémoire. Ce qui signifie, tout en NÉON montre les mêmes caractéristiques que avec 32 bits, ARM est à la traîne lourdement.

16 bits : De la même manière ici. À l'exception de BRAS 16bit lire n'est pas mauvais.

float : La version C++ compiler dans VFP codes. Et il n'y a pas un VFP sur Coretex A8, mais VFP lite, qui n'a pas de pipeline rien qui suce.

Ce n'est pas que le NÉON est de se comporter étrangement traitement 32 bits. C'est juste le BRAS qui répond à la condition idéale. Votre fonction est très inapproprié pour l'analyse comparative des fins en raison de sa simplicité. Essayez quelque chose de plus complexe comme YUV-conversion RGB :

Pour info, mon optimisée NEON version fonctionne à peu près 20 fois plus rapide que mon entièrement optimisé pour la version C et 8 fois plus rapide que mon optimisée BRAS de montage version. J'espère que cela va vous donner une idée de la puissance de NÉON peut être.

Dernier mais non le moindre, le BRAS d'instruction le PLD est NEON meilleur ami. Bien placée, elle apportera au moins 40% d'augmentation des performances.

5voto

webshaker Points 41

Vous pouvez essayer quelques modifications pour améliorer le code.

Si vous le pouvez: - utiliser un tampon pour stocker les résultats. - essayer d'aligner les données sur 8 octets.

Le code doit être quelque chose comme (désolé je ne connais pas la gcc inline syntaxe)

.loop1:
 vld1.32   {q0}, [%[x]:128]!
 vld1.32   {q1}, [%[y]:128]!
 vadd.i32  q0 ,q0, q1
 vst1.32   {q0}, [%[z]:128]!
 subs     %[i], %[i], $1
bne      .loop1

Comme Exophase dit que vous avez quelques pipeline de latence. peut-être vous pouvez essayer

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

sub     %[i], %[i], $1

.loop1:
vadd.i32  q2 ,q0, q1

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

vst1.32   {q2}, [%[z]:128]!
subs     %[i], %[i], $1
bne      .loop1

vadd.i32  q2 ,q0, q1
vst1.32   {q2}, [%[z]:128]!

Enfin, il est clair que vous allez saturer la bande passante de la mémoire

Vous pouvez essayer d'ajouter un petit

PLD [%[x], 192]

dans votre boucle.

dites-nous si c'est mieux...

0voto

Giovanni Funchal Points 3275

8ms de la différence est SI petite que vous êtes probablement à la mesure des objets de la des caches ou des pipelines.

EDIT: Avez-vous essayez de comparer avec quelque chose comme ça pour les types, tels que le flotteur et le court etc? Je m'attends au compilateur d'optimiser encore mieux et de réduire l'écart. Également dans votre test, vous faire la version C++ d'abord, puis l'ASM version, ce qui peut avoir un impact dans la performance à ce que j'avais à écrire deux programmes différents pour être plus juste.

for ( register int i = 0; i < ARR_SIZE_TEST/4; ++i )
{
    x[ i ] = x[ i ] + y[ i ];
    x[ i+1 ] = x[ i+1 ] + y[ i+1 ];
    x[ i+2 ] = x[ i+2 ] + y[ i+2 ];
    x[ i+3 ] = x[ i+3 ] + y[ i+3 ];
}

Dernière chose, dans la signature de votre fonction, vous utilisez unsigned* au lieu de unsigned[]. Ce dernier est préféré, car le compilateur suppose que les tableaux ne se chevauchent pas et qu'il est autorisé à réorganiser les accès. Essayez d'utiliser l' restrict mot-clé pour une meilleure protection contre l'aliasing.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X