Существует ли способ сделать этот поиск хеша немного быстрее?

У меня есть требование к (очень) быстро строкам процесса ограниченного диапазона, соответствуя их значениям. Входной файл имеет форму:

January    7
March     22
September 87
March     36

и т.д. Поскольку ширины строки идентичны, я могу просто читать в строке с fread довольно быстро, и я разработал идеальную хеш-функцию, которая работает, но я хотел видеть, мог ли кто-либо дать совет о том, как сделать его еще быстрее. Я представлю каждое предложение, чтобы видеть, как оно идет.

Хеш-функция основана на имени месяца для разрешения быстрого выделения значения к блоку. Терпите меня здесь. Я сначала выяснил минимальное количество символов для идеального хеша:

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

Следует иметь в виду, что месяцы являются всеми девятью символами из-за того, что у меня есть вся входная строка.

К сожалению, нет никакого отдельного столбца для маркировки уникального месяца. Дубликаты столбца 1 J, дубликаты столбца 2 a, дубликаты столбца 3 r, дубликаты столбца 4 u и столбцы 5 вперед дубликат <space> (существуют другие дубликаты, но каждого достаточно для предотвращения ключа хеша отдельного столбца).

Однако при помощи первого и четвертого столбца, я получаю значения Ju, Fr, Mc, Ai, M<space>, Je, Jy, Au, St, Oo, Ne и De, которые уникальны. Не будет никаких недопустимых значений в этом файле, таким образом, я не должен буду волноваться о неправильных блоках для входных данных.

Путем просмотра шестнадцатеричного числа кодирует для символов, я нашел, что мог получить низкие уникальные значения просто Выполнением операции "И" со стратегическими значениями:

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. Теория будет все еще стоять, но операция И может измениться немного, так как символы имеют различные кодовые точки. Я буду доволен любой помощью только на передней стороне ASCII, так как я уверен независимо от того, что совет дан, переведет хорошо в EBCDIC.

19
задан paxdiablo 6 August 2010 в 07:51
поделиться

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 ])];
}

Второе место, 25 элементов с xor:

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: Как бы то ни было, первый вариант отлично работал для кодовой страницы 500 EBCDIC:

## 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
8
ответ дан 30 November 2019 в 04:11
поделиться

Я согласен с другими в том, что здесь мало возможностей для улучшения. Все, что я могу предложить, - это меньшая таблица поиска, которая работает с тем же количеством операций, что может заставить ее дольше оставаться в кеше ЦП. Кроме того, он не полагается на символы, заполняющие пробелы в конце, и работает с любым сочетанием символов верхнего и нижнего регистра. Я обнаружил, что добавление некоторой разумной устойчивости к вероятным изменениям требований часто окупается в будущем, особенно когда реализация оптимизирована до такой степени, что небольшие изменения уже не так просты.

#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
9
ответ дан 30 November 2019 в 04:11
поделиться

В ASCII, если вы возьмете месяц [0] ^ месяц [2] ^ месяц [3] , то получите уникальный хеш с максимальным значением 95 (июль ), что должно позволить вам немного уменьшить размер вашей таблицы (и минимальное значение 20 (май), поэтому вычитание снова сделает ее меньше).

Это может быть не так в EBCDIC, но может быть что-то подобное.

1
ответ дан 30 November 2019 в 04:11
поделиться

Это проверено для 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];
}
4
ответ дан 30 November 2019 в 04:11
поделиться

Хорошо, как и все в 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)];
}

, который изменяет его с 16 на 26 на 16 на 13.

РЕДАКТИРОВАТЬ

Если, как предлагается в других сообщениях, ваши строки ВЫРАВНИВАЮТСЯ, чтобы вы могли использовать их как короткие, вы можете добавить первый и второй короткие, xor два байта вместе, и вы получите уникальный 8-битный ключ (ну, собственно, семь). Может, твоего тоже стоит. Это 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
2
ответ дан 30 November 2019 в 04:11
поделиться

Выглядит достаточно хорошо для меня. Вопрос в том, является ли сама хэш-функция достаточно узким местом, чтобы оправдать постоянные усилия по устранению из нее еще одной или двух простых двоичных операций. Учитывая, что, похоже, задействован доступ к файлам, я сомневаюсь в этом, конечно, не зная никаких подробностей об общей обработке.

EDIT:

Может быть, вы можете посмотреть, не найдется ли какая-нибудь пара символов, при сложении которых получаются уникальные младшие биты (4, 5 или 6):

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

Если сложение не поможет, может быть, можно использовать одну из других операций & | ^. Если и это не поможет, может быть, использовать три символа.

1
ответ дан 30 November 2019 в 04:11
поделиться

Действительно ли вам нужно сопоставление между хэшем и индексом месяца для подсчета? Вы могли бы отказаться от поиска, если бы вместо месяца возвращали хэш и использовали его для подсчета. В ответе x4u последняя строка хэш-функции может выглядеть как

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

и вы все равно сможете выполнять суммирование, сортируя результаты только в конце, а не внутри цикла.

1
ответ дан 30 November 2019 в 04:11
поделиться

Я бы начал с подробного описания вашего более крупного процесса, чтобы убедиться, что вы не занимаетесь преждевременной оптимизацией.

На первый взгляд это выглядит довольно быстро, но если память действительно дешевая, может быть лучше просто использовать еще более разреженный массив и позволить вашему кешу делать часть работы. Например (и здесь не о чем думать), что если вы просто добавите короткий короткий , найденный в первых двух байтах, к короткому короткому в следующих двух. Это включает в себя как первый, так и четвертый символы, поэтому, предположительно, он должен выдавать ваши 12 различных значений и не включает извлечение битовых полей, которое может плохо оптимизироваться. Затем сделайте соответствующий массив bucket [] имеющим 64К записей, только 12 из которых когда-либо попадают. Если все сработает правильно, эти 12 записей в конечном итоге займут часть вашего кэша D, и вы обменяли несколько арифметических операций на индекс в кешированный массив большего размера.

Но делайте профилирование как до, так и после любых попыток ускорить арифметические операции, и не беспокойтесь об оптимизации там, где это фактически не сэкономит время. (Я знаю, что Pax знает об этом, но это обязательное предупреждение, прилагаемое к любому обсуждению оптимизации.)

3
ответ дан 30 November 2019 в 04:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: