2016-02-16 3 views
1

J'ai écrit le code d'assemblage du bras v7 pour la fonction c ci-dessous. Mais notre code de montage prend plus de temps comparé au code C. S'il vous plaît quelqu'un peut-il me dire la raison.code d'assemblage de bras prenant plus de temps par rapport à C

int get_maximum_sample_value (short int *inp_frame, int frame_size) { 
    short int *temp_buff = inp_frame; // Holds the local pointer. 

    int maximum_value = -1000; // Holds the maximum value. 
    int abs_value  = 0;  // Holds the absolute value. 

    // Get the maximum value of the frame. 
    for (int index = 0; index < frame_size; ++index) { 

     abs_value = abs(*temp_buff); 

     if (maximum_value < abs_value) { 
      maximum_value = abs_value; 
     } 
     ++temp_buff; 
    } 

    return maximum_value; 
} 

asm:

.cfi_startproc 

push{r4} 

ldr r4,LC_P1000 // LC_P1000 = -1000 
vdup.s32 q2,r4 
cmp r1, #0 
beq LP_VD_END 

lsrs r4,r1,#2 
beq LP_VD_END 

LP_VD1: 

vldm r0,{d0} 
add r0,#8 
vmovl.s16 q1,d0 

vabs.s32 q1,q1 
subs r4, r4, #1 
vmax.s32 q2,q1,q2 
bne LP_VD1 
vmax.s32 d4,d5,d4 

vmov r0,s8 
vmov r2,s9 
cmp r0, r2 
it lt 
movlt r0, r2 

LP_VD_END: 
pop{r4} 
bx lr 
.cfi_endproc 
+0

Je n'ai pas vraiment lu votre code mais une chose à garder à l'esprit: Les compilateurs sont "beaucoup" meilleurs pour optimiser le code que les humains –

+1

