19

限られた範囲の文字列を(非常に)迅速に処理し、それらの値を集計する必要があります。入力ファイルの形式は次のとおりです。

January    7
March     22
September 87
March     36

などなど。線幅が同じなので、適度に速い行を簡単に読み取ることができ、fread機能する完璧なハッシュ関数を開発しましたが、それをさらに速くする方法について誰かがアドバイスをくれるかどうかを確認したいと思いました。それぞれの提案のプロファイルを作成して、それがどのように行われるかを確認します。

ハッシュ関数は月の名前に基づいており、バケットへの値の迅速な割り当てを可能にします。ここで私と一緒に耐えなさい。私は最初に、完全なハッシュの最小文字数を見つけました。

January
February
March
April
May
June
July
August
September
October
November
December

入力行全体があるため、月はすべて9文字であることに注意してください。

残念ながら、1か月を一意としてマークする単一の列はありません。列1の重複J、列2の重複a、列3の重複r、列4の重複u、列5以降の重複<space>(他にも重複がありますが、単一列のハッシュキーを防ぐには1つで十分です)。

ただし、1列目と4列目を使用すると、一意の値、、、、、、、、、、、、、、Juが得られます。このファイルには無効な値が含まれないため、入力データのバケットが正しくないことを心配する必要はありません。FrMcAiM<space>JeJyAuStOoNeDe

文字の16進コードを表示することにより、戦略的な値とANDをとるだけで、低い一意の値を取得できることがわかりました。

FirstChar  Hex  Binary     &0x0f
---------  ---  ---------  -----
   A       x41  0100 0001      1
   D       x44  0100 0100      4
   F       x46  0100 0110      6
   J       x4a  0100 1010     10
   M       x4d  0100 1101     13
   N       x4e  0100 1110     14
   O       x4f  0100 1111     15
   S       x53  0101 0011      3

SecondChar  Hex  Binary     &0x1f
----------  ---  ---------  -----
 <space>    x20  0010 0000      0
    c       x63  0110 0011      3
    e       x65  0110 0101      5
    i       x69  0110 1001      9
    o       x6f  0110 1111     15
    r       x72  0111 0010     18
    t       x74  0111 0100     20
    u       x75  0111 0101     21
    y       x79  0111 1001     25

これにより、静的配列を設定して、(うまくいけば)目がくらむほど高速なハッシュ関数を作成できました。

#define __ -1
static unsigned int hash (const char *str) {
    static unsigned char bucket[] = {
        //   A       S   D       F               J           M   N   O
        __, __, __, __, __, __, __, __, __, __, __, __, __,  4, __, __, // space
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __,  2, __, __, // c
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, 11, __, __, __, __, __,  5, __, __, __, 10, __, // e
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __,  3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // i
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __,  9, // o
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __,  1, __, __, __, __, __, __, __, __, __, // r
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __,  8, __, __, __, __, __, __, __, __, __, __, __, __, // t
        __,  7, __, __, __, __, __, __, __, __,  0, __, __, __, __, __, // u
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __,  6, __, __, __, __, __  // y
    };
    return bucket[((unsigned int)(str[3]&0x1f)<<4)|(str[0]&0xf)];
}

コードでそれをテストします:

#include <stdio.h>
#include <string.h>

// Hash function here.

static char *months[] = {
    "January  ", "February ", "March    ", "April    ", "May      ", "June     ",
    "July     ", "August   ", "September", "October  ", "November ", "December "
};

int main (void) {
    int i;
    for (i = 0; i < sizeof(months)/sizeof(*months); i++)
        printf ("%-10s -> %2d\n", months[i], hash(months[i]));
    return 0;
}

機能的に正しいことを示しています:

January    ->  0
February   ->  1
March      ->  2
April      ->  3
May        ->  4
June       ->  5
July       ->  6
August     ->  7
September  ->  8
October    ->  9
November   -> 10
December   -> 11

しかし、もっと速くできるかどうか知りたいです。

そこに何か提案はありますか?ハッシュ関数に本質的に悪いことがあれば、単純な最適化や完全な書き直しを受け入れることができます。


これはそれほど重要ではないと思いますが、最終バージョンではEBCDICを使用します。理論はそのままですが、文字のコードポイントが異なるため、AND演算がわずかに変わる可能性があります。提供されたアドバイスがEBCDICに問題なく変換されると確信しているので、ASCIIの面でのみ支援に満足します。

4

8 に答える 8

9

私は、改善の余地があまりないという他の人たちに同意します。私が提案できるのは、同じ数の操作で機能する小さなルックアップテーブルであり、CPUキャッシュに長く留まる可能性があります。さらに、最後のスペース充填文字に依存せず、大文字と小文字の任意の混合で機能します。要件の変更の可能性に対してある程度の堅牢性を追加することで、将来、特に小さな変更がそれほど簡単ではなくなるまで実装が最適化された場合に、多くの場合、成果が得られることがわかりました。

