【问题标题】:Creating a faster perfect hash function for 6-byte strings为 6 字节字符串创建更快的完美哈希函数
【发布时间】:2016-10-21 09:03:38
【问题描述】:

我有格式为 cccnnn 的 6 字节字符串,其中 c 是字符 A-Z (ASCII 65-90),n 是字符 0-9 (ASCII 48-57)。总共有 263 * 103 = 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]包含字符索引,worduint64_t45700328 = ((((65*26+65)*26+65)*10+48)*10+48)*10+48typedef,它将字符转换为正确的基数而不是写(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;
}

不幸的是,hash2hash1 慢一点。这就是我没有好主意的地方。我当然可以尝试制作一个简单地移动每个字符的有效位的函数,将它们堆叠在一起,形成一个 227 位数,但这需要 16MB 向量 = 太大.

那么,无论是使用相同的原理改代码还是使用完全不同的原理,如何才能让我的哈希函数更快按照我在第一段中提到的要求呢?

【问题讨论】:

  • 第一个不应该是(((((buffer[0]-'A') * 26 + (buffer[1]-'A')) * 26 + (buffer[2]-'A')) * 10 + (buffer[3]-'0')) * 10 + (buffer[4]-'0')) * 10 + (buffer[5]-'0')吗?
  • 我将所有字符减法预先计算为 45700328 的一个减法,因此它们在这方面是等价的。我还需要进行(word) 转换,以便我们处理uint64_t 值而不是unsigned char 值,否则会由于大乘法而导致溢出。
  • 如果您使用的是 x86,一个建议是将基数从 26 更改为 27,因为 lea 指令可用于非常快速地乘以 3、5 或 9
  • @SJuan76 过早优化不好,但在这种情况下是合理的。
  • @SectoKia 我没说错。仅仅乘以 27 可能比乘以 26 快。看看反汇编,你会看到

标签: c string optimization hash-function


【解决方案1】:

使用 3 A-Z 的 5 个最低有效位并将这些数字乘以 10 位乘积:215 + 10

如果&lt;&lt;* 快,预计这会更快。 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 263 * 103 个条目。

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 个存储桶。

【讨论】:

  • 一个小的区别是 OP 中的 b[0] 到 b[2] 位于 0-25 范围内,而不是 'A'-'Z'
  • @LưuVĩnhPhúc 好点。在我看来,不需要将字符转换为主要范围 0-25 和 0-9 的时间和功能,并且 faster 代码可以从 ASCII 字符开始。
  • 聪明的主意!我已经尝试过您的版本,并将其与 Born 的 hash2 函数进行了比较。如果没有优化标志,它们的性能大致相同,但使用 -Ofast 和类似的 Bjorn 在我的 64 位 i3-3225 CPU 上仍然更快。你做过比较吗?
【解决方案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 位非冲突哈希,其中包含 *&gt;&gt;&amp;

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 进行屏蔽也可以。

【讨论】:

  • 是否有太多数学证明必须有一个 mod 值来做到这一点?你是在强行吗?
  • @SectoKia 是否有太多的数学证明?可能。你是在强行吗?是的。
  • 继续努力,这很有趣。我有一个理论认为效果很好的值是 n^2/n-1,其中 n 是 AAA000 null null。
  • @SectoKia 没有看到 that value 有什么特别之处(也没有一些变化)。我已经测试了值的前 1/4 260*260*260260*260*260*2 以及该范围内的各种随机值。使用上面代码的最终&amp; 0x1FFFFFF,或者甚至更宽松的% 260u*260*260*2 值以上260*260*260*2 也可以进行测试
【解决方案3】:

这是我对哈希问题的看法。方法是使用更少的中间值和更多的常量,使编译器更容易优化代码。

#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 结果略有不同。您可能想自己进行实验。

【讨论】:

  • 感谢您的意见。我尝试了您的代码,确实,您的 hash2 函数似乎要快一些!你有理由不inline 散列函数吗?我认为它会通过消除函数调用开销来加快执行速度。我想这取决于您使用的编译器标志,因为编译器可能会自动内联函数 - 例如,GCC 中的 -finline-functions 标志。
  • @Max 我没有内联函数的原因是我希望能够按名称分析它们。顺便说一句:我对分析数据的精度有些担心,它变化太大。昨天,我仔细查看了这两个函数的汇编代码,AFAICT 没有任何抖动的原因。
猜你喜欢
  • 2015-02-25
  • 1970-01-01
  • 2014-09-02
  • 1970-01-01
  • 2012-03-21
  • 1970-01-01
  • 2018-06-19
  • 2015-03-09
相关资源
最近更新 更多