web-dev-qa-db-fra.com

Trouver rapidement si une valeur est présente dans un tableau C?

J'ai une application intégrée avec un ISR à temps critique qui doit parcourir un tableau de taille 256 (de préférence 1024, mais 256 est le minimum) et vérifier si une valeur correspond au contenu des tableaux. Un bool sera mis à true si tel est le cas.

Le microcontrôleur est un NXP LPC4357, ARM Core Cortex M4, et le compilateur est GCC. J'ai déjà combiné le niveau d'optimisation 2 (3 est plus lent) et je place la fonction dans RAM au lieu de flash. J'utilise également l'arithmétique des pointeurs et une boucle for, qui effectue le décomptage au lieu de monter (vérifier si i!=0 est plus rapide que de vérifier si i<256). Dans l'ensemble, je me retrouve avec une durée de 12,5 µs qui doit être considérablement réduite pour être réalisable. Voici le (pseudo) code que j'utilise maintenant:

uint32_t i;
uint32_t *array_ptr = &theArray[0];
uint32_t compareVal = 0x1234ABCD;
bool validFlag = false;

for (i=256; i!=0; i--)
{
    if (compareVal == *array_ptr++)
    {
         validFlag = true;
         break;
     }
}

Quel serait le moyen le plus rapide pour ce faire? L'utilisation de l'assemblage en ligne est autorisée. D'autres astuces "moins élégantes" sont également autorisées.

124
wlamers

Dans les situations où les performances sont de la plus haute importance, le compilateur C ne produira probablement pas le code le plus rapide par rapport à ce que vous pouvez faire avec un langage d'assemblage réglé manuellement. J'ai tendance à prendre le chemin de la moindre résistance - pour les petites routines comme celle-ci, j'écris simplement du code asm et j'ai une bonne idée du nombre de cycles qu'il faudra pour exécuter. Vous pourrez peut-être jouer avec le code C et obtenir que le compilateur génère une bonne sortie, mais vous risquez de perdre beaucoup de temps à régler la sortie de cette façon. Les compilateurs (en particulier de Microsoft) ont parcouru un long chemin ces dernières années, mais ils ne sont toujours pas aussi intelligents que le compilateur entre vos oreilles parce que vous travaillez sur votre situation spécifique et pas seulement sur un cas général. Le compilateur peut ne pas utiliser certaines instructions (par exemple LDM) qui peuvent accélérer cela, et il est peu probable qu'il soit suffisamment intelligent pour dérouler la boucle. Voici une façon de le faire qui intègre les 3 idées que j'ai mentionnées dans mon commentaire: le déroulement de la boucle, la pré-lecture du cache et l'utilisation de l'instruction de chargement multiple (ldm). Le nombre de cycles d'instructions s'élève à environ 3 horloges par élément de tableau, mais cela ne prend pas en compte les retards de mémoire.

Théorie de fonctionnement: La conception du processeur d'ARM exécute la plupart des instructions en un cycle d'horloge, mais les instructions sont exécutées dans un pipeline. Les compilateurs C tenteront d'éliminer les retards de pipeline en entrelaçant d'autres instructions entre les deux. Lorsqu'il est présenté avec une boucle serrée comme le code C d'origine, le compilateur aura du mal à masquer les retards car la valeur lue dans la mémoire doit être immédiatement comparée. Mon code ci-dessous alterne entre 2 ensembles de 4 registres pour réduire considérablement les retards de la mémoire elle-même et le pipeline de récupération des données. En général, lorsque vous travaillez avec de grands ensembles de données et que votre code n'utilise pas la plupart ou la totalité des registres disponibles, vous n'obtenez pas de performances maximales.

; r0 = count, r1 = source ptr, r2 = comparison value

   stmfd sp!,{r4-r11}   ; save non-volatile registers
   mov r3,r0,LSR #3     ; loop count = total count / 8
   pld [r1,#128]
   ldmia r1!,{r4-r7}    ; pre load first set
loop_top:
   pld [r1,#128]
   ldmia r1!,{r8-r11}   ; pre load second set
   cmp r4,r2            ; search for match
   cmpne r5,r2          ; use conditional execution to avoid extra branch instructions
   cmpne r6,r2
   cmpne r7,r2
   beq found_it
   ldmia r1!,{r4-r7}    ; use 2 sets of registers to hide load delays
   cmp r8,r2
   cmpne r9,r2
   cmpne r10,r2
   cmpne r11,r2
   beq found_it
   subs r3,r3,#1        ; decrement loop count
   bne loop_top
   mov r0,#0            ; return value = false (not found)
   ldmia sp!,{r4-r11}   ; restore non-volatile registers
   bx lr                ; return
found_it:
   mov r0,#1            ; return true
   ldmia sp!,{r4-r11}
   bx lr

Mise à jour: Il y a beaucoup de sceptiques dans les commentaires qui pensent que mon expérience est anecdotique/sans valeur et nécessite une preuve. J'ai utilisé GCC 4.8 (à partir du Android NDK 9C) pour générer la sortie suivante avec l'optimisation -O2 (toutes les optimisations activées y compris le déroulement de la boucle). J'ai compilé l'original Code C présenté dans la question ci-dessus. Voici ce que GCC a produit:

.L9: cmp r3, r0
     beq .L8
.L3: ldr r2, [r3, #4]!
     cmp r2, r1
     bne .L9
     mov r0, #1
.L2: add sp, sp, #1024
     bx  lr
.L8: mov r0, #0
     b .L2

La sortie de GCC non seulement ne déroule pas la boucle, mais gaspille également une horloge sur un décrochage après le LDR. Il nécessite au moins 8 horloges par élément de tableau. Il fait un bon travail en utilisant l'adresse pour savoir quand sortir de la boucle, mais toutes les choses magiques que les compilateurs sont capables de faire ne se trouvent nulle part dans ce code. Je n'ai pas exécuté le code sur la plate-forme cible (je n'en possède pas), mais toute personne expérimentée dans les performances du code ARM peut voir que mon code est plus rapide.

pdate 2: J'ai donné à Visual Studio 2013 SP2 de Microsoft la possibilité de faire mieux avec le code. Il a pu utiliser les instructions NEON pour vectoriser l'initialisation de mon tableau, mais la recherche de valeur linéaire telle qu'écrite par l'OP est sortie similaire à ce que GCC a généré (j'ai renommé les étiquettes pour le rendre plus lisible):

loop_top:
   ldr  r3,[r1],#4  
   cmp  r3,r2  
   beq  true_exit
   subs r0,r0,#1 
   bne  loop_top
false_exit: xxx
   bx   lr
true_exit: xxx
   bx   lr

Comme je l'ai dit, je ne possède pas le matériel exact de l'OP, mais je vais tester les performances sur un nVidia Tegra 3 et Tegra 4 des 3 versions différentes et publier les résultats ici bientôt.

Mise à jour 3: J'ai exécuté mon code et Microsoft a compilé ARM sur un Tegra 3 et Tegra 4 (Surface RT, Surface RT 2) J'ai exécuté 1000000 itérations d'une boucle qui ne trouve pas de correspondance pour que tout soit en cache et facile à mesurer.

             My Code       MS Code
Surface RT    297ns         562ns
Surface RT 2  172ns         296ns  

Dans les deux cas, mon code s'exécute presque deux fois plus vite. La plupart des processeurs modernes ARM donneront probablement des résultats similaires.

104
BitBank

Il y a une astuce pour l'optimiser (on m'a posé cette question lors d'un entretien d'embauche une fois):

  • Si la dernière entrée du tableau contient la valeur que vous recherchez, retournez true
  • Écrivez la valeur que vous recherchez dans la dernière entrée du tableau
  • Itérer le tableau jusqu'à ce que vous rencontriez la valeur que vous recherchez
  • Si vous l'avez rencontré avant la dernière entrée du tableau, retournez true
  • Retour faux

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    uint32_t x = theArray[SIZE-1];
    if (x == compareVal)
        return true;
    theArray[SIZE-1] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    theArray[SIZE-1] = x;
    return i != SIZE-1;
}

Cela donne une branche par itération au lieu de deux branches par itération.


MISE À JOUR:

Si vous êtes autorisé à allouer le tableau à SIZE+1, vous pouvez alors vous débarrasser de la partie "échange de la dernière entrée":

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t i;
    theArray[SIZE] = compareVal;
    for (i = 0; theArray[i] != compareVal; i++);
    return i != SIZE;
}

Vous pouvez également vous débarrasser de l'arithmétique supplémentaire intégrée dans theArray[i], en utilisant à la place:

bool check(uint32_t theArray[], uint32_t compareVal)
{
    uint32_t *arrayPtr;
    theArray[SIZE] = compareVal;
    for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
    return arrayPtr != theArray+SIZE;
}

Si le compilateur ne l'applique pas déjà, cette fonction le fera à coup sûr. En revanche, il peut être plus difficile pour l'optimiseur de dérouler la boucle, vous devrez donc vérifier que dans le code d'assembly généré ...

87
barak manos

Vous demandez de l'aide pour optimiser votre algorithme, ce qui pourrait vous pousser à assembler. Mais votre algorithme (une recherche linéaire) n'est pas si intelligent, vous devriez donc envisager de changer votre algorithme. Par exemple.:

Fonction de hachage parfaite

Si vos 256 valeurs "valides" sont statiques et connues au moment de la compilation, alors vous pouvez utiliser une fonction de hachage parfaite . Vous devez trouver une fonction de hachage qui mappe votre valeur d'entrée à une valeur dans la plage 0 .. n , où il n'y en a pas collisions pour toutes les valeurs valides qui vous intéressent. C'est-à-dire qu'il n'y a pas deux valeurs "valides" hachées vers la même valeur de sortie. Lorsque vous recherchez une bonne fonction de hachage, vous visez à:

  • Gardez la fonction de hachage assez rapidement.
  • Réduisez n . Le plus petit que vous pouvez obtenir est 256 (fonction de hachage parfaite minimale), mais c'est probablement difficile à réaliser, selon les données.

Remarque pour les fonctions de hachage efficaces, n est souvent une puissance de 2, ce qui équivaut à un masque au niveau des bits de bits bas (opération AND). Exemples de fonctions de hachage:

  • CRC d'octets d'entrée, modulo n .
  • ((x << i) ^ (x >> j) ^ (x << k) ^ ...) % n (En sélectionnant autant de i, j, k, ... au besoin, avec des décalages vers la gauche ou la droite)

Ensuite, vous créez un tableau fixe de n entrées, où le hachage mappe les valeurs d'entrée à un index i dans le tableau. Pour les valeurs valides, l'entrée de table i contient la valeur valide. Pour toutes les autres entrées de table, assurez-vous que chaque entrée d'index i contient une autre valeur non valide qui ne doit pas être hachée ( i .

Puis dans votre routine d'interruption, avec entrée x :

  1. Hachage x pour indexer i (qui est dans la plage 0 .. n)
  2. Recherchez l'entrée i dans le tableau et voyez si elle contient la valeur x .

Ce sera beaucoup plus rapide qu'une recherche linéaire de 256 ou 1024 valeurs.

J'ai écrit du Python pour trouver des fonctions de hachage raisonnables.

Recherche binaire

Si vous triez votre tableau de 256 valeurs "valides", vous pouvez alors faire une recherche binaire , plutôt qu'une recherche linéaire. Cela signifie que vous devriez pouvoir rechercher une table à 256 entrées en seulement 8 étapes (log2(256)), ou une table à 1024 entrées en 10 étapes. Encore une fois, ce sera beaucoup plus rapide qu'une recherche linéaire de 256 ou 1024 valeurs.

62
Craig McQueen

Gardez la table dans l'ordre trié et utilisez la recherche binaire déroulée de Bentley:

i = 0;
if (key >= a[i+512]) i += 512;
if (key >= a[i+256]) i += 256;
if (key >= a[i+128]) i += 128;
if (key >= a[i+ 64]) i +=  64;
if (key >= a[i+ 32]) i +=  32;
if (key >= a[i+ 16]) i +=  16;
if (key >= a[i+  8]) i +=   8;
if (key >= a[i+  4]) i +=   4;
if (key >= a[i+  2]) i +=   2;
if (key >= a[i+  1]) i +=   1;
return (key == a[i]);

Le fait est,

  • si vous savez quelle est la taille de la table, alors vous savez combien d'itérations il y aura, afin que vous puissiez la dérouler complètement.
  • Ensuite, il n'y a aucun point à tester le cas == À chaque itération car, sauf à la dernière itération, la probabilité de ce cas est trop faible pour justifier de passer du temps à le tester. **
  • Enfin, en étendant la table à une puissance de 2, vous ajoutez au plus une comparaison et au plus un facteur de deux stockage.

** Si vous n'êtes pas habitué à penser en termes de probabilités, chaque point de décision a un entropie, qui est l'information moyenne que vous apprenez en l'exécutant. Pour les tests >=, La probabilité de chaque branche est d'environ 0,5 et -log2 (0,5) est 1, ce qui signifie que si vous prenez une branche, vous apprenez 1 bit, et si vous prenez l'autre branche, vous apprenez un bit, et la moyenne est juste la somme de ce que vous apprenez sur chaque branche fois la probabilité de cette branche. Donc 1*0.5 + 1*0.5 = 1, Donc l'entropie du test >= Est 1. Puisque vous avez 10 bits à apprendre, cela prend 10 branches. Voilà pourquoi c'est rapide!

D'un autre côté, que faire si votre premier test est if (key == a[i+512)? La probabilité d'être vrai est de 1/1024, tandis que la probabilité de faux est de 1023/1024. Donc, si c'est vrai, vous apprenez les 10 bits! Mais si c'est faux, vous apprenez -log2 (1023/1024) = .00141 bits, pratiquement rien! Ainsi, la quantité moyenne que vous apprenez de ce test est de 10/1024 + .00141*1023/1024 = .0098 + .00141 = .0112 Bits. Environ un centième de bit. Ce test est ne portant pas son poids!

60
Mike Dunlavey

Si l'ensemble des constantes de votre table est connu à l'avance, vous pouvez utiliser hachage parfait pour vous assurer qu'un seul accès est fait à la table. Le hachage parfait détermine une fonction de hachage qui mappe chaque clé intéressante sur un emplacement unique (cette table n'est pas toujours dense, mais vous pouvez décider de la densité d'une table que vous pouvez vous permettre, avec des tables moins denses conduisant généralement à des fonctions de hachage plus simples).

Habituellement, la fonction de hachage parfaite pour le jeu de clés spécifique est relativement facile à calculer; vous ne voulez pas que cela soit long et compliqué parce que cela concurrence le temps peut-être mieux passé à faire plusieurs sondes.

Le hachage parfait est un schéma "1 sonde max". On peut généraliser l'idée, en pensant qu'il faut échanger la simplicité de calcul du code de hachage avec le temps qu'il faut pour faire k sondes. Après tout, l'objectif est "le moins de temps total pour rechercher", pas le moins de sondes ou la fonction de hachage la plus simple. Cependant, je n'ai jamais vu personne construire un algorithme de hachage k-probes-max. Je soupçonne qu'on peut le faire, mais c'est probablement de la recherche.

Une autre pensée: si votre processeur est extrêmement rapide, la seule sonde à la mémoire à partir d'un hachage parfait domine probablement le temps d'exécution. Si le processeur n'est pas très rapide, alors k> 1 sondes peuvent être pratiques.

16
Ira Baxter

Utilisez un ensemble de hachage. Cela donnera O(1) temps de recherche.

Le code suivant suppose que vous pouvez réserver une valeur 0 en tant que valeur 'vide', c'est-à-dire ne se produisant pas dans les données réelles. La solution peut être étendue pour une situation où ce n'est pas le cas.

#define HASH(x) (((x >> 16) ^ x) & 1023)
#define HASH_LEN 1024
uint32_t my_hash[HASH_LEN];

int lookup(uint32_t value)
{
    int i = HASH(value);
    while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
    return i;
}

void store(uint32_t value)
{
    int i = lookup(value);
    if (my_hash[i] == 0)
       my_hash[i] = value;
}

bool contains(uint32_t value)
{
    return (my_hash[lookup(value)] == value);
}

Dans cet exemple d'implémentation, le temps de recherche sera généralement très faible, mais au pire des cas peut aller jusqu'au nombre d'entrées stockées. Pour une application en temps réel, vous pouvez également envisager une implémentation utilisant des arbres binaires, qui aura un temps de recherche plus prévisible.

14
jpa

Dans ce cas, il pourrait être utile d'étudier filtres Bloom . Ils sont capables d'établir rapidement qu'aucune valeur n'est présente, ce qui est une bonne chose car la plupart des 2 ^ 32 valeurs possibles ne se trouvent pas dans ce tableau de 1024 éléments. Cependant, certains faux positifs nécessiteront une vérification supplémentaire.

Étant donné que votre table est apparemment statique, vous pouvez déterminer quels faux positifs existent pour votre filtre Bloom et les mettre dans un hachage parfait.

9
MSalters

En supposant que votre processeur fonctionne à 204 MHz, ce qui semble être le maximum pour le LPC4357, et en supposant également que votre résultat de synchronisation reflète le cas moyen (la moitié de la matrice traversée), nous obtenons:

  • Fréquence du processeur: 204 MHz
  • Période de cycle: 4,9 ns
  • Durée en cycles: 12,5 µs/4,9 ns = 2551 cycles
  • Cycles par itération: 2551/128 = 19,9

Ainsi, votre boucle de recherche passe environ 20 cycles par itération. Cela ne semble pas terrible, mais je suppose que pour l'accélérer, vous devez examiner l'Assemblée.

Je recommanderais de supprimer l'index et d'utiliser une comparaison de pointeurs à la place, et de faire tous les pointeurs const.

bool arrayContains(const uint32_t *array, size_t length)
{
  const uint32_t * const end = array + length;
  while(array != end)
  {
    if(*array++ == 0x1234ABCD)
      return true;
  }
  return false;
}

Cela vaut au moins la peine d'être testé.

8
unwind

D'autres personnes ont suggéré de réorganiser votre table, d'ajouter une valeur sentinelle à la fin ou de la trier afin de fournir une recherche binaire.

Vous dites "J'utilise également l'arithmétique du pointeur et une boucle for, qui effectue le décomptage au lieu de monter (vérifier si i != 0 est plus rapide que de vérifier si i < 256). "

Mon premier conseil est de se débarrasser de l'arithmétique des pointeurs et du décomptage. Choses comme

for (i=0; i<256; i++)
{
    if (compareVal == the_array[i])
    {
       [...]
    }
}

tend à être idiomatique pour le compilateur. La boucle est idiomatique et l'indexation d'un tableau sur une variable de boucle est idiomatique. Jongler avec l'arithmétique des pointeurs et les pointeurs aura tendance à obscurcir les idiomes du compilateur et à lui faire générer du code lié à ce que vous a écrit plutôt que ce que le rédacteur du compilateur a décidé d'être le meilleur cours pour le général tâche.

Par exemple, le code ci-dessus peut être compilé dans une boucle exécutée à partir de -256 ou -255 à zéro, indexation désactivée &the_array[256]. Peut-être des choses qui ne sont même pas exprimables en C valide mais qui correspondent à l'architecture de la machine pour laquelle vous générez.

Donc ne pas microoptimiser. Vous jetez simplement des clés dans les travaux de votre optimiseur. Si vous voulez être intelligent, travaillez sur les structures de données et les algorithmes mais ne microoptimisez pas leur expression. Il reviendra juste pour vous mordre, sinon sur le compilateur/l'architecture actuel, puis sur le suivant.

En particulier, l'utilisation de l'arithmétique des pointeurs au lieu des tableaux et des index est un poison pour le compilateur étant pleinement conscient des alignements, des emplacements de stockage, des considérations d'alias et d'autres choses, et pour effectuer des optimisations telles que la réduction de la force de la manière la mieux adaptée à l'architecture de la machine.

6
user4015204

La vectorisation peut être utilisée ici, comme c'est souvent le cas dans les implémentations de memchr. Vous utilisez l'algorithme suivant:

  1. Créez un masque de répétition de votre requête, de longueur égale au nombre de bits de votre système d'exploitation (64 bits, 32 bits, etc.). Sur un système 64 bits, vous répéteriez la requête 32 bits deux fois.

  2. Traitez la liste comme une liste de plusieurs éléments de données à la fois, simplement en transtypant la liste en une liste d'un type de données plus grand et en extrayant des valeurs. Pour chaque morceau, XOR avec le masque, puis XOR avec 0b0111 ... 1, puis ajoutez 1, puis & avec un masque de 0b1000 .. .0 répétition. Si le résultat est 0, il n'y a certainement pas de correspondance. Sinon, il peut y avoir (généralement avec une probabilité très élevée) une correspondance, alors recherchez le morceau normalement.

Exemple d'implémentation: https://sourceware.org/cgi-bin/cvsweb.cgi/src/newlib/libc/string/memchr.c?rev=1.3&content-type=text/x-cvsweb-markup&cvsroot= src

3
meisel

Si vous pouvez adapter le domaine de vos valeurs avec le quantité de mémoire disponible à votre application, alors la solution la plus rapide serait de représenter votre tableau comme un tableau de bits:

bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
uint32_t compareVal = 0x1234ABCD;
bool validFlag = theArray[compareVal];

MODIFIER

Je suis stupéfait par le nombre de critiques. Le titre de ce fil est "Comment puis-je trouver rapidement si une valeur est présente dans un tableau C?" pour lequel je maintiendrai ma réponse car elle répond précisément à cela. Je pourrais affirmer que cela a la fonction de hachage la plus efficace en termes de vitesse (depuis l'adresse === valeur). J'ai lu les commentaires et je suis conscient des mises en garde évidentes. Sans aucun doute, ces mises en garde limitent la gamme de problèmes que cela peut être utilisé pour résoudre, mais, pour les problèmes qu'il résout, il résout très efficacement.

Plutôt que de rejeter cette réponse, considérez-la comme le point de départ optimal pour lequel vous pouvez évoluer en utilisant des fonctions de hachage pour atteindre un meilleur équilibre entre vitesse et performances.

3
Stephen Quan

Assurez-vous que les instructions ("le pseudo code") et les données ("theArray") sont dans des mémoires distinctes (RAM) afin que l'architecture CM4 Harvard soit utilisée à son plein potentiel. Du manuel d'utilisation:

enter image description here

Pour optimiser les performances du processeur, le ARM Cortex-M4 dispose de trois bus pour l'accès Instruction (code) (I), l'accès Data (D) et l'accès System (S). Lorsque les instructions et les données sont conservés dans des mémoires séparées, les accès au code et aux données peuvent être effectués en parallèle en un seul cycle. Lorsque le code et les données sont conservés dans la même mémoire, les instructions de chargement ou de stockage des données peuvent prendre deux cycles.

1
francek

Je suis désolé si ma réponse a déjà été répondue - je suis juste un lecteur paresseux. Sentez-vous libre de voter ensuite))

1) vous pouvez supprimer le compteur "i" - comparez simplement les pointeurs, c.-à-d.