#define __ -1
static unsigned int hash (const char *str)
{
    static unsigned char tab[] = {
        __, __,  1, 11, __, __, __, __,  7, __, __, __, __,  6,  0,  5,
         8, __,  2,  3,  9, __, 10, __, __,  4, __, __, __, __, __, __
    };
    return tab[ ( ( str[ 1 ] >> 4 ) & 1 ) + ( str[ 2 ] & 0x1f ) ];
}

これは元のアイデアと同じように機能しますが、空白が少なくなります。

Month  s[1]          s[2]          s[1].4  s[2].4-0  sum  lookup
-----  ------------  ------------  ------  --------  ---  ------
Jan    61:0110 0001  6e:0110 1110       0        14   14       0
Feb    65:0110 0101  62:0110 0010       0         2    2       1
Mar    61:0110 0001  72:0111 0010       0        18   18       2
Apr    70:0111 0000  72:0111 0010       1        18   19       3
May    61:0110 0001  79:0111 1001       0        25   25       4
Jun    75:0111 0101  6e:0110 1110       1        14   15       5
Jul    75:0111 0101  6c:0110 1100       1        12   13       6
Aug    75:0111 0101  67:0110 0111       1         7    8       7
Sep    65:0110 0101  70:0111 0000       0        16   16       8
Oct    63:0110 0011  74:0111 0100       0        20   20       9
Nov    6f:0110 1111  76:0111 0110       0        22   22      10
Dec    65:0110 0101  63:0110 0111       0         3    3      11
             ^             ^ ^^^^
bits:        4             4 3210
于 2010-08-06T08:47:53.573 に答える
8

これが、 EBCDIC-USで見つけた最小のシーケンスです。

バケットには24個の要素があり、インデックスの計算には2つの操作のみを使用します。

static unsigned int hash (const char *str)
{
 static unsigned char tab[] = {
    11, 4,__, 7,__,__, 9, 1,
    __,__,__,__,__,__,__,__,
     3, 5, 2,10, 8,__, 0, 6
 };
 return tab[0x17 & (str[ 1 ] + str[ 2 ])];
}

2番目に良い、xor付きの25アイテム:

static unsigned int hash(const char *str)
{
 static unsigned char tab[] = {
  9,__,__, 7,__,__,11, 1,
 __, 4,__,__,__,__, 3,__,
 __, 5, 8,10, 0,__,__, 6, 2
 };
 return tab[0x1f & (str[ 1 ] ^ str[ 2 ])];
}

(実際には、tab []はここでは32エントリの長さである必要があります。これは、0x1fが誤った入力に対してオーバーフローを生成する可能性があるためです)。


Paxからの更新:その価値については、最初のオプションはEBCDICコードページ500で完全に機能しました。

## Month     str[1] str[2] Lookup
-- --------- ------ ------ ------
 0 January   a (81) n (95)      0
 1 February  e (85) b (82)      1
 2 March     a (81) r (99)      2
 3 April     p (97) r (99)      3
 4 May       a (81) y (a8)      4
 5 June      u (a4) n (95)      5
 6 July      u (a4) l (93)      6
 7 August    u (a4) g (87)      7
 8 September e (85) p (97)      8
 9 October   c (83) t (a3)      9
10 November  o (96) v (a5)     10
11 December  e (85) c (83)     11
于 2010-08-06T11:00:14.677 に答える
4

これは、EBDIC(CCSID 500)、テーブル32バイト(自分のものより小さく、x4uと同じサイズ)でテストされています。

#define __ -1
static unsigned int hash(const char *str)
{
    static unsigned char bucket[] = {
        __, __, __, __, __, __,  1,  8,
        __,  7, __, __, __,  3, __, __,
        11,  6, __, __,  4, __,  2, __,
        __,  0, __,  5,  9, __, __, 10,
    }
    return bucket[(unsigned int)(str[0]|str[3]<<1)&0x1f];
}
于 2010-08-06T10:05:46.017 に答える
3

私はあなたが時期尚早の最適化に従事していないことを確認するためにあなたのより大きなプロセスの詳細なプロファイルから始めます。

これは一見するとかなり高速に見えますが、メモリが本当に安い場合は、さらにまばらな配列を使用して、キャッシュに一部の作業を任せる方がよい場合があります。たとえば(そしてここでカフを考えて)、short最初の2バイトで見つかったものを次の2バイトに単純に追加するとどうなるでしょうかshort。これには1番目と4番目の文字の両方が含まれるため、推測では12個の異なる値が生成され、最適化されない可能性のあるビットフィールド抽出は含まれません。次に、一致するbucket[]配列に64Kのエントリを作成し、そのうち12のみがヒットします。それが正しく機能する場合、それらの12個のエントリは最終的にDキャッシュの一部を占有し、キャッシュされたより大きな配列へのインデックスのいくつかの算術演算を交換しました。

ただし、算術演算を高速化しようとする前と後の両方でプロファイルを作成し、実際に時間を節約できない場所でわざわざ最適化しないでください。(私はPaxがこれを知っていることを知っていますが、最適化の議論に付随する義務的な警告です。)

于 2010-08-06T08:05:48.460 に答える
2

OK、SOの全員として、私は担当者のためにすべてです。; *)上記のコメントで書いたように、ターゲットアーキテクチャの下端のキャッシュラインサイズは256バイトなので、最終的にはテーブルルックアップでのキャッシュトラッシング(テーブルが256バイトを超える)。安価なビットトリックを使用してテーブルを折りたたむと、実際にパフォーマンスが向上する可能性があります。

私はあなたのデータをいじっています。2列目と3列目のオプションもあります。8ビット未満にする方法はまだわかりません。

...そしていつものように、プロファイルを作成し、それが努力を適用するのに最適なポイントであることを確認し、後でもう一度プロファイルを作成して、より高速であることを確認します。

...そしてあなたは一度に複数の行を読んでいますよね?固定レコードサイズはそのように優れており、区切り文字(改行)を検索する必要がなく、一度にそれらの大きなチャンクを読み取ることができます。

次を使用して、配列サイズを減らすことができます。

#define __ -1
static unsigned int hash (const char *str) {
    static unsigned char alloc_to[] = {
        //   A       S   D       F               J           M   N   O
        __, __, __, __, __, __, __, __, __, __, __, __, __,  4, __, __, // space
        __, __, __, __, __, __, __, __, __, __, __, __, __,  2, __, __, // c
        __, __, __, __, 11, __, __, __, __, __,  5, __, __, __, 10, __, // e
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __,  3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // i
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __,  9, // o
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __,  1, __, __, __, __, __, __, __, __, __, // r
        __,  7, __,  8, __, __, __, __, __, __,  0, __, __, __, __, __, // t/u
        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, //
        __, __, __, __, __, __, __, __, __, __,  6, __, __, __, __, __  // y
    };
    return alloc_to[((unsigned int)(str[3]&0x1e)<<3)|(str[0]&0xf)];
}

これにより、16x26から16x13に変更されます。

編集

他の投稿で示唆されているように、文字列が整列されている場合は、それらをショートパンツとして使用できるように、1番目と2番目のショートパンツを追加し、2バイトをxorすると、一意の8ビットキーが得られます(まあ、実際には7つ)。しばらくの間も価値があるかもしれません。ただし、これはASCIIであるため、EBCDICでは機能しない可能性があります。ASCIIでは、キーは次のようになります。

6e Jan
7f Feb
7b Mar
6a Apr
47 May
62 Jun
58 Jul
42 Aug
1a Sep
11 Oct
10 Nov
6d Dec
于 2010-08-06T08:38:15.417 に答える
1

私には十分に似合っています。問題は、ハッシュ関数自体が、ハッシュ関数から1つまたは2つの単純な二項演算を排除するという継続的な取り組みを正当化するのに十分なボトルネックであるかどうかです。もちろん、ファイルアクセスが関係しているように思われるので、全体的な処理についての詳細を知らずに、私はそれを疑っています。

編集:

たぶん、追加されたときに一意の下位ビット(4、5、または6)になる文字のペアが見つかるかどうかを確認できます。

(str[1] + str[2]) & 0x1f

加算してもうまくいかない場合は、他の操作の1つである可能性があります& | ^。これで問題が解決しない場合は、3文字を使用してください。

于 2010-08-06T07:59:54.090 に答える
1

ASCIIでは、month[0] ^ month[2] ^ month[3]取得すると、最大値が95(7月)の一意のハッシュが得られます。これにより、テーブルサイズをかなり小さくすることができます(最小値は20(5月)なので、減算すると次のようになります。再び小さくなります)。

同じことはEBCDICには当てはまらないかもしれませんが、似たようなことがあるかもしれません。

于 2010-08-06T09:10:49.953 に答える
1

集計を行うために、ハッシュと月のインデックスの間のマッピングが本当に必要ですか?ハッシュを返した月を返す代わりに、それを集計に使用する場合は、ルックアップを削除できます。x4uの回答では、ハッシュ関数の最後の行は次のようになります。

return ( ( str[ 1 ] >> 4 ) & 1 ) + ( str[ 2 ] & 0x1f )

そして、ループ内ではなく、最後にのみ結果をソートして、合計を行うことができます。

于 2010-08-06T12:22:38.317 に答える