Vous devriez créer le code source assembleur de sortie du compilateur (tous les compilateurs C que j'ai essayés a cette option) alors vous pouvez le comparer avec votre version asm. –

+0

Qu'est-ce que le compilateur fait mal que vous voyez que vous pouvez faire mieux? Il est souvent possible de battre le compilateur, surtout si vous connaissez la microarchitecture cible et comment l'optimiser. (par exemple pour x86, http://agner.org/optimize/ guide de microarchitecture). Souvent, la sortie 'gcc -O3' est un bon point de départ. Je ne serais pas surpris si votre compilateur a déjà fait un travail décent auto-vectorisation de cette fonction, donc il n'y a peut-être pas grand chose à gagner. Parfois, vous pouvez seulement découper une ou deux instructions, ou resserrer la disposition de branchement. –

Répondre

3

Il est assez difficile de dire pourquoi l'assemblée écrite à la main est plus lent que C sans voir la sortie du compilateur, sans savoir si le compilateur ne auto-vectorisation, etc. Cependant, il est il est facile de dire pourquoi le code d'assemblage est (très) lent:

  • Les instructions simon NEON ont une longue latence et un débit élevé. En utilisant seulement 1 vecteur de valeur maximale, vous avez sérialisé le problème à l'origine parallèle. Toutes les opérations vectorielles dépendent du résultat de l'instruction précédente, ce qui les oblige à attendre la totalité de la latence de 4 cycles avant de pouvoir s'exécuter. Le problème est encore pire sur les noyaux avec des pipelines d'exécution simd en ordre (tous les nouveaux noyaux Cortex-A A9, A15, A57, A72 et certains d'Apple).
  • Si la matrice d'entrée est grande et n'est pas présente dans les caches, le code est limité en attendant que les opérations de la mémoire se terminent. Certains processeurs ARM ont des préextracteurs de mémoire L2 matériels, mais même sur ces prélecture, la mémoire du logiciel peut accélérer la boucle plusieurs fois.

Une mise en œuvre rapide écrit en intrinsics NEON pourrait ressembler à ceci:

int16_t* buf = inp_frame; 

// These variables hold the absolute values during the loop. 
// Must use 32-bit values because abs(INT16_MIN) doesn't fit in 16-bit signed int. 
int32x4_t max0 = vmovq_n_s32(INT16_MIN); 
int32x4_t max1 = vmovq_n_s32(INT16_MIN); 
int32x4_t max2 = vmovq_n_s32(INT16_MIN); 
int32x4_t max3 = vmovq_n_s32(INT16_MIN); 
int32x4_t max4 = vmovq_n_s32(INT16_MIN); 
int32x4_t max5 = vmovq_n_s32(INT16_MIN); 
int32x4_t max6 = vmovq_n_s32(INT16_MIN); 
int32x4_t max7 = vmovq_n_s32(INT16_MIN); 

// Process 32 values = 64 bytes per iteration. 
for(int i = frame_size/32; i != 0; i--) 
{ 
    // Prefetch data 8 64-byte cache lines ahead (or 16, optimal distance depends on hw). 
    __prefetch(8 * 64 + ((int8_t*)buf)); // whatever intrinsic your compiler has 

    int16x8_t val0 = vld1q_s16(buf); 
    int16x8_t val1 = vld1q_s16(buf + 8); 
    int16x8_t val2 = vld1q_s16(buf + 16); 
    int16x8_t val3 = vld1q_s16(buf + 24); 
    buf += 32; 

    // Widen the values before taking abs. 
    int32x4_t vall0 = vmovl_s16(vget_low_s16(val0)); 
    int32x4_t vall1 = vmovl_s16(vget_high_s16(val0)); 
    int32x4_t vall2 = vmovl_s16(vget_low_s16(val1)); 
    int32x4_t vall3 = vmovl_s16(vget_high_s16(val1)); 
    int32x4_t vall4 = vmovl_s16(vget_low_s16(val2)); 
    int32x4_t vall5 = vmovl_s16(vget_high_s16(val2)); 
    int32x4_t vall6 = vmovl_s16(vget_low_s16(val3)); 
    int32x4_t vall7 = vmovl_s16(vget_high_s16(val3)); 

    int32x4_t abs_vall0 = vabsq_s32(vall0); 
    int32x4_t abs_vall1 = vabsq_s32(vall1); 
    int32x4_t abs_vall2 = vabsq_s32(vall2); 
    int32x4_t abs_vall3 = vabsq_s32(vall3); 
    int32x4_t abs_vall4 = vabsq_s32(vall4); 
    int32x4_t abs_vall5 = vabsq_s32(vall5); 
    int32x4_t abs_vall6 = vabsq_s32(vall6); 
    int32x4_t abs_vall7 = vabsq_s32(vall7); 

    max0 = vmaxq_s32(max0, abs_vall0); 
    max1 = vmaxq_s32(max1, abs_vall1); 
    max2 = vmaxq_s32(max2, abs_vall2); 
    max3 = vmaxq_s32(max3, abs_vall3); 
    max4 = vmaxq_s32(max4, abs_vall4); 
    max5 = vmaxq_s32(max5, abs_vall5); 
    max6 = vmaxq_s32(max6, abs_vall6); 
    max7 = vmaxq_s32(max7, abs_vall7); 
} 

// Reduce the maximum value to a single one. 
int32x4_t max01 = vmaxq_s32(max0, max1); 
int32x4_t max23 = vmaxq_s32(max2, max3); 
int32x4_t max45 = vmaxq_s32(max4, max5); 
int32x4_t max67 = vmaxq_s32(max6, max7); 

int32x4_t max= vmaxq_s32(max01, max23); 
int32x4_t max4567 = vmaxq_s32(max45, max67); 
int32x4_t qmax = vmaxq_s32(max0123, max4567); 

// Horizontal max inside q-register. 
int32x2_t dmax = vmax_s32(vget_low_s32(qmax), vget_high_s32(qmax)); 
int32_t max_value = vget_lane_s32(vpmax_s32(dmax, dmax), 0); 

// TODO process any remaining items here 

Ce désentrelacement genre produit beaucoup de parallélisme au niveau des instructions, ce qui permet au cœur d'exécuter des instructions à chaque cycle au lieu de caler en raison des dépendances de données . L'entrelacement/le déroulement à 8 voies est suffisant pour garder le Cortex-A72 le plus rapide capable d'exécuter 2 de ces instructions NEON ALU de 3 cycles de latence par heure, occupé. Notez que le code utilise les 16 registres q architecturaux disponibles, vous pouvez donc vouloir vérifier que le compilateur n'en déverse aucun (tous les compilateurs ne gèrent pas très bien la situation).

+0

merci Ta Réponse – ravi