for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
{
    if (compareVal == *ptr)
    {
       break;
    }
}
... compare ptr and the_array+1024 here - you do not need validFlag at all.

tout cela ne donnera cependant aucune amélioration significative, une telle optimisation pourrait probablement être réalisée par le compilateur lui-même.

2) Comme cela a déjà été mentionné dans d'autres réponses, presque tous les processeurs modernes sont basés sur RISC, par exemple ARM. Même les processeurs Intel X86 modernes utilisent des cœurs RISC à l'intérieur, pour autant que je sache (compilation à partir de X86 à la volée). L'optimisation majeure pour RISC est l'optimisation du pipeline (et pour Intel et les autres processeurs également), minimisant les sauts de code. Un type d'optimisation de ce type (probablement majeur) est le "cycle rollback". C'est incroyablement stupide et efficace, même le compilateur Intel peut le faire AFAIK. Ça ressemble à:

if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
...and so on...
end_of_compare:

De cette façon, l'optimisation est que le pipeline n'est pas rompu dans le pire des cas (si compareVal est absent dans le tableau), il est donc aussi rapide que possible (bien sûr sans compter les optimisations d'algorithmes telles que les tables de hachage, les tableaux triés, etc.) mentionné dans d'autres réponses, ce qui peut donner de meilleurs résultats en fonction de la taille de la matrice. L'approche Cycles Rollback peut également y être appliquée d'ailleurs. J'écris ici à ce sujet, je pense que je ne l'ai pas vu dans d'autres)

La deuxième partie de cette optimisation est que cet élément de tableau est pris par adresse directe (calculé au stade de la compilation, assurez-vous d'utiliser un tableau statique), et n'a pas besoin d'opération ADD supplémentaire pour calculer le pointeur à partir de l'adresse de base du tableau. Cette optimisation peut ne pas avoir d'effet significatif, car l'architecture AFAIK ARM a des fonctionnalités spéciales pour accélérer l'adressage des tableaux. Mais de toute façon, il est toujours préférable de savoir que vous avez fait tout ce que vous avez de mieux en code C directement, à droite ?

Le cycle de restauration peut sembler gênant en raison du gaspillage de ROM (oui, vous avez bien placé ce dernier sur une partie rapide de la RAM, si votre carte prend en charge cette fonctionnalité), mais en fait, c'est un juste prix pour la vitesse, étant basé sur le concept RISC. Ce n'est qu'un point général d'optimisation du calcul - vous sacrifiez de l'espace pour gagner en vitesse, et vice versa, selon vos besoins.

Si vous pensez que la restauration d'un tableau de 1024 éléments est un sacrifice trop important pour votre cas, vous pouvez envisager un `` retour en arrière partiel '', par exemple en divisant le tableau en 2 parties de 512 éléments chacune, ou 4x256, et ainsi de suite.

3) les processeurs modernes prennent souvent en charge les opérations SIMD, par exemple ARM NEON instruction set - il permet d'exécuter les mêmes opérations en parallèle. Franchement, je ne me souviens pas s'il convient aux opérations de comparaison, mais Je pense que cela peut être, vous devriez vérifier cela. La recherche sur Google montre qu'il peut également y avoir des astuces pour obtenir la vitesse maximale, voir https://stackoverflow.com/a/5734019/1028256

J'espère que cela pourra vous donner de nouvelles idées.

0
Mixaz

Je suis un grand fan de hachage. Le problème est bien sûr de trouver un algorithme efficace qui soit à la fois rapide et utilise un minimum de mémoire (notamment sur un processeur embarqué).

Si vous connaissez à l'avance les valeurs qui peuvent se produire, vous pouvez créer un programme qui exécute une multitude d'algorithmes pour trouver le meilleur - ou plutôt les meilleurs paramètres pour vos données.

J'ai créé un tel programme que vous pouvez lire dans ce post et j'ai obtenu des résultats très rapides. 16000 entrées se traduisent approximativement par 2 ^ 14 ou une moyenne de 14 comparaisons pour trouver la valeur à l'aide d'une recherche binaire. Je visais explicitement des recherches très rapides - en moyenne, trouver la valeur dans <= 1,5 recherches - ce qui entraînait une plus grande RAM exigences. Je pense qu'avec une valeur moyenne plus conservatrice (disons <= 3) En comparaison, le cas moyen d'une recherche binaire sur vos 256 ou 1024 entrées entraînerait un nombre moyen de comparaisons de 8 et 10, respectivement.

Ma recherche moyenne a nécessité environ 60 cycles (sur un ordinateur portable avec un Intel i5) avec un algorithme générique (utilisant une division par une variable) et 40 à 45 cycles avec un spécialisé (utilisant probablement une multiplication). Cela devrait se traduire par des temps de recherche inférieurs à la microseconde sur votre MCU, en fonction bien sûr de la fréquence d'horloge à laquelle il s'exécute.

Il peut être modifié davantage dans la réalité si le tableau d'entrées garde une trace du nombre de fois où une entrée a été consultée. Si le tableau d'entrée est trié du plus grand au moins accessible avant que les indéces ne soient calculés, il trouvera les valeurs les plus courantes avec une seule comparaison.

0
Olof Forshell

Cela ressemble plus à un addendum qu'à une réponse.

J'ai eu un cas similaire dans le passé, mais mon tableau était constant sur un nombre considérable de recherches.

Dans la moitié d'entre eux, la valeur recherchée n'était PAS présente dans le tableau. Puis j'ai réalisé que je pouvais appliquer un "filtre" avant de faire une recherche.

Ce "filtre" n'est qu'un simple nombre entier, calculé UNE FOIS et utilisé dans chaque recherche.

C'est en Java, mais c'est assez simple:

binaryfilter = 0;
for (int i = 0; i < array.length; i++)
{
    // just apply "Binary OR Operator" over values.
    binaryfilter = binaryfilter | array[i];
}

Donc, avant de faire une recherche binaire, je vérifie le filtre binaire:

// Check binaryfilter vs value with a "Binary AND Operator"
if ((binaryfilter & valuetosearch) != valuetosearch)
{
    // valuetosearch is not in the array!
    return false;
}
else
{
    // valuetosearch MAYBE in the array, so let's check it out
    // ... do binary search stuff ...

}

Vous pouvez utiliser un "meilleur" algorithme de hachage, mais cela peut être très rapide, spécialement pour les grands nombres. Cela pourrait vous faire économiser encore plus de cycles.

0
Christian