Y a-t-il une optimisation possible pour l'accès aléatoire sur un très grand tableau (j'utilise actuellement uint8_t
, et je demande ce qui est mieux)
uint8_t MyArray[10000000];
lorsque la valeur à n'importe quelle position dans le tableau est
- 0 ou 1 pour 95% de tous les cas,
- 2 dans 4% des cas,
- entre 3 et 255 dans l'autre 1% des cas?
Alors, y a-t-il quelque chose de mieux qu'un uint8_t
tableau à utiliser pour cela? Il devrait être aussi rapide que possible de boucler sur l'ensemble de la matrice dans un ordre aléatoire, et cela est très lourd sur la bande passante de la RAM, donc lorsque plus de quelques threads font cela en même temps pour différentes baies, actuellement toute la bande passante de la RAM est vite saturé.
Je demande car il semble très inefficace d'avoir un si grand tableau (10 Mo) quand on sait en fait que presque toutes les valeurs, à l'exception de 5%, seront soit 0 ou 1. Donc, lorsque 95% de toutes les valeurs du tableau n'aurait en fait besoin que de 1 bit au lieu de 8 bits, ce qui réduirait l'utilisation de la mémoire de presque un ordre de grandeur. On a l'impression qu'il doit y avoir une solution plus efficace en mémoire qui réduirait considérablement la bande passante RAM requise pour cela et, par conséquent, serait également beaucoup plus rapide pour un accès aléatoire.
Réponses:
Une possibilité simple qui vient à l'esprit est de conserver un tableau compressé de 2 bits par valeur pour les cas courants, et un tableau séparé de 4 octets par valeur (24 bits pour l'index de l'élément d'origine, 8 bits pour la valeur réelle, donc
(idx << 8) | value)
) un tableau trié pour le autres.Lorsque vous recherchez une valeur, vous effectuez d'abord une recherche dans le tableau 2bpp (O (1)); si vous trouvez 0, 1 ou 2, c'est la valeur que vous voulez; si vous trouvez 3, cela signifie que vous devez le rechercher dans le tableau secondaire. Ici, vous effectuerez une recherche binaire pour rechercher l' indice de votre intérêt décalé à gauche de 8 (O (log (n) avec un petit n, car cela devrait être le 1%), et extrayez la valeur de 4- byte thingie.
Pour un tableau tel que celui que vous avez proposé, cela devrait prendre 10 000 000/4 = 2 500 000 octets pour le premier tableau, plus 10 000 000 * 1% * 4 B = 400 000 octets pour le deuxième tableau; d'où 2900000 octets, c'est-à-dire moins d'un tiers du tableau d'origine, et la partie la plus utilisée est gardée ensemble en mémoire, ce qui devrait être bon pour la mise en cache (elle peut même tenir L3).
Si vous avez besoin d'un adressage supérieur à 24 bits, vous devrez modifier le "stockage secondaire"; une manière simple de l'étendre est d'avoir un tableau de pointeurs de 256 éléments pour basculer sur les 8 premiers bits de l'index et transmettre à un tableau trié indexé 24 bits comme ci-dessus.
Benchmark rapide
(code et données toujours mis à jour dans mon Bitbucket)
Le code ci-dessus remplit un tableau d'éléments 10M avec des données aléatoires distribuées comme OP spécifié dans leur message, initialise ma structure de données, puis:
(notez qu'en cas de recherche séquentielle, le tableau gagne toujours par une énorme mesure, car c'est la recherche la plus conviviale pour le cache que vous puissiez faire)
Ces deux derniers blocs sont répétés 50 fois et chronométrés; à la fin, la moyenne et l'écart type pour chaque type de recherche sont calculés et imprimés, avec l'accélération (lookup_mean / array_mean).
J'ai compilé le code ci-dessus avec g ++ 5.4.0 (
-O3 -static
, plus quelques avertissements) sur Ubuntu 16.04, et l' ai exécuté sur certaines machines; la plupart utilisent Ubuntu 16.04, certains Linux plus anciens, certains Linux plus récents. Je ne pense pas que le système d'exploitation devrait être pertinent du tout dans ce cas.Les résultats sont ... mitigés!
la source
uint32_t
sera bien. Effacer un élément du tampon secondaire le laissera évidemment trié. L'insertion d'un élément peut se faire avecstd::lower_bound
and theninsert
(plutôt que d'ajouter et de re-trier le tout). Les mises à jour rendent le tableau secondaire pleine taille beaucoup plus attrayant - je commencerais certainement par cela.(idx << 8) + val
vous n'avez pas à vous soucier de la partie valeur, utilisez simplement une comparaison directe. Il comparera toujours moins((idx+1) << 8) + val
et moins que((idx-1) << 8) + val
populate
fonction qui devrait peuplermain_arr
etsec_arr
selon le format que l'onlookup
attend. Je ne l'ai pas vraiment essayé, alors ne vous attendez pas à ce qu'il fonctionne vraiment correctement :-); quoi qu'il en soit, cela devrait vous donner une idée générale.Une autre option pourrait être
En d'autres termes, quelque chose comme:
où
bmap
utilise 2 bits par élément avec la valeur 3 signifiant «autre».Cette structure est simple à mettre à jour, utilise 25% de mémoire en plus mais la grande partie n'est recherchée que dans 5% des cas. Bien sûr, comme d'habitude, si c'est une bonne idée ou non, cela dépend de beaucoup d'autres conditions, donc la seule réponse est d'expérimenter un usage réel.
la source
if(code != 3) return code;
enif(code == 0) return 0; if(code==1) return 1; if(code == 2) return 2;
__builtin_expect
& co ou PGO peuvent également vous aider.C'est plus un "long commentaire" qu'une réponse concrète
À moins que vos données ne soient quelque chose de bien connu, je doute que quiconque puisse répondre DIRECTEMENT à votre question (et je ne suis au courant de rien qui correspond à votre description, mais alors je ne sais pas TOUT sur toutes sortes de modèles de données pour tous types de cas d'utilisation). Les données éparses sont un problème courant dans le calcul haute performance, mais c'est typiquement "nous avons un très grand tableau, mais seules certaines valeurs sont non nulles".
Pour les modèles pas bien connus comme ce que je pense être le vôtre, personne ne saura directement ce qui est le meilleur, et cela dépend des détails: à quel point l'accès aléatoire est-il aléatoire - le système accède-t-il à des grappes d'éléments de données, ou est-il complètement aléatoire comme à partir de un générateur de nombres aléatoires uniforme. Les données de la table sont-elles complètement aléatoires ou y a-t-il des séquences de 0 puis des séquences de 1, avec une dispersion d'autres valeurs? L'encodage de longueur d'exécution fonctionnerait bien si vous avez des séquences raisonnablement longues de 0 et 1, mais ne fonctionnera pas si vous avez un "damier de 0/1". De plus, vous devrez conserver un tableau des "points de départ", afin que vous puissiez vous rendre à l'endroit pertinent assez rapidement.
Je sais depuis longtemps que certaines grandes bases de données ne sont qu'une grande table en RAM (données d'abonné du central téléphonique dans cet exemple), et l'un des problèmes est que les caches et les optimisations de tables de pages dans le processeur sont assez inutiles. L'appelant est si rarement le même que celui qui a récemment appelé quelqu'un, qu'il n'y a pas de données pré-chargées d'aucune sorte, c'est purement aléatoire. Les grands tableaux de pages sont la meilleure optimisation pour ce type d'accès.
Dans de nombreux cas, faire un compromis entre «vitesse et petite taille» est l'une de ces choses que vous devez choisir entre l'ingénierie logicielle [dans d'autres ingénieurs, ce n'est pas nécessairement un compromis]. Ainsi, "gaspiller de la mémoire pour un code plus simple" est souvent le choix préféré. En ce sens, la solution "simple" est probablement meilleure pour la vitesse, mais si vous avez une "meilleure" utilisation de la RAM, l'optimisation de la taille de la table vous donnerait des performances suffisantes et une bonne amélioration de la taille. Il existe de nombreuses façons différentes d'y parvenir - comme suggéré dans un commentaire, un champ de 2 bits où les deux ou trois valeurs les plus courantes sont stockées, puis un autre format de données pour les autres valeurs - une table de hachage serait mon première approche, mais une liste ou un arbre binaire peut également fonctionner - encore une fois, cela dépend des modèles où se trouvent vos "pas 0, 1 ou 2". Encore une fois, cela dépend de la façon dont les valeurs sont «dispersées» dans le tableau - sont-elles en grappes ou sont-elles plus uniformément réparties?
Mais un problème avec cela est que vous lisez toujours les données de la RAM. Vous dépensez alors plus de code pour traiter les données, y compris du code pour faire face au "ce n'est pas une valeur commune".
Le problème avec les algorithmes de compression les plus courants est qu'ils sont basés sur des séquences de décompression, vous ne pouvez donc pas y accéder de manière aléatoire. Et la surcharge de fractionner vos données volumineuses en morceaux de, par exemple, 256 entrées à la fois, et de décompresser les 256 dans un tableau uint8_t, de récupérer les données que vous voulez, puis de jeter vos données non compressées, est très peu susceptible de vous donner du bon. performances - en supposant que cela ait une certaine importance, bien sûr.
En fin de compte, vous devrez probablement implémenter une ou plusieurs des idées dans les commentaires / réponses pour tester, voir si cela aide à résoudre votre problème, ou si le bus mémoire est toujours le principal facteur limitant.
la source
uint8_t
tableau, la bande passante RAM est saturée après ~ 5 threads travaillent dessus en même temps (sur un système à quatre canaux), donc utiliser plus de 5 threads ne donne plus aucun avantage. Je voudrais que cela utilise> 10 threads sans rencontrer de problèmes de bande passante RAM, mais si le côté CPU de l'accès devient si lent que 10 threads sont moins exécutés que 5 threads auparavant, ce ne serait évidemment pas un progrès.Ce que j'ai fait dans le passé, c'est d'utiliser un hashmap devant un ensemble de bits.
Cela divise par deux l'espace par rapport à la réponse de Matteo, mais peut être plus lent si les recherches "d'exception" sont lentes (c'est-à-dire qu'il existe de nombreuses exceptions).
Souvent, cependant, «le cache est roi».
la source
0
moyens regardentmain_arr
et1
moyens regarder lesec_arr
(dans le cas du code Matteos)? Cela nécessiterait globalement plus d'espace que la réponse de Matteos, car il s'agit d'un tableau supplémentaire. Je ne comprends pas très bien comment vous le feriez en utilisant seulement la moitié de l'espace par rapport à la réponse de Matteos.À moins qu'il n'y ait un modèle dans vos données, il est peu probable qu'il y ait une optimisation raisonnable de la vitesse ou de la taille, et - en supposant que vous ciblez un ordinateur normal - 10 Mo n'est pas si grave de toute façon.
Il y a deux hypothèses dans vos questions:
Je pense que ces deux hypothèses sont fausses. Dans la plupart des cas, la manière appropriée de stocker des données est de stocker la représentation la plus naturelle. Dans votre cas, c'est celui que vous avez choisi: un octet pour un nombre compris entre 0 et 255. Toute autre représentation sera plus complexe et donc - toutes choses égales par ailleurs - plus lente et plus sujette aux erreurs. Pour avoir besoin de détourner de ce principe général, vous avez besoin d'une raison plus forte que potentiellement six bits "gaspillés" sur 95% de vos données.
Pour votre deuxième hypothèse, ce sera vrai si, et seulement si, la modification de la taille de la matrice entraîne beaucoup moins d'erreurs de cache. Que cela se produise ne peut être définitivement déterminé que par le profilage du code de travail, mais je pense qu'il est très peu probable que cela fasse une différence substantielle. Étant donné que vous accéderez de manière aléatoire au tableau dans les deux cas, le processeur aura du mal à savoir quels bits de données mettre en cache et conserver dans les deux cas.
la source
Si les données et les accès sont uniformément distribués de manière aléatoire, les performances dépendront probablement de la fraction des accès qui évite un échec du cache de niveau externe. Pour optimiser cela, il faudra savoir quelle taille de tableau peut être logée de manière fiable dans le cache. Si votre cache est suffisamment grand pour accueillir un octet pour cinq cellules, l'approche la plus simple peut être d'avoir un octet contenant les cinq valeurs codées en base trois dans la plage 0-2 (il y a 243 combinaisons de 5 valeurs, de sorte que place dans un octet), avec un tableau de 10 000 000 octets qui serait interrogé chaque fois qu'une valeur de base 3 indique «2».
Si le cache n'est pas si grand, mais peut accueillir un octet par 8 cellules, il ne serait pas possible d'utiliser une valeur d'octet pour sélectionner parmi les 6561 combinaisons possibles de huit valeurs de base 3, mais puisque le seul effet de changer un 0 ou 1 en un 2 entraînerait une recherche autrement inutile, l'exactitude ne nécessiterait pas de prendre en charge les 6 561. Au lieu de cela, on pourrait se concentrer sur les 256 valeurs les plus «utiles».
Surtout si 0 est plus courant que 1, ou vice versa, une bonne approche pourrait être d'utiliser 217 valeurs pour encoder les combinaisons de 0 et 1 qui contiennent 5 ou moins de 1, 16 valeurs pour encoder xxxx0000 à xxxx1111, 16 pour encoder 0000xxxx à travers 1111xxxx et un pour xxxxxxxx. Quatre valeurs resteraient pour toute autre utilisation que l'on pourrait trouver. Si les données sont distribuées aléatoirement comme décrit, une légère majorité de toutes les requêtes toucheraient des octets qui ne contenaient que des zéros et des uns (dans environ 2/3 de tous les groupes de huit, tous les bits seraient des zéros et des uns, et environ 7/8 de ceux-ci auraient six bits ou moins 1); la grande majorité de ceux qui ne le font pas atterriraient dans un octet contenant quatre x et auraient 50% de chances d'atterrir sur un zéro ou un un. Ainsi, seulement environ une requête sur quatre nécessiterait une recherche sur un grand tableau.
Si les données sont distribuées aléatoirement mais que le cache n'est pas assez grand pour gérer un octet pour huit éléments, on pourrait essayer d'utiliser cette approche avec chaque octet gérant plus de huit éléments, mais à moins qu'il n'y ait un fort biais vers 0 ou vers 1 , la fraction des valeurs qui peuvent être gérées sans avoir à faire une recherche dans le grand tableau diminuera à mesure que le nombre géré par chaque octet augmentera.
la source
J'ajouterai à la réponse de @ o11c , car son libellé peut être un peu déroutant. Si j'ai besoin de presser le dernier bit et le cycle du processeur, je ferais ce qui suit.
Nous commencerons par construire un arbre de recherche binaire équilibré contenant les 5% de cas «autre chose». Pour chaque recherche, vous parcourez rapidement l'arborescence: vous avez 10 000 000 éléments dont 5% dans l'arborescence: la structure de données arborescente contient donc 500 000 éléments. Marcher ceci en temps O (log (n)), vous donne 19 itérations. Je ne suis pas un expert en la matière, mais je suppose qu'il existe des implémentations économes en mémoire. Faisons une estimation:
Total, 4 octets: 500000 * 4 = 1953 ko. Convient au cache!
Pour tous les autres cas (0 ou 1), vous pouvez utiliser un bitvector. Notez que vous ne pouvez pas omettre les 5% autres cas d'accès aléatoire: 1,19 Mo.
La combinaison de ces deux utilise environ 3 099 Mo. En utilisant cette technique, vous économiserez un facteur 3,08 de mémoire.
Cependant, cela ne bat pas la réponse de @Matteo Italia (qui utilise 2,76 Mo), dommage. Y a-t-il quelque chose que nous pouvons faire de plus? La partie la plus consommatrice de mémoire est constituée des 3 octets d'index dans l'arborescence. Si nous pouvons ramener cela à 2, nous économiserions 488 Ko et l'utilisation totale de la mémoire serait de: 2,622 Mo, ce qui est plus petit!
Comment faisons-nous cela? Nous devons réduire l'indexation à 2 octets. Encore une fois, 10000000 prend 23 bits. Nous devons pouvoir supprimer 7 bits. Nous pouvons simplement le faire en partitionnant la plage de 10000000 éléments en 2 ^ 7 (= 128) régions de 78125 éléments. Nous pouvons maintenant construire un arbre équilibré pour chacune de ces régions, avec 3906 éléments en moyenne. Le choix du bon arbre se fait par une simple division de l'index cible par 2 ^ 7 (ou un décalage de bits
>> 7
). L'index requis à stocker peut maintenant être représenté par les 16 bits restants. Notez qu'il y a une surcharge pour la longueur de l'arbre qui doit être stockée, mais c'est négligeable. Notez également que ce mécanisme de fractionnement réduit le nombre d'itérations nécessaires pour parcourir l'arbre, cela se réduit désormais à 7 itérations de moins, car nous avons laissé tomber 7 bits: il ne reste que 12 itérations.Notez que vous pourriez théoriquement répéter le processus pour couper les 8 bits suivants, mais cela vous obligerait à créer 2 ^ 15 arbres équilibrés, avec ~ 305 éléments en moyenne. Cela donnerait 2,143 Mo, avec seulement 4 itérations pour parcourir l'arbre, ce qui représente une accélération considérable par rapport aux 19 itérations avec lesquelles nous avons commencé.
En guise de conclusion finale: cela bat la stratégie vectorielle 2 bits par un tout petit peu d'utilisation de la mémoire, mais c'est tout un combat à mettre en œuvre. Mais si cela peut faire la différence entre l'installation du cache ou non, cela vaut peut-être la peine d'essayer.
la source
Si vous n'effectuez que des opérations de lecture, il est préférable de ne pas affecter de valeur à un seul index mais à un intervalle d'index.
Par exemple:
Cela peut être fait avec un struct. Vous pouvez également définir une classe similaire à celle-ci si vous aimez une approche OO.
Maintenant, il vous suffit de parcourir une liste d'intervalles et de vérifier si votre index se trouve dans l'un d'entre eux, ce qui peut être beaucoup moins gourmand en mémoire en moyenne mais coûte plus de ressources CPU.
Si vous triez les intervalles par taille décroissante, vous augmentez la probabilité que l'élément que vous recherchez soit trouvé tôt, ce qui diminue encore votre utilisation moyenne de la mémoire et des ressources du processeur.
Vous pouvez également supprimer tous les intervalles d'une taille de 1. Mettez les valeurs correspondantes dans une carte et ne les vérifiez que si l'élément que vous recherchez n'a pas été trouvé dans les intervalles. Cela devrait également augmenter un peu les performances moyennes.
la source
unt8_t
, même si cela prend beaucoup moins de mémoire.Il y a longtemps, je me souviens juste ...
À l'université, nous avons eu la tâche d'accélérer un programme de traceur de rayons, qui doit lire par algorithme encore et encore à partir de tableaux de tampons. Un ami m'a dit de toujours utiliser des lectures de RAM qui sont des multiples de 4 octets. J'ai donc changé le tableau d'un modèle de [x1, y1, z1, x2, y2, z2, ..., xn, yn, zn] à un modèle de [x1, y1, z1,0, x2, y2, z2 , 0, ..., xn, yn, zn, 0]. Cela signifie que j'ajoute un champ vide après chaque coordonnée 3D. Après quelques tests de performances: c'était plus rapide. Si longue histoire courte: lisez plusieurs de 4 octets de votre tableau à partir de la RAM, et peut-être aussi à partir de la bonne position de départ, vous lisez donc un petit cluster où se trouve l'index recherché et lisez l'index recherché à partir de ce petit cluster dans le processeur. (Dans votre cas, vous n'aurez pas besoin d'insérer des champs de remplissage, mais le concept doit être clair)
Peut-être que d'autres multiples pourraient également être la clé des systèmes plus récents.
Je ne sais pas si cela fonctionnera dans votre cas, donc si cela ne fonctionne pas: Désolé. Si cela fonctionne, je serais heureux d'entendre les résultats de certains tests.
PS: Oh et s'il y a un modèle d'accès ou des index accessibles à proximité, vous pouvez réutiliser le cluster mis en cache.
PPS: Il se pourrait que le facteur multiple ressemble plus à 16 octets ou quelque chose comme ça, il y a trop longtemps, dont je me souviens exactement.
la source
En regardant cela, vous pouvez diviser vos données, par exemple:
Dans ce cas, toutes les valeurs apparaissent jusqu'à un index donné, vous pouvez donc même supprimer l'un des ensembles de bits et représenter la valeur car elle est manquante dans les autres.
Cela vous fera économiser de la mémoire pour ce cas, mais aggraverait le pire des cas. Vous aurez également besoin de plus de puissance de processeur pour effectuer les recherches.
Assurez-vous de mesurer!
la source
Comme Mats le mentionne dans son commentaire-réponse, il est difficile de dire quelle est réellement la meilleure solution sans savoir précisément quel type de données vous avez (par exemple, y a-t-il de longues séries de 0, etc.), et à quoi ressemble votre modèle d'accès comme (est-ce que "aléatoire" signifie "partout" ou simplement "pas strictement de façon complètement linéaire" ou "chaque valeur exactement une fois, juste aléatoire" ou ...).
Cela dit, deux mécanismes me viennent à l'esprit:
(index,value)
ou des(value,index)
tables. Par exemple, ayez une très petite table pour le cas 1%, peut-être une table pour le cas 5% (qui n'a besoin que de stocker les index car tous ont la même valeur), et un grand tableau de bits compressés pour les deux derniers cas. Et avec "table", je veux dire quelque chose qui permet une recherche relativement rapide; c'est-à-dire, peut-être un hachage, un arbre binaire, etc., en fonction de ce dont vous disposez et de vos besoins réels. Si ces sous-tables correspondent à vos caches de 1er / 2ème niveau, vous pourriez avoir de la chance.la source
Je ne suis pas très familier avec C, mais en C ++, vous pouvez utiliser des caractères non signés pour représenter un entier compris entre 0 et 255.
Comparé à un int normal (encore une fois, je viens du monde Java et C ++ ) dans lequel 4 octets (32 bits) sont requis, un caractère non signé nécessite 1 octet (8 bits). il peut donc réduire la taille totale de la baie de 75%.
la source
uint8_t
- le 8 signifie 8 bits.Vous avez décrit succinctement toutes les caractéristiques de distribution de votre tableau; lancez le tableau .
Vous pouvez facilement remplacer le tableau par une méthode aléatoire qui produit la même sortie probabiliste que le tableau.
Si la cohérence est importante (produire la même valeur pour le même index aléatoire), envisagez d'utiliser un filtre de floraison et / ou une carte de hachage pour suivre les hits répétés. Si les accès à votre tableau sont vraiment aléatoires, cela est totalement inutile.
la source