我有6字节的格式为cccnnn
的字符串,其中c
是字符AZ(ASCII 65-90),n
字符0-9(ASCII 48-57) 。总共有26个 3 * 10 3 = 17,576,000个不同的组合。
我想创建一个完美的哈希函数,将这种类型的每个字符串映射到一个整数索引,我希望它尽可能快。该功能不必极小,但范围不能太大。组合数量的两倍可能没问题,但最好不要超过这个数量,因为每个字符串将映射到位数已经大约2MB的位。
我能想到的最明显,也是最好的解决方案是将字符串解释为基数为26和10的数字,然后执行所需的乘法和减法以得到[0,17576000-范围内的整数1]:
inline word hash1(unsigned char *buffer)
{
return (((((word) buffer[0] * 26 + buffer[1]) * 26
+ buffer[2]) * 10 + buffer[3]) * 10
+ buffer[4]) * 10 + buffer[5] - 45700328;
}
此处buffer[0-5]
包含字符索引,word
是typedef
uint64_t
和45700328 = ((((65*26+65)*26+65)*10+48)*10+48)*10+48
,它将字符转换为正确的基础,而非写入(buffer[0] - 65) * 26
等(它节省了一些减法。)
我想到了改善这种情况的方法。我的一个想法是使用相同的原理,但使用位移而不是乘法。我不得不围绕角色的顺序混合,以找到尽可能少的操作的解决方案。我发现乘以260和10只需要两次移位和一次加法,分别是(x << 8) + (x << 2)
和(x << 3) + (x << 1)
,并且我可以使用它来分别计算表达式((x2*260+x1)*260+x0)*10+(x4*260+x3)*260+x5-47366978
中的每个乘法,其中hi = buffer[i]
。实施是:
inline word hash1(unsigned char *buffer)
{
word y0, y1, y2, y3, y4;
word x0 = buffer[0]; word x1 = buffer[1];
word x2 = buffer[2]; word x3 = buffer[3];
word x4 = buffer[4]; word x5 = buffer[5];
y0 = (x4 << 2) + (x4 << 8) + x3;
y1 = (y0 << 2) + (y0 << 8) + x5;
y2 = (x2 << 2) + (x2 << 8) + x1;
y3 = (y2 << 2) + (y2 << 8) + x0;
y4 = (y3 << 3) + (y3 << 1) + y1;
return y4 - 47366978;
}
不幸的是,hash2
比hash1
慢一点。这是我用完好主意的地方。我当然可以尝试创建一个函数,只需移动每个字符的有效位,将它们叠加在一起,形成一个2 27 位数,但这需要16MB向量=太大
因此,无论是使用相同的原理并更改代码还是使用完全不同的原则,如何根据我在第一段中提到的要求使哈希函数更快? / p>
答案 0 :(得分:2)
使用3个A-Z的5个最低有效位和多个数字到10位乘积中:2 15 + 10 &lt; 2 * 17576000。
如果<<
快于*
,则预计会更快。 YMMV
使用const
指针可以进行可能没有准备就绪的优化。
inline size_t hash3x26k(const unsigned char *buf) {
return 0x1FFFFFF
& (((buf[0] << 20) ^ (buf[1] << 15) ^ (buf[2] << 10))
^ ((buf[3] * 100 + buf[4] * 10 + buf[5])));
}
测试代码以显示完美哈希值并且不超过2x 26 3 * 10 3 条目。
unsigned char z[0x1FFFFFF + 1u];
int main() {
size_t max = 0;
unsigned char b[7] = { 0 };
for (b[0] = 'A'; b[0] <= 'Z'; b[0]++) {
for (b[1] = 'A'; b[1] <= 'Z'; b[1]++) {
for (b[2] = 'A'; b[2] <= 'Z'; b[2]++) {
for (b[3] = '0'; b[3] <= '9'; b[3]++) {
for (b[4] = '0'; b[4] <= '9'; b[4]++) {
for (b[5] = '0'; b[5] <= '9'; b[5]++) {
size_t i = hash3x26k(b);
if (i > max) max = i;
//printf("%s %zu\n", b, i);
if (z[i]++) {
printf("%s %zu\n", b, i);
exit(-1);
}
}
}
}
}
}
}
printf("%zu\n", max + 1);
return 0;
}
需要29,229,056个桶。
答案 1 :(得分:2)
[更新10/27]
简单方法将48位数组用作整数,然后使用特定数字 mod 。可以使用原始ASCII字符串。无需从每个字符中减去26或10,甚至删除'\n'
。不需要任何乘法。只需1 %
次操作。
typedef union {
unsigned char b[8];
uint64_t u64;
} U;
// Return a value in the range 0 to 33,541,273 which is less than 2*26*26*26*10*10*10
inline uint32_t hash3x26_mod(const unsigned char *buf) {
static const uint32_t mod = 0X1FFCC9A; // Determined by tests, assume little endian.
return (uint32_t) (x->u64 % mod);
}
用法
fgets(&U.b, sizeof U.b, istream);
// Assume U.b[7] == 0
// Assume U.b[6] == 0 or `\n`, consistently
uint32_t perfect_AAA000_hash = hash3x26k_1(&U);
或者,虽然OP不想使用更宽的索引,但下面很快会生成一个带有*
,>>
和&
<的30位非碰撞哈希值/ p>
inline size_t hash3x26k_1(const unsigned char *buf) {
typedef union {
unsigned char b[6];
uint64_t u64;
} U;
U *x = (U*) buf;
uint64_t y = (x->u64 * (1ull + 16 + 16*16 + 16*16*8 + 16ull*16*8*8 + 16ull*16*8*8*8))
>> 17;
return (size_t) (y & 0x3FFFFFFF);
}
我怀疑是某个TBD常数的乘法,而使用0x01FF_FFFF掩码也可以。
答案 2 :(得分:1)
这是我对哈希问题的看法。方法是使用较少的中间值和更多常量,以便编译器轻松优化代码。
#include <stdio.h>
#include <stdint.h>
uint64_t hash1(unsigned char *buffer)
{
return
(
(
(
(
(uint64_t)
buffer[0] * 26
+ buffer[1]
) * 26
+ buffer[2]
) * 10
+ buffer[3]
) * 10
+ buffer[4]
) * 10
+ buffer[5]
- 45700328;
}
uint64_t hash2(const unsigned char *buffer)
{
uint64_t res
= buffer[0] * 676000
+ buffer[1] * 26000
+ buffer[2] * 1000
+ buffer[3] * 100
+ buffer[4] * 10
+ buffer[5] * 1;
return res - 45700328u;
}
int main(void)
{
unsigned char a, b, c, d, e, f;
unsigned char buf[7] = { 0 }; // make it printable
uint64_t h1, h2;
for (a = 'A'; a <= 'Z'; a++) {
buf[0] = a;
for (b = 'A'; b <= 'Z'; b++) {
buf[1] = b;
for (c = 'A'; c <= 'Z'; c++) {
buf[2] = c;
for (d = '0'; d <= '9'; d++) {
buf[3] = d;
for (e = '0'; e <= '9'; e++) {
buf[4] = e;
for (f = '0'; f <= '9'; f++) {
buf[5] = f;
h1 = hash1(buf);
h2 = hash2(buf);
if (h1 != h2) {
printf("Meh: %s mismatch: %llx %llx\n", (const char *)buf,
(unsigned long long)h1, (unsigned long long)h2);
return 1;
}
}
}
}
}
}
}
return 0;
}
一些简单的gprofing表明hash2()更快,至少大部分时间都是如此。每次运行的gprof结果都有所不同。你可能想要自己试验一下。