Есть ли способ сделать этот хэш-поиск быстрее?
У меня есть требование (очень) быстро обрабатывать строки ограниченного диапазона, подсчитывая их значения. Входной файл имеет вид:
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
, дубликаты столбцов u
и столбцы 5 повторяются дубликаты <space>
(есть и другие дубликаты, но одного достаточно, чтобы предотвратить одностолбцовую хеш-ключ).
Однако, используя первый и четвертый столбцы, я получаю значения Ju
, Fr
, Mc
, Ai
, M<space>
, Je
, Jy
, Au
, St
, Oo
, Ne
и De
, которые являются уникальными. В этом файле не будет недопустимых значений, поэтому мне не нужно беспокоиться о некорректных кодах для входных данных.
Просмотрев шестнадцатеричные коды для символов, я обнаружил, что могу получить низкие уникальные значения только с помощью ANDing со стратегическими значениями:
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.
Ответы
Ответ 1
Здесь самая маленькая последовательность, которую я смог найти для 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 ])];
}
(На самом деле, вкладка [] должна быть здесь 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
Ответ 2
Я согласен с остальными, что возможностей для улучшения нет. Все, что я могу предложить, - это небольшая таблица поиска, которая работает с тем же количеством операций, которые могут задержать ее дольше в кэше 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
Ответ 3
Это проверено для 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
Я бы начал с подробного профиля вашего более крупного процесса, чтобы убедиться, что вы не занимаетесь преждевременной оптимизацией.
Это выглядит довольно быстро на первый взгляд, но если память действительно дешевая, может быть лучше просто использовать еще более разреженный массив и позволить вашему кешу выполнять некоторую работу. Например (и придумывая здесь манжету), что, если вы просто добавите short
, найденный в первых двух байтах, к short
в следующих двух. Это включает в себя как первый, так и четвертый символы, поэтому при угадывании он должен генерировать ваши 12 различных значений, и он не включает экстракции битового поля, которые могут не оптимизироваться хорошо. Затем сделайте сопоставление массива bucket[]
с 64 КБ, только 12 из которых когда-либо попадаются.
Если это правильно, те 12 записей в конечном итоге занимают часть вашего кеша D, и вы обменяли несколько арифметических операций для индекса в кешированный более крупный массив.
Но делайте профили как до, так и после каких-либо ошибок, пытаясь сделать арифметику быстрее, и не беспокойтесь, оптимизируя, где это фактически не сэкономит время. (Я знаю, что Pax знает это, но его обязательное предупреждение придается любому обсуждению оптимизации.)
Ответ 5
Хорошо, так как все в SO, я все в нем для rep..; *) Как я уже писал в комментариях выше, нижний конец ваших целевых архитектур имеет размер кеша в 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.
ИЗМЕНИТЬ
Если, как было предложено другими сообщениями, ваши строки ARE выровнены, так что вы можете использовать их как шорты, вы можете добавить первый и второй короткие, 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
Ответ 6
Выглядит неплохо для меня. Вопрос в том, достаточно ли самой хэш-функции узкого места, чтобы оправдать продолжающиеся усилия по устранению одной или двух более простых двоичных операций. Учитывая, что доступ к файлам, похоже, задействован, я сомневаюсь, конечно, не зная подробностей об общей обработке.
EDIT:
Возможно, вы могли увидеть, если вы найдете пару символов, которые при добавлении будут иметь уникальные младшие биты (4, 5 или 6):
(str[1] + str[2]) & 0x1f
Если сложение не будет выполнено, возможно, одна из других операций & | ^
. Если это не поможет, возможно, используя три символа.
Ответ 7
В ASCII, если вы берете month[0] ^ month[2] ^ month[3]
, тогда вы получаете уникальный хеш с максимальным значением 95 (июль), что должно позволить вам уменьшить размер таблицы справедливым битом (и минимальным значением 20 (май), поэтому вычитание уменьшает его снова).
То же самое может быть неверно в EBCDIC, но что-то подобное может быть.
Ответ 8
Вам действительно нужно сопоставление между хэшем и индексом месяца, чтобы сделать подсчет? Вы можете устранить поиск, если вместо возвращения месяца вы вернули хэш и используете его для подсчета голосов. В x4u answer последняя строка хеш-функции может выглядеть как
return ( ( str[ 1 ] >> 4 ) & 1 ) + ( str[ 2 ] & 0x1f )
и вы все равно сможете делать суммы, сортируя результаты только в конце, а не внутри цикла.