別の質問 に答えて、ソートされた配列内のさまざまな検索方法を比較するプログラムを以下に書きました。基本的に、補間検索の2つの実装とバイナリ検索の1つを比較しました。異なるバリアントによって費やされたサイクルを(同じデータセットで)カウントすることにより、パフォーマンスを比較しました。
ただし、これらの関数を最適化してさらに高速化する方法があると確信しています。この検索機能をより高速にするにはどうすればよいですか? CまたはC++でのソリューションは許容可能ですが、100,000要素の配列を処理するために必要です。
#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;
}
}
データのメモリ内レイアウトをある程度制御できる場合は、Judy配列を確認することをお勧めします。
あるいは、もっと単純なアイデアを出すために、二分探索は常に探索空間を半分に減らします。最適なカットポイントは、補間で見つけることができます(カットポイントは、キーが予想される場所ではなく、次のステップの検索スペースの統計的期待値を最小にするポイントにする必要があります)。これにより、ステップ数は最小限に抑えられますが、すべてのステップのコストが等しいわけではありません。階層メモリを使用すると、局所性を維持できる場合に、単一のテストと同時に複数のテストを実行できます。バイナリ検索の最初のMステップは最大2 ** Mの一意の要素にしか触れないため、これらを一緒に保存すると、キャッシュスペースフェッチごと(比較ごとではなく)の検索スペースを大幅に削減でき、現実世界でのパフォーマンスが向上します。
n-aryツリーはその上で機能し、Judy配列はそれほど重要ではないいくつかの最適化を追加します。
結論:ランダムアクセスメモリ(RAM)でも、ランダムアクセスよりもシーケンシャルアクセスの方が高速です。検索アルゴリズムはその事実をその利点に利用するべきです。
Win32 Core2 Quad Q6600、gcc v4.3 msysでベンチマーク。 g ++ -O3を使用したコンパイル。
観察-アサート、タイミング、およびループのオーバーヘッドは約40%であるため、テスト対象のアルゴリズムを実際に改善するには、以下に示すゲインを0.6で割る必要があります。
簡単な答え:
私のマシンでは、interpolationSearchで「low」、「high」、「mid」のint64_tをintに置き換えると、20%から40%高速化されます。これは、私が見つけた中で最速の簡単な方法です。私のマシンでは、ルックアップごとに約150サイクルかかります(アレイサイズが100000の場合)。これは、キャッシュミスとほぼ同じサイクル数です。したがって、実際のアプリケーションでは、キャッシュの世話がおそらく最大の要因になるでしょう。
BinarySearchの「/ 2」を「>> 1」に置き換えると、4%高速になります。
「arr」と同じデータを含むベクトルでSTLのbinary_searchアルゴリズムを使用すると、手動でコーディングしたbinarySearchとほぼ同じ速度になります。小さい「サイズ」ではSTLははるかに遅くなりますが、約40%です。
非常に複雑な解決策があり、専門の並べ替え機能が必要です。ソートは、優れたクイックソートよりも少し遅いですが、すべてのテストで、検索機能がバイナリまたは補間検索よりもはるかに高速であることが示されています。名前が既に使用されていることを知る前に、これを回帰ソートと呼びましたが、新しい名前を思いつくことはありませんでした(アイデア?)。
デモするファイルは3つあります。
回帰ソート/検索コード:
#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);
}
そして、通常の検索機能があります:
#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;
}
次に、さまざまな種類をテストするための簡単なメインを作成しました。
#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;
}
それを試して、それがあなたのために速いかどうか教えてください。非常に複雑なので、何をしているのかわからない場合は簡単に解読できます。変更には注意してください。
Ubuntuでメインをg ++でコンパイルしました。
まずデータを見て、一般的な方法よりもデータ固有の方法で大きな利益が得られるかどうかを確認します。
大規模な静的にソートされたデータセットの場合、追加のインデックスを作成して、使用する予定のメモリ量に基づいて部分的なハト穴を提供できます。例えば範囲の256x256 2次元配列を作成するとします。これには、対応する上位バイトを持つ要素の検索配列の開始位置と終了位置が含まれます。検索に来たら、キーの上位バイトを使用して、検索する必要のある配列の範囲/サブセットを見つけます。 100,000要素のバイナリ検索で〜20の比較があった場合O(log2(n))これで、16要素の最大4つの比較、またはO(log2(n/15))ここでのメモリコストは約512kです
別の方法も、あまり変化しないデータに適していますが、一般的に求められるアイテムとめったに求められないアイテムの配列にデータを分割することです。たとえば、既存の検索をそのままにして、長期にわたるテスト期間にわたって多数の実世界のケースを実行し、求められているアイテムの詳細をログに記録すると、分布が非常に不均一であることがわかります。他よりはるかに定期的に求めました。これが事実である場合、配列を一般的に求められる値のはるかに小さい配列とより大きい残りの配列に分割し、最初に小さい配列を検索します。データが正しい場合(大きな場合!)、メモリコストをかけずに、最初のソリューションとほぼ同様の改善を達成できることがよくあります。
他の多くのデータ固有の最適化があり、それらは、試行され、テストされ、はるかに広く使用されている一般的なソリューションを改善しようとするよりもはるかに優れています。
データに特別な特性があることがわかっている場合を除き、純粋な内挿検索には線形時間がかかるリスクがあります。補間がほとんどのデータに役立つことを期待しているが、病理学的データの場合にそれが害を与えたくない場合は、補間された推測と中間点の(場合によっては重み付けされた)平均を使用して、実行時の対数境界を確保します。
これに取り組む1つの方法は、スペースと時間のトレードオフを使用することです。実行できる方法はいくつもあります。極端な方法は、最大サイズがソートされた配列の最大値である配列を作成することです。各位置をインデックスで初期化し、sortedArrayに入れます。次に、検索は単にO(1)になります。
ただし、次のバージョンはもう少し現実的であり、現実の世界で役立つ可能性があります。最初の呼び出しで初期化される「ヘルパー」構造を使用します。あまりテストせずに空中に引き出した数値で除算することで、検索スペースを小さなスペースにマッピングします。これは、sortedArrayの値のグループの下限のインデックスをヘルパーマップに格納します。実際の検索では、toFind
数を選択した除数で除算し、通常のバイナリ検索のsortedArray
の狭められた境界を抽出します。
たとえば、ソートされた値の範囲が1〜1000で、除数が100の場合、ルックアップ配列には10個の「セクション」が含まれる場合があります。値250を検索するには、値を100で割って整数のインデックス位置250/100 = 2を求めます。 map[2]
には、200以上の値のSortedArrayインデックスが含まれます。 map[3]
のインデックス位置は300以上で、通常のバイナリ検索の境界位置が小さくなります。関数の残りの部分は、バイナリ検索関数の正確なコピーになります。
ヘルパーマップの初期化は、単純なスキャンよりもバイナリ検索を使用して位置を入力する方が効率的かもしれませんが、1回限りのコストであるため、テストしませんでした。このメカニズムは、均等に分散された特定のテスト番号に対して適切に機能します。書かれているように、分布が均一でなければ、それは良くありません。この方法は浮動小数点検索値でも使用できると思います。ただし、それを一般的な検索キーに外挿することは困難な場合があります。たとえば、文字データキーのメソッドがどうなるかわかりません。インデックスの境界を見つけるには、特定の配列の位置にマップされた何らかの種類のO(1) lookup/hashが必要です。現時点では、その関数がどうなるか、または存在します。
次の実装では、ヘルパーマップのセットアップをかなり迅速にまとめました。それはきれいではなく、私はそれがすべての場合に正しいことを100%確信していませんが、それはアイデアを示しています。デバッグテストを使用して実行し、結果を既存のbinarySearch関数と比較して、正しく機能することを確認します。
以下は数値の例です。
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
以下は素早い実装です:
#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
}
ヘルパー構造を初期化する(そしてメモリを解放する)必要があります。
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;
質問が終了する前に現在のバージョンを投稿する(うまくいけば、後でそれを理解できるようになるでしょう)。現時点では、他のすべてのバージョンよりも悪いです(ループの終わりに対する私の変更がこの影響を与える理由を誰かが理解している場合は、コメントを歓迎します)。
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
}
比較に使用されたバイナリ検索の実装を改善できます。重要なアイデアは、最初に範囲を「正規化」して、最初のステップの後でターゲットが常に最小値より大きく最大値より小さくなるようにすることです。これにより、終了デルタサイズが増加します。また、並べ替えられた配列の最初の要素よりも小さいか、並べ替えられた配列の最後の要素よりも大きい特別な大文字と小文字のターゲットの効果もあります。検索時間は約15%向上すると予想されます。 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