Répondant à une autre question , j’ai écrit le programme ci-dessous pour comparer différentes méthodes de recherche dans un tableau trié. En gros, j'ai comparé deux implémentations de la recherche par interpolation et une de la recherche binaire. J'ai comparé les performances en comptant les cycles passés (avec le même ensemble de données) selon les différentes variantes.
Cependant, je suis sûr qu'il existe des moyens d'optimiser ces fonctions pour les rendre encore plus rapides. Quelqu'un a-t-il une idée sur la façon dont je peux rendre cette fonction de recherche plus rapide? Une solution en C ou C++ est acceptable, mais j'en ai besoin pour traiter un tableau contenant 100 000 éléments.
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <assert.h>
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int interpolationSearch(int sortedArray[], int toFind, int len) {
// Returns index of toFind in sortedArray, or -1 if not found
int64_t low = 0;
int64_t high = len - 1;
int64_t mid;
int l = sortedArray[low];
int h = sortedArray[high];
while (l <= toFind && h >= toFind) {
mid = low + (int64_t)((int64_t)(high - low)*(int64_t)(toFind - l))/((int64_t)(h-l));
int m = sortedArray[mid];
if (m < toFind) {
l = sortedArray[low = mid + 1];
} else if (m > toFind) {
h = sortedArray[high = mid - 1];
} else {
return mid;
}
}
if (sortedArray[low] == toFind)
return low;
else
return -1; // Not found
}
int interpolationSearch2(int sortedArray[], int toFind, int len) {
// Returns index of toFind in sortedArray, or -1 if not found
int low = 0;
int high = len - 1;
int mid;
int l = sortedArray[low];
int h = sortedArray[high];
while (l <= toFind && h >= toFind) {
mid = low + ((float)(high - low)*(float)(toFind - l))/(1+(float)(h-l));
int m = sortedArray[mid];
if (m < toFind) {
l = sortedArray[low = mid + 1];
} else if (m > toFind) {
h = sortedArray[high = mid - 1];
} else {
return mid;
}
}
if (sortedArray[low] == toFind)
return low;
else
return -1; // Not found
}
int binarySearch(int sortedArray[], int toFind, int len)
{
// Returns index of toFind in sortedArray, or -1 if not found
int low = 0;
int high = len - 1;
int mid;
int l = sortedArray[low];
int h = sortedArray[high];
while (l <= toFind && h >= toFind) {
mid = (low + high)/2;
int m = sortedArray[mid];
if (m < toFind) {
l = sortedArray[low = mid + 1];
} else if (m > toFind) {
h = sortedArray[high = mid - 1];
} else {
return mid;
}
}
if (sortedArray[low] == toFind)
return low;
else
return -1; // Not found
}
int order(const void *p1, const void *p2) { return *(int*)p1-*(int*)p2; }
int main(void) {
int i = 0, j = 0, size = 100000, trials = 10000;
int searched[trials];
srand(-time(0));
for (j=0; j<trials; j++) { searched[j] = Rand()%size; }
while (size > 10){
int arr[size];
for (i=0; i<size; i++) { arr[i] = Rand()%size; }
qsort(arr,size,sizeof(int),order);
unsigned long long totalcycles_bs = 0;
unsigned long long totalcycles_is_64 = 0;
unsigned long long totalcycles_is_float = 0;
unsigned long long totalcycles_new = 0;
int res_bs, res_is_64, res_is_float, res_new;
for (j=0; j<trials; j++) {
unsigned long long tmp, cycles = rdtsc();
res_bs = binarySearch(arr,searched[j],size);
tmp = rdtsc(); totalcycles_bs += tmp - cycles; cycles = tmp;
res_is_64 = interpolationSearch(arr,searched[j],size);
assert(res_is_64 == res_bs || arr[res_is_64] == searched[j]);
tmp = rdtsc(); totalcycles_is_64 += tmp - cycles; cycles = tmp;
res_is_float = interpolationSearch2(arr,searched[j],size);
assert(res_is_float == res_bs || arr[res_is_float] == searched[j]);
tmp = rdtsc(); totalcycles_is_float += tmp - cycles; cycles = tmp;
}
printf("----------------- size = %10d\n", size);
printf("binary search = %10llu\n", totalcycles_bs);
printf("interpolation uint64_t = %10llu\n", totalcycles_is_64);
printf("interpolation float = %10llu\n", totalcycles_is_float);
printf("new = %10llu\n", totalcycles_new);
printf("\n");
size >>= 1;
}
}
Si vous avez un certain contrôle sur la disposition en mémoire des données, vous pouvez consulter les tableaux de Judy.
Ou pour simplifier: une recherche binaire coupe toujours l’espace de recherche de moitié. Une interpolation permet de trouver un point de coupure optimal (le point de coupure NE DOIT PAS être l'endroit où la clé devrait être, mais le point qui minimise l'attente statistique de l'espace de recherche pour l'étape suivante). Cela minimise le nombre d'étapes mais ... toutes les étapes n'ont pas le même coût. Les mémoires hiérarchiques permettent d'exécuter plusieurs tests en même temps qu'un seul test, si la localité peut être conservée. Étant donné que les premières M étapes d'une recherche binaire ne touchent qu'un maximum de 2 ** M éléments uniques, leur stockage peut permettre de réduire considérablement l'espace de recherche par extraction de mémoire cache (et non par comparaison), ce qui représente de meilleures performances dans le monde réel.
les arbres n-aire fonctionnent sur cette base, puis les tableaux de Judy ajoutent quelques optimisations moins importantes.
En bout de ligne: même la "RAM" (Random Access Memory) est plus rapide lorsqu’on y accède de manière séquentielle à celle aléatoire. Un algorithme de recherche devrait utiliser ce fait à son avantage.
Benchmarked sur Win32 Core2 Quad Q6600, gcc v4.3 msys. Compiler avec g ++ -O3, rien d’extraordinaire.
Observation - les assertions, le temps et la surcharge de la boucle sont d’environ 40%. Par conséquent, les gains énumérés ci-dessous doivent être divisés par 0,6 pour obtenir l’amélioration réelle des algorithmes testés.
Réponses simples:
Sur ma machine, remplacer int64_t par int pour "low", "high" et "mid" dans interpolationSearch accélère de 20% à 40%. C'est la méthode facile la plus rapide que j'ai pu trouver. Il faut environ 150 cycles par consultation sur ma machine (pour une taille de matrice de 100 000). C'est à peu près le même nombre de cycles qu'un cache manqué. Donc, dans les applications réelles, la gestion de votre cache sera probablement le facteur le plus important.
Remplacer le "/ 2" de binarySearch par un ">> 1" accélère de 4%.
L'utilisation de l'algorithme binary_search de STL, sur un vecteur contenant les mêmes données que "arr", est environ la même vitesse que le binarySearch codé à la main. Bien que sur les plus petites "tailles", la STL est beaucoup plus lente - environ 40%.
J'ai une solution excessivement compliquée, qui nécessite une fonction de tri spécialisée. Le tri est légèrement plus lent qu'un bon tri rapide, mais tous mes tests montrent que la fonction de recherche est beaucoup plus rapide qu'une recherche binaire ou par interpolation. J'ai appelé cela une sorte de régression avant de découvrir que le nom était déjà pris, mais je n'ai pas pris la peine de penser à un nouveau nom (idées?).
Il y a trois fichiers à démontrer.
Le code de tri/recherche de la régression:
#include <sstream>
#include <math.h>
#include <ctime>
#include "limits.h"
void insertionSort(int array[], int length) {
int key, j;
for(int i = 1; i < length; i++) {
key = array[i];
j = i - 1;
while (j >= 0 && array[j] > key) {
array[j + 1] = array[j];
--j;
}
array[j + 1] = key;
}
}
class RegressionTable {
public:
RegressionTable(int arr[], int s, int lower, int upper, double mult, int divs);
RegressionTable(int arr[], int s);
void sort(void);
int find(int key);
void printTable(void);
void showSize(void);
private:
void createTable(void);
inline unsigned int resolve(int n);
int * array;
int * table;
int * tableSize;
int size;
int lowerBound;
int upperBound;
int divisions;
int divisionSize;
int newSize;
double multiplier;
};
RegressionTable::RegressionTable(int arr[], int s) {
array = arr;
size = s;
multiplier = 1.35;
divisions = sqrt(size);
upperBound = INT_MIN;
lowerBound = INT_MAX;
for (int i = 0; i < size; ++i) {
if (array[i] > upperBound)
upperBound = array[i];
if (array[i] < lowerBound)
lowerBound = array[i];
}
createTable();
}
RegressionTable::RegressionTable(int arr[], int s, int lower, int upper, double mult, int divs) {
array = arr;
size = s;
lowerBound = lower;
upperBound = upper;
multiplier = mult;
divisions = divs;
createTable();
}
void RegressionTable::showSize(void) {
int bytes = sizeof(*this);
bytes = bytes + sizeof(int) * 2 * (divisions + 1);
}
void RegressionTable::createTable(void) {
divisionSize = size / divisions;
newSize = multiplier * double(size);
table = new int[divisions + 1];
tableSize = new int[divisions + 1];
for (int i = 0; i < divisions; ++i) {
table[i] = 0;
tableSize[i] = 0;
}
for (int i = 0; i < size; ++i) {
++table[((array[i] - lowerBound) / divisionSize) + 1];
}
for (int i = 1; i <= divisions; ++i) {
table[i] += table[i - 1];
}
table[0] = 0;
for (int i = 0; i < divisions; ++i) {
tableSize[i] = table[i + 1] - table[i];
}
}
int RegressionTable::find(int key) {
double temp = multiplier;
multiplier = 1;
int minIndex = table[(key - lowerBound) / divisionSize];
int maxIndex = minIndex + tableSize[key / divisionSize];
int guess = resolve(key);
double t;
while (array[guess] != key) {
// uncomment this line if you want to see where it is searching.
//cout << "Regression Guessing " << guess << ", not there." << endl;
if (array[guess] < key) {
minIndex = guess + 1;
}
if (array[guess] > key) {
maxIndex = guess - 1;
}
if (array[minIndex] > key || array[maxIndex] < key) {
return -1;
}
t = ((double)key - array[minIndex]) / ((double)array[maxIndex] - array[minIndex]);
guess = minIndex + t * (maxIndex - minIndex);
}
multiplier = temp;
return guess;
}
inline unsigned int RegressionTable::resolve(int n) {
float temp;
int subDomain = (n - lowerBound) / divisionSize;
temp = n % divisionSize;
temp /= divisionSize;
temp *= tableSize[subDomain];
temp += table[subDomain];
temp *= multiplier;
return (unsigned int)temp;
}
void RegressionTable::sort(void) {
int * out = new int[int(size * multiplier)];
bool * used = new bool[int(size * multiplier)];
int higher, lower;
bool placed;
for (int i = 0; i < size; ++i) {
/* Figure out where to put the darn thing */
higher = resolve(array[i]);
lower = higher - 1;
if (higher > newSize) {
higher = size;
lower = size - 1;
} else if (lower < 0) {
higher = 0;
lower = 0;
}
placed = false;
while (!placed) {
if (higher < size && !used[higher]) {
out[higher] = array[i];
used[higher] = true;
placed = true;
} else if (lower >= 0 && !used[lower]) {
out[lower] = array[i];
used[lower] = true;
placed = true;
}
--lower;
++higher;
}
}
int index = 0;
for (int i = 0; i < size * multiplier; ++i) {
if (used[i]) {
array[index] = out[i];
++index;
}
}
insertionSort(array, size);
}
Et puis il y a les fonctions de recherche habituelles:
#include <iostream>
using namespace std;
int binarySearch(int array[], int start, int end, int key) {
// Determine the search point.
int searchPos = (start + end) / 2;
// If we crossed over our bounds or met in the middle, then it is not here.
if (start >= end)
return -1;
// Search the bottom half of the array if the query is smaller.
if (array[searchPos] > key)
return binarySearch (array, start, searchPos - 1, key);
// Search the top half of the array if the query is larger.
if (array[searchPos] < key)
return binarySearch (array, searchPos + 1, end, key);
// If we found it then we are done.
if (array[searchPos] == key)
return searchPos;
}
int binarySearch(int array[], int size, int key) {
return binarySearch(array, 0, size - 1, key);
}
int interpolationSearch(int array[], int size, int key) {
int guess = 0;
double t;
int minIndex = 0;
int maxIndex = size - 1;
while (array[guess] != key) {
t = ((double)key - array[minIndex]) / ((double)array[maxIndex] - array[minIndex]);
guess = minIndex + t * (maxIndex - minIndex);
if (array[guess] < key) {
minIndex = guess + 1;
}
if (array[guess] > key) {
maxIndex = guess - 1;
}
if (array[minIndex] > key || array[maxIndex] < key) {
return -1;
}
}
return guess;
}
Et puis j'ai écrit un simple principal pour tester les différentes sortes.
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <ctime>
#include "regression.h"
#include "search.h"
using namespace std;
void randomizeArray(int array[], int size) {
for (int i = 0; i < size; ++i) {
array[i] = Rand() % size;
}
}
int main(int argc, char * argv[]) {
int size = 100000;
string arg;
if (argc > 1) {
arg = argv[1];
size = atoi(arg.c_str());
}
srand(time(NULL));
int * array;
cout << "Creating Array Of Size " << size << "...\n";
array = new int[size];
randomizeArray(array, size);
cout << "Sorting Array...\n";
RegressionTable t(array, size, 0, size*2.5, 1.5, size);
//RegressionTable t(array, size);
t.sort();
int trials = 10000000;
int start;
cout << "Binary Search...\n";
start = clock();
for (int i = 0; i < trials; ++i) {
binarySearch(array, size, i % size);
}
cout << clock() - start << endl;
cout << "Interpolation Search...\n";
start = clock();
for (int i = 0; i < trials; ++i) {
interpolationSearch(array, size, i % size);
}
cout << clock() - start << endl;
cout << "Regression Search...\n";
start = clock();
for (int i = 0; i < trials; ++i) {
t.find(i % size);
}
cout << clock() - start << endl;
return 0;
}
Essayez et dites-moi si c'est plus rapide pour vous. C'est super compliqué, donc c'est vraiment facile de le casser si vous ne savez pas ce que vous faites. Attention à ne pas le modifier.
J'ai compilé le principal avec g ++ sur Ubuntu.
Examinez d'abord les données et déterminez si une méthode spécifique permet d'obtenir un gain important par rapport à une méthode générale.
Pour les grands ensembles de données triés statiques, vous pouvez créer un index supplémentaire pour fournir une séparation partielle, en fonction de la quantité de mémoire que vous souhaitez utiliser. par exemple. Supposons que nous créons un tableau de plages bidimensionnel de 256x256, que nous peuplons avec les positions de début et de fin dans le tableau de recherche d'éléments avec les octets de poids fort correspondants. Lorsque nous en arrivons à la recherche, nous utilisons ensuite les octets de poids fort sur la clé pour trouver la plage/le sous-ensemble du tableau à rechercher. Si nous avions environ 20 comparaisons sur notre recherche binaire de 100 000 éléments O(log2(n)), nous en sommes maintenant à environ 4 comparaisons pour 16 éléments, ou à 0 (log2 (n/15)). . Le coût en mémoire ici est d'environ 512k
Une autre méthode, toujours adaptée aux données qui ne changent pas beaucoup, consiste à les diviser en tableaux contenant des éléments communément recherchés et des éléments rarement recherchés. Par exemple, si vous laissez votre recherche existante en place et exécutez un grand nombre de cas réels sur une période d’essai prolongée, et enregistrez les détails de l’article recherché, vous constaterez peut-être que la distribution est très inégale, c’est-à-dire que recherché beaucoup plus régulièrement que les autres. Si tel est le cas, divisez votre tableau en un tableau beaucoup plus petit de valeurs communément recherchées et en un tableau restant plus grand, puis recherchez le tableau le plus petit en premier. Si les données sont correctes (gros si!), Vous pouvez souvent obtenir des améliorations globalement similaires à la première solution sans le coût en mémoire.
Il existe de nombreuses autres optimisations spécifiques aux données qui donnent de bien meilleurs résultats que d'essayer d'améliorer des solutions générales éprouvées, testées et utilisées plus largement.
À moins que vos données ne possèdent des propriétés spéciales, la recherche par interpolation pure risque de prendre du temps linéaire. Si vous prévoyez une interpolation pour la plupart des données, mais que vous ne voulez pas que les données pathologiques vous fassent mal, utilisez une moyenne (éventuellement pondérée) de l'estimation approximative interpolée et du point médian, afin de garantir une liaison logarithmique du temps d'exécution.
Une façon de procéder consiste à utiliser un compromis espace-temps. Il y a plusieurs façons de procéder. La solution extrême consiste simplement à créer un tableau dont la taille maximale est la valeur maximale du tableau trié. Initialisez chaque position avec l'index dans triArray. Ensuite, la recherche serait simplement O (1).
La version suivante, cependant, pourrait être un peu plus réaliste et peut-être utile dans le monde réel. Il utilise une structure "d'assistance" initialisée lors du premier appel. Il mappe l'espace de recherche sur un espace plus petit en divisant par un nombre que j'ai sorti de l'air sans trop de tests. Il stocke l'index de la limite inférieure pour un groupe de valeurs de sortArray dans la mappe d'assistance. La recherche réelle divise le nombre toFind
par le diviseur choisi et extrait les limites restreintes de sortedArray
pour une recherche binaire normale.
Par exemple, si les valeurs triées vont de 1 à 1 000 et que le diviseur est 100, le tableau de correspondance peut contenir 10 "sections". Pour rechercher la valeur 250, il faudrait la diviser par 100 pour obtenir la position d'index de nombre entier 250/100 = 2. map[2]
contiendrait l'index trié Tableau pour les valeurs 200 et supérieures. map[3]
aurait la position d'index de 300 et plus, fournissant ainsi une position limite plus petite pour une recherche binaire normale. Le reste de la fonction est alors une copie exacte de votre fonction de recherche binaire.
L'initialisation de la carte d'assistance pourrait être plus efficace en utilisant une recherche binaire pour combler les positions plutôt qu'une simple analyse, mais il s'agit d'un coût ponctuel, je ne me suis donc pas soucié de le tester. Ce mécanisme fonctionne bien pour les nombres de tests donnés qui sont répartis de manière uniforme. Tel qu'écrit, il ne serait pas aussi bon si la distribution n'était pas égale. Je pense que cette méthode pourrait également être utilisée avec des valeurs de recherche à virgule flottante. Cependant, l'extrapoler à des clés de recherche génériques pourrait être plus difficile. Par exemple, je ne suis pas sûr de la méthode à utiliser pour les clés de données de caractères. Il faudrait une sorte de O(1) lookup/hash mappé sur une position de tableau spécifique pour trouver les limites de l'index. Je ne sais pas pour le moment quelle serait cette fonction ou si elle existait.
J'ai kludé la configuration de la carte d'assistance dans l'implémentation suivante assez rapidement. Ce n’est pas joli et je ne suis pas sûr à 100% que c’est correct dans tous les cas mais cela montre bien l’idée. Je l'ai exécuté avec un test de débogage pour comparer les résultats à votre fonction existante de recherche binaire afin d'être certain de son fonctionnement correct.
Voici des exemples de numéros:
100000 * 10000 : cycles binary search = 10197811
100000 * 10000 : cycles interpolation uint64_t = 9007939
100000 * 10000 : cycles interpolation float = 8386879
100000 * 10000 : cycles binary w/helper = 6462534
Voici l'implémentation rapide et sale:
#define REDUCTION 100 // pulled out of the air
typedef struct {
int init; // have we initialized it?
int numSections;
int *map;
int divisor;
} binhelp;
int binarySearchHelp( binhelp *phelp, int sortedArray[], int toFind, int len)
{
// Returns index of toFind in sortedArray, or -1 if not found
int low;
int high;
int mid;
if ( !phelp->init && len > REDUCTION ) {
int i;
int numSections = len / REDUCTION;
int divisor = (( sortedArray[len-1] - 1 ) / numSections ) + 1;
int threshold;
int arrayPos;
phelp->init = 1;
phelp->divisor = divisor;
phelp->numSections = numSections;
phelp->map = (int*)malloc((numSections+2) * sizeof(int));
phelp->map[0] = 0;
phelp->map[numSections+1] = len-1;
arrayPos = 0;
// Scan through the array and set up the mapping positions. Simple linear
// scan but it is a one-time cost.
for ( i = 1; i <= numSections; i++ ) {
threshold = i * divisor;
while ( arrayPos < len && sortedArray[arrayPos] < threshold )
arrayPos++;
if ( arrayPos < len )
phelp->map[i] = arrayPos;
else
// kludge to take care of aliasing
phelp->map[i] = len - 1;
}
}
if ( phelp->init ) {
int section = toFind / phelp->divisor;
if ( section > phelp->numSections )
// it is bigger than all values
return -1;
low = phelp->map[section];
if ( section == phelp->numSections )
high = len - 1;
else
high = phelp->map[section+1];
} else {
// use normal start points
low = 0;
high = len - 1;
}
// the following is a direct copy of the Kriss' binarySearch
int l = sortedArray[low];
int h = sortedArray[high];
while (l <= toFind && h >= toFind) {
mid = (low + high)/2;
int m = sortedArray[mid];
if (m < toFind) {
l = sortedArray[low = mid + 1];
} else if (m > toFind) {
h = sortedArray[high = mid - 1];
} else {
return mid;
}
}
if (sortedArray[low] == toFind)
return low;
else
return -1; // Not found
}
La structure d'assistance doit être initialisée (et libérée de la mémoire):
help.init = 0;
unsigned long long totalcycles4 = 0;
... make the calls same as for the other ones but pass the structure ...
binarySearchHelp(&help, arr,searched[j],length);
if ( help.init )
free( help.map );
help.init = 0;
Publier ma version actuelle avant que la question ne soit fermée (j'espère pouvoir ainsi l'apprécier plus tard). Pour le moment, c'est pire que toutes les autres versions (si quelqu'un comprend pourquoi mes modifications en fin de boucle ont cet effet, les commentaires sont les bienvenus).
int newSearch(int sortedArray[], int toFind, int len)
{
// Returns index of toFind in sortedArray, or -1 if not found
int low = 0;
int high = len - 1;
int mid;
int l = sortedArray[low];
int h = sortedArray[high];
while (l < toFind && h > toFind) {
mid = low + ((float)(high - low)*(float)(toFind - l))/(1+(float)(h-l));
int m = sortedArray[mid];
if (m < toFind) {
l = sortedArray[low = mid + 1];
} else if (m > toFind) {
h = sortedArray[high = mid - 1];
} else {
return mid;
}
}
if (l == toFind)
return low;
else if (h == toFind)
return high;
else
return -1; // Not found
}
L'implémentation de la recherche binaire utilisée pour les comparaisons peut être améliorée. L'idée principale est de "normaliser" initialement la plage pour que la cible soit toujours> minimum et <maximum après la première étape. Cela augmente la taille du delta de terminaison. Cela a également pour effet des cibles de casse spéciales inférieures au premier élément du tableau trié ou supérieures au dernier élément du tableau trié. Attendez-vous à une amélioration d'environ 15% du temps de recherche. Voici à quoi le code pourrait ressembler en C++.
int binarySearch(int * &array, int target, int min, int max)
{ // binarySearch
// normalize min and max so that we know the target is > min and < max
if (target <= array[min]) // if min not normalized
{ // target <= array[min]
if (target == array[min]) return min;
return -1;
} // end target <= array[min]
// min is now normalized
if (target >= array[max]) // if max not normalized
{ // target >= array[max]
if (target == array[max]) return max;
return -1;
} // end target >= array[max]
// max is now normalized
while (min + 1 < max)
{ // delta >=2
int tempi = min + ((max - min) >> 1); // point to index approximately in the middle between min and max
int atempi = array[tempi]; // just in case the compiler does not optimize this
if (atempi > target)max = tempi; // if the target is smaller, we can decrease max and it is still normalized
else if (atempi < target)min = tempi; // the target is bigger, so we can increase min and it is still normalized
else return tempi; // if we found the target, return with the index
// Note that it is important that this test for equality is last because it rarely occurs.
} // end delta >=2
return -1; // nothing in between normalized min and max
} // end binarySearch