Portage de sections bas niveau d'applications Android natives sur des plates-formes basées sur l'architecture Intel

Tao Peng

Traducteur

Lana Bauer

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Portage de sections bas niveau d'applications Android natives sur des plates-formes basées sur l'architecture Intel

- Auteur : Tao Peng

- Traducteur : Lana Bauer

- Parution : 10 juillet 2014

- Licence : Copyright ® 2014 Tao Peng. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Instruction unique et données multiples (SIMD)

- II-A. Intel® Streaming SIMD Extensions (Intel® SSE)

- III. Comment convertir NEON en Intel SSE ?

- IV. Activer le langage Assembleur

- IV-A. La conversion du boutisme en ARM et x86

- IV-B. Conclusion

- IV-C. À propos de l'auteur

- IV-D. Ressources

I - Introduction

Il existe deux types d'applications pour Android. Le premier type correspond à l'application Dalvik qui est basée sur Java et qui peut tourner correctement sur n'importe quelle architecture sans aucune modification. Le second type concerne les applications NDK dont une partie du code est écrite en C/C++ ou ASM et qui doivent être compilées pour un processeur spécifique.

Cette discussion sera axée sur les applications NDK. On a généralement juste besoin de modifier le paramètre APPABI, dans le fichier application.mk et de compiler la partie NDK, ce qui lui permettra, par la suite, d'être exécuté sur le périphérique correspondant. Cependant, certaines parties des applications NDK ne peuvent pas être simplement compilées à nouveau si elles contiennent certains types de codes, comme un code en langage assembleur ou un code à instruction unique et données multiples (SIMD : Single Instruction Multiple Data).

Cet article explique comment traiter ces problèmes et présente les majeures considérations que les développeurs doivent connaitre sur le fait de porter une application de plate-forme d'architecture non Intel (x86) sur une plate-forme d'architecture Intel. On traitera également la conversion de boutisme du x86 vers les plates-formes non x86.

II - Instruction unique et données multiples (SIMD)

SIMD (Single Instruction Multiple Data) est une classe d'ordinateurs parallèles, comme décrit par la taxonomie de Flynn, avec de multiples éléments de traitement qui effectuent la même opération sur plusieurs points de données simultanément. Ces machines exploitent le parallélisme des données, car il y a simultanément des calculs. SIMD est particulièrement adéquat sur des tâches courantes telles que le réglage du contraste dans une image numérique, ou le réglage du volume du son numérique.

La plupart des modèles de processeurs incluent les instructions SIMD afin d'améliorer les performances pour une utilisation multimédia. Pour la plate-forme x86 mobile, SIMD est appelé « Intel Streaming SIMD Extensions » (Intel® SSE, SSE2, SSE3, etc.). Pour la plate-forme ARM*, SIMD est appelé « NEON technology ». Si vous avez besoin d'informations sur NEON, référez-vous à la documentation du constructeur.

II-A - Intel® Streaming SIMD Extensions (Intel® SSE)

D'abord, qu'est-ce que Intel SSE ? C'est essentiellement une collection de registres de processeur 128 bits. Ces registres peuvent être représentés en quatre scalaires de 32 bits qui permettent à une opération d'être effectuée sur chacun des quatre éléments simultanément.

En revanche, ça peut prendre quatre opérations ou plus en assembleur régulier pour faire la même chose. Dans le diagramme ci-dessous, vous pouvez voir deux vecteurs (registres Intel SSE) représentés avec des scalaires. Les registres sont multipliés avec MULPS qui stocke le résultat. Voilà quatre multiplications réduites en une seule opération. L'intérêt d'utiliser Intel SSE est suffisamment important pour ne pas être ignoré.

Figure 1 : deux vecteurs (registres Intel SSE) représentés avec des scalaires

Maintenant, avec l'idée de base d'Intel SSE en tête, nous allons jeter un coup d'œil à quelques-unes des instructions les plus courantes.

Les instructions de mouvement de données

MOVUPS

Déplacer 128 bits de données vers un registre SIMD de la mémoire ou registre SIMD. Non-aligné.

MOVAPS

Déplacer 128 bits de données vers un registre SIMD de la mémoire ou registre SIMD. Aligné.

MOVHPS

Déplacer 64 bits à des bits supérieurs d'un registre SIMD (élevé).

MOVLPS

Déplacer 64 bits à bits de poids faible d'un registre SIMD (bas).

MOVHLPS

Déplacer les 64 bits supérieurs du registre source vers 64 bits inférieurs du registre de destination.

MOVLHPS

Déplacer les 64 bits inférieurs du registre source vers 64 bits supérieurs du registre de destination.

MOVMSKPS

Déplacer les bits de signe de chacun des quatre scalaires emballés vers un registre x86 d'entiers.

MOVSS

Déplacer 32 bits à un registre SIMD de la mémoire ou du registre SIMD.

Instructions arithmétiques

NOTE : un scalaire va effectuer l'opération uniquement sur les premiers éléments. La version parallèle va effectuer l'opération sur tous les éléments dans le registre.

Parallèle

Scalaire

Description

ADDPS

ADDSS

additionne les opérandes.

SUBPS

SUBSS

soustrait les opérandes.

MULPS

MULSS

multiplie les opérandes.

DIVPS

DIVSS

divise les opérandes.

SQRTPS

SQRTSS

racine carrée de l'opérande.

MAXPS

MAXSS

Maximum des opérandes.

MINPS

MINSS

Minimum des opérandes.

RCPPS

RCPSS

inverse de l'opérande.

RSQRTPS

RSQRTSS

inverse de la racine carrée de l'opérande.

Instructions de comparaison

Parallèle

Scalaire

CMPPS, CMPSS

Compare les opérateurs et retourne tout à 1 ou tout à 0.

Instructions logiques

ANDPS

ET bit à bit des opérandes.

ANDNPS

NON-ET bit à bit des opérandes.

ORPS

OU bit à bit des opérandes.

XORPS

OU exclusive des opérandes.

Instructions aléatoires

SHUFPS

Mélanger les nombres d'un opérande à un autre ou lui-même.

UNPCKHPS

Déballer les nombres d'ordre élevé pour un registre SIMD.

UNPCKLPS

Déballer les nombres d'ordre bas pour un registre SIMD.

D'autres instructions non traitées ici incluent la conversion des données entre le x86 et les registres MMX, les instructions de contrôle de la cache et les instructions de la gestion du statut.

III - Comment convertir NEON en Intel SSE ?

Les instructions Intel SSE et NEON ne sont pas parfaitement équivalentes. Bien que basée sur le même principe de conception, la méthode d'implémentation est différente. Vous devez traduire chaque instruction une par une.

Le code suivant utilise les instructions NEON :

int16x8_t q0 = vdupq_n_s16(-1000), q1 = vdupq_n_s16(1000);
int16x8_t zero = vdupq_n_s16(0);
for( k = 0; k < 16; k += 8 )
{
int16x8_t v0 = vld1q_s16((const int16_t*)(d+k+1));
int16x8_t v1 = vld1q_s16((const int16_t*)(d+k+2));
int16x8_t a = vminq_s16(v0, v1);
int16x8_t b = vmaxq_s16(v0, v1);
v0 = vld1q_s16((const int16_t*)(d+k+3));
a = vminq_s16(a, v0);
b = vmaxq_s16(b, v0);
v0 = vld1q_s16((const int16_t*)(d+k+4));
a = vminq_s16(a, v0);
b = vmaxq_s16(b, v0);
v0 = vld1q_s16((const int16_t*)(d+k+5));
a = vminq_s16(a, v0);
b = vmaxq_s16(b, v0);
v0 = vld1q_s16((const int16_t*)(d+k+6));
a = vminq_s16(a, v0);
b = vmaxq_s16(b, v0);
v0 = vld1q_s16((const int16_t*)(d+k+7));
a = vminq_s16(a, v0);
b = vmaxq_s16(b, v0);
v0 = vld1q_s16((const int16_t*)(d+k+8));
a = vminq_s16(a, v0);
b = vmaxq_s16(b, v0);
v0 = vld1q_s16((const int16_t*)(d+k));
q0 = vmaxq_s16(q0, vminq_s16(a, v0));
q1 = vminq_s16(q1, vmaxq_s16(b, v0));
v0 = vld1q_s16((const int16_t*)(d+k+9));
q0 = vmaxq_s16(q0, vminq_s16(a, v0));
q1 = vminq_s16(q1, vmaxq_s16(b, v0));
}
q0 = vmaxq_s16(q0, vsubq_s16(zero, q1));
// première erreur, ça produit un mauvais résultat
//q0 = vmaxq_s16(q0, vzipq_s16(q0, q0).val[1]);
// peut être que quelqu'un connait une meilleure méthode?
int16x4_t a_hi = vget_high_s16(q0);
q1 = vcombine_s16(a_hi, a_hi);
q0 = vmaxq_s16(q0, q1);

// Ceci est _mm_srli_si128(q0, 4)
q1 = vextq_s16(q0, zero, 2);
q0 = vmaxq_s16(q0, q1);

// Ceci est _mm_srli_si128(q0, 2)
q1 = vextq_s16(q0, zero, 1);
q0 = vmaxq_s16(q0, q1);

// lire le résultat
int16_t __attribute__ ((aligned (16))) x[8];
vst1q_s16(x, q0);
threshold = x[0] - 1;

Le code suivant utilise Intel SSE :

__m128i q0 = _mm_set1_epi16(-1000), q1 = _mm_set1_epi16(1000);
for( k = 0; k < 16; k += 8 )
{
__m128i v0 = _mm_loadu_si128((__m128i*)(d+k+1));
__m128i v1 = _mm_loadu_si128((__m128i*)(d+k+2));
__m128i a = _mm_min_epi16(v0, v1);
__m128i b = _mm_max_epi16(v0, v1);
v0 = _mm_loadu_si128((__m128i*)(d+k+3));
a = _mm_min_epi16(a, v0);
b = _mm_max_epi16(b, v0);
v0 = _mm_loadu_si128((__m128i*)(d+k+4));
a = _mm_min_epi16(a, v0);
b = _mm_max_epi16(b, v0);
v0 = _mm_loadu_si128((__m128i*)(d+k+5));
a = _mm_min_epi16(a, v0);
b = _mm_max_epi16(b, v0);
v0 = _mm_loadu_si128((__m128i*)(d+k+6));
a = _mm_min_epi16(a, v0);
b = _mm_max_epi16(b, v0);
v0 = _mm_loadu_si128((__m128i*)(d+k+7));
a = _mm_min_epi16(a, v0);
b = _mm_max_epi16(b, v0);
v0 = _mm_loadu_si128((__m128i*)(d+k+8));
a = _mm_min_epi16(a, v0);
b = _mm_max_epi16(b, v0);
v0 = _mm_loadu_si128((__m128i*)(d+k));
q0 = _mm_max_epi16(q0, _mm_min_epi16(a, v0));
q1 = _mm_min_epi16(q1, _mm_max_epi16(b, v0));
v0 = _mm_loadu_si128((__m128i*)(d+k+9));
q0 = _mm_max_epi16(q0, _mm_min_epi16(a, v0));
q1 = _mm_min_epi16(q1, _mm_max_epi16(b, v0));
}
q0 = _mm_max_epi16(q0, _mm_sub_epi16(_mm_setzero_si128(), q1));
q0 = _mm_max_epi16(q0, _mm_unpackhi_epi64(q0, q0));
q0 = _mm_max_epi16(q0, _mm_srli_si128(q0, 4));
q0 = _mm_max_epi16(q0, _mm_srli_si128(q0, 2));
threshold = (short)_mm_cvtsi128_si32(q0) - 1;

Pour plus d'informations à propos de la conversion NEON en Intel SSE, référez-vous au blog cité dans la section références [1]. Il fournit un fichier d'en-tête qui peut être utilisé pour créer automatiquement une carte des instructions NEON et Intel SSE.

IV - Activer le langage Assembleur

Les microprocesseurs à jeu d'instruction étendu (Complex Instruction Set Computer : CISC), comme ceux d'Intel, ont un ensemble d'instructions riches et capables d'effectuer des actions complexes avec une instruction unique (contrairement à l'architecture RISC qui vise des instructions plus généralistes ainsi que l'efficacité). Les instructions CISC ont un nombre relativement important de registres à usage généraliste, et d'instructions de données qui utilisent généralement trois registres : une destination et deux opérandes. Si vous avez besoin d'informations sur les instructions/l'architecture ARM, référez-vous à la documentation du constructeur.

Les processeurs Intel (cf. 386 et plus) ont huit registres 32 bits à usage général comme montré dans le diagramme suivant. Les noms d'un registre sont pour la plupart historiques. Par exemple, EAX était appelé « l'accumulateur » puisqu'il était utilisé par un certain nombre d'opérations arithmétiques ; ECX était connu sous le nom de « calculateur » puisqu'il était utilisé pour maintenir un indice de boucle. Alors que la plupart des registres ont perdu leurs fins spéciales dans le jeu d'instructions modernes ; par convention, deux sont réservés à des fins particulières : le pointeur de pile (ESP) et le pointeur de base (EBP).

Figure 2 : processeurs Intel x86 avec huit registres de 32 bits à usage général

Pour les registres EAX, EBX, ECX et EDX, des sous-ensembles peuvent être utilisés. Par exemple, les deux octets les moins significatifs de EAX peuvent être traités comme un registre de 16 bits appelé AX. L'octet le moins significatif de AX peut être utilisé comme un simple registre de 8 bits appelé AL, tandis que l'octet le plus significatif de AX peut être utilisé comme un simple registre de 8 bits appelé AH. Ces noms font référence au même registre physique. Quand une quantité de deux octets est placée dans DX, la mise à jour affecte la valeur de DH, DL et EDX. Ces sous-registres sont principalement les restants des anciennes versions 16 bits du jeu d'instructions. Cependant, ils sont parfois appropriés lorsqu'il s'agit de données qui sont plus petites que 32 bits (par exemple, les caractères ASCII sur un octet).

Puisqu'il y a des différences entre ARM et le langage assembleur x86, le code assembleur ARM ne peut pas être utilisé directement sur les plates-formes x86 :

1. Implémenter la même fonction avec le langage assembleur x86 ;

2. Remplacer le code ARM avec le code C.

Dans de nombreux programmes Open Source, le code ASM a été remplacé pour améliorer les performances, mais la performance n'est pas un problème dans ce cas, car les processeurs sont plus robustes que jamais. Cependant, contrairement au code ASM écrasé, le code C qui a implémenté la même fonction a été retenu dans le code source, cela nous permet de compiler du code C pour une plate-forme x86.

Par exemple, un éditeur de logiciels a développé un jeu qui utilise le format de compression audio Vorbis, un programme open source qui contient du code segmenté assembleur ARM. Donc, l'éditeur de logiciels n'a pas pu le convertir en x86 NDK. Au lieu d'avoir à réécrire cette section de code x86 ASM, le développeur la recompile en C de sorte qu'il s'exécute correctement sur x86. Le problème a été résolu en désactivant la macro_ARM_ASSEM_ et en activant la macro _LOW_ACCURACY_.

IV-A - La conversion du boutisme en ARM et x86

Dans les projets multiplateformes, nous sommes souvent confrontés à un vieux problème de conversion de boutisme. Si le fichier est généré sur une machine petit boutiste, un entier 255 peut être stocké comme suit :

ff 00 00 00

Mais quand la valeur est lue dans la mémoire, elle sera différente pour diverses plates-formes, ce qui provoquera un problème de portage.

int a;
fread(&a, sizeof(int), 1, file);
// sur une machine petit boutiste, a = 0xff;
// mais sur une machine grand boutiste, a = 0xff000000;

Une manière très simple et efficace de résoudre ce problème est d'écrire une fonction appelée readInt() :

void readInt(void* p, file)
{
char buf[4];
fread(buf, 4, 1, file);
*((uint32*)p) = buf[0] << 24 | buf[1] << 16
| buf[2] << 8 | buf[3];}

La fonction a l'avantage de marcher sur les plates-formes grand boutiste et petit boutiste, mais elle est incompatible avec la méthode habituelle pour lire une structure.

fread(&header, sizeof(struct MyFileHeader), 1, file);

Si MyFileHeader contient beaucoup d'entiers, ça va résulter en plusieurs read(). Ce n'est pas seulement lourd à coder, mais en plus c'est lent en raison de l'exploitation accrue des opérations d'entrée-sortie. Je propose donc une autre méthode : laissons le code inchangé et utilisons plusieurs macros pour traiter les données plus tard.

fread(&header, sizeof(struct MyFileHeader), 1, file);
CQ_NTOHL(header.version);
CQ_NTOHL_ARRAY(&header.box, 4); // box est une structure RECT

Si le boutisme de l'ordinateur ne correspond pas à celle du fichier de données, ces macros seront soient définies pour exécuter certaines fonctions, soient définies vides :

#if defined(ENDIAN_CONVERSION)
#    define CQ_NTOHL(a) {a = ((a) >> 24) | (((a) & 0xff0000) >> 8) |         (((a) & 0xff00) << 8) | ((a) << 24); }
#    define CQ_NTOHL_ARRAY(arr, num) {uint32 i;
for(i = 0; i < num; i++) {CQ_NTOHL(arr[i]); }}
#else
#    define CQ_NTOHL(a)
#    define CQ_NTOHL_ARRAY(arr, num)
#endif

Cette approche a l'avantage de ne pas perdre des cycles du processeur si ENDIAN_CONVERSION n'est pas défini, et le code peut être conservé dans sa forme naturelle de sorte à lire toute une structure à la fois.

IV-B - Conclusion

ARM et x86 possèdent une architecture et des ensembles d'instructions différentes, il y a donc des divergences dans les fonctionnalités de bas niveau. J'espère que les informations contenues dans cet article vont vous aider à surpasser ces différences lors du développement d'applications Android NDK pour plusieurs plates-formes.

Vous trouverez également d'autres outils pour optimiser les performances de vos applications sur l'Intel Developer Zone Android.

IV-C - À propos de l'auteur

Peng Tao (tao.peng@intel.com) est un ingénieur d'applications logicielles dans le groupe Intel Software and Services. Il se concentre actuellement sur les jeux et l'activation du multimédia sur les applications et l'optimisation des performances, en particulier sur les plates-formes mobiles Android.

IV-D - Ressources

Ressources Intel Developer Zone

Article original (Anglais)

Extra: From ARM NEON to Intel SSE- The Automatic Porting Solution, Tips and Tricks