美文网首页程序员lua
[lua source code] luaS_hash

[lua source code] luaS_hash

作者: ffl | 来源:发表于2019-01-09 15:53 被阅读26次

上一节对global_State做了子模块划分,回顾一下global_State整理后的代码:

typedef struct global_State {
  // Version
  const lua_Number *version;      /* pointer to version number */

  // Hash
  unsigned int seed;              /* randomized seed for hashes */

  // Global Registry
  TValue l_registry;

  // Global String table
  stringtable strt;               /* hash table for strings */
  Mbuffer buff;                   /* temporary buffer for string concatenation */

  // Global Meta table
  TString *tmname[TM_N];          /* array with tag-method names */
  struct Table *mt[LUA_NUMTAGS];  /* metatables for basic types */

  // Global Thread list
  struct lua_State *mainthread;
  struct lua_State *twups;        /* list of threads with open upvalues */
  
  // Memory Allocator
  lua_Alloc frealloc;             /* function to reallocate memory */
  void *ud;                       /* auxiliary data to 'frealloc' */

  // GC Info
  GCInfo *gcinfo;

  // Error Recover
  lua_CFunction panic;            /* to be called in unprotected errors */
}

加深一下印象,也就是说global_State内部包含了下面这些重要部分:

  • Version:pointer to version number
  • Hash:randomized seed for hashes
  • Global Registry
  • Global String table
  • Global Meta table
  • Global Thread list
  • Memory Allocator
  • GC Info
  • Error Recover

其中,Version指向了版本指针,通常我们发布程序版本号都是外置的,Lua把版本号内置在全局状态里,使得可以在运行时动态判断自己的版本号。

而,Hash部分的seed则是用在散列表里面计算哈希时的种子,在Lua的代码里可以找到这段代码:

//lstring.c
unsigned int luaS_hash (const char *str, size_t l, unsigned int seed) {
  unsigned int h = seed ^ cast_uint(l);
  size_t step = (l >> LUAI_HASHLIMIT) + 1;
  for (; l >= step; l -= step)
    h ^= ((h<<5) + (h>>2) + cast_byte(str[l - 1]));
  return h;
}

第1眼看上去luaS_hash的代码没什么道理可言,其实一步步展开还是能理解的。首先,哈希函数有两类:

  • 加密用的哈希函数,例如SHA-256, SHA-512, MD5, RIPEMD-160等
  • 非加密用的哈希函数

luaS_hash即属于非加密用的哈希函数,在散列表里的主要作用是:

  • 把输入字符串随机映射到散列表的索引范围[0, len-1]内
  • 哈希应该在[0,len-1]内尽量符合均匀分布(Unifrom),也就是合法的输入被映射到[0,len-1]中任何一个地址的概率是相等的,这就使得输入经过哈希后可以得到一个“随机的地址”,以减少碰撞。

可见,它本质上是一种伪随机数生成器,而我们知道伪随机数生成器最古典的就是线性同余方法(wiki:Linear congruential generator(LCG)),它的算法如下:

N_{j+1}===(A*N_j+B)(mod M)

LCG算出来的伪随机数序列在模M之后就是一个周期整数序列,周期的大小决定了碰撞的概率。LCG的周期最大是M,但大部分情况都会少于M,如果要让LCG达到最大周期,应该要符合以下Hull–Dobell Theorem条件:

  1. B,M互质
  2. M的所有质因子都能整除A-1
  3. 若M是4的倍数,A-1也是。
  4. A,B,N_0都比M小
  5. A,B都是正整数

LCG本身只是伪随机数生成器,需要满足均匀分布以减少碰撞才能在散列表里面使用。因此一个非密码学使用的哈希函数是否足够好(good hash function),就要看它是否足够均匀分布。

先看一个常见的Hash函数的实现,叫做DJBX33A (Daniel J. Bernstein, Times 33 with Addition):

unsigned int DJBHash(char* str, unsigned int len){
   unsigned int hash = 5381;
   unsigned int i    = 0;

   for(i = 0; i < len; str++, i++) {   
      hash = ((hash << 5) + hash) + (*str);
   }   

   return hash;
}

在for循环里做的hash = ((hash << 5) + hash) + (*str);实际上就是hash = hash*33+str[i],就是LCG递归算法的一个中间步骤。对比一下公式,可以看到:

  • N_{j+1}=左边的hash
  • N_j=右边的hash
  • A=33
  • B=str[i],str是ASCII字符串,str[i]是8 bits,从而B是[0,256)范围内的整数。
  • M=2^{32}(或者 2^{64},取决于int的位数)

可以看到A,B,M满足如下的几个性质:

  • 如果B是奇数,B和M互质,至少一半的概率
  • M的所有质因子是2,它可以整除A-1=32
  • M(2^{32} 或者 2^{64} )是4的倍数,A-1=32也是4的倍数
  • A,B,N_0都比M小
  • A,B是正整数(除0外)。

因此DJBX33A这个哈希函数具有很好的最大周期性,从而尽可能减少碰撞。但这只解释了一半原因,另一半原因是:

  • A选择33这个接近2^n的数字,可以充分利用计算机里面用shift-and-add的方式计算乘法,即:h^33 = h^32+h = (h<<5)+h
  • 假设h是32位,则二进制表达式为A1A2...A32,其中Ai取0或者1,那么h<<5的二进制表达式是A6A7...A3200000,那么(h<<5)+h实际上把h的bit做了一次位移后再逐位和h自己相加,得到的h^33的每一个bit就尽可能均匀地混合了原来h的每一个bit信息。
  • 从这个角度来说,如果h<<n里面的n太大,则会每次只混合了比较少的h原来bit的信息;如果n太小,则h<<n的每个bit离h的每个bit很近,这会需要更多的迭代才能让h的32个bit混合均匀。而5和32互质,也有利于多次迭代中让h的每个bit有更大概率与输出h的每个bit都有机会做混合。
  • 考虑B=str[i],str是ASCII字符串,str[i]是8 bits。ASCII的前4个bit都含有0x3。则:
    • 迭代一次:h1=h0<<n+h0+str[i]
    • 迭代二次:h2=h1<<n+h1+str[i+1] = h1'+str[i+1]
    • 可以看到如果n是8,h1'的低8位就是str[i],那么h1'和str[i+1]的[4,8]位之间就会有相同的值做混合,不利于增加信息。n取4和2也是一样的问题。而n取5则可以让str[i]的低4位有更多机会和str[i+1]的高4位做混合。

上面这段解释来自参考资料[2]的评论,而参考资料[3]的评论下则有一段对DJB里的这几个魔数的来源做了另一番解释:直接暴力计算对比,you can you up!

/*
* DJBX33A (Daniel J. Bernstein, Times 33 with Addition)
*
* This is Daniel J. Bernstein's popular `times 33' hash function as
* posted by him years ago on comp.lang.c. It basically uses a function
* like ``hash(i) = hash(i-1) * 33 + str[i]''. This is one of the best
* known hash functions for strings. Because it is both computed very
* fast and distributes very well.
*
* The magic of number 33, i.e. why it works better than many other
* constants, prime or not, has never been adequately explained by
* anyone. So I try an explanation: if one experimentally tests all
* multipliers between 1 and 256 (as RSE did now) one detects that even
* numbers are not useable at all. The remaining 128 odd numbers
* (except for the number 1) work more or less all equally well. They
* all distribute in an acceptable way and this way fill a hash table
* with an average percent of approx. 86%.
*
* If one compares the Chi^2 values of the variants, the number 33 not
* even has the best value. But the number 33 and a few other equally
* good numbers like 17, 31, 63, 127 and 129 have nevertheless a great
* advantage to the remaining numbers in the large set of possible
* multipliers: their multiply operation can be replaced by a faster
* operation based on just one shift plus either a single addition
* or subtraction operation. And because a hash function has to both
* distribute good _and_ has to be very fast to compute, those few
* numbers should be preferred and seems to be the reason why Daniel J.
* Bernstein also preferred it.
*
*
* -- Ralf S. Engelschall <rse@engelschall.com>
*/

除了DJBX33A哈希函数外,下面两个著名的哈希函数也是类似的做法:

回到luaS_hash,与DJBX33A哈希函数是类似的,其中:

  • 通过随机种子unsigned int h = seed ^ cast_uint(l);来防御哈希碰撞攻击
  • 通过LUAI_HASHLIMIT控制step,通过控制step可以控制哈希计算的速度
  • (h<<5)(h>>2) 做混合

可以看到seed正是使用global_State里的seed,下面的代码验证了这点:

static TString *internshrstr (lua_State *L, const char *str, size_t l) {
  TString *ts;
  global_State *g = G(L);
  stringtable *tb = &g->strt;
  unsigned int h = luaS_hash(str, l, g->seed);
  TString **list = &tb->hash[lmod(h, tb->size)];
  ...
}

而global_State->seed的初始化如下:

LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
  ...
  g->seed = luai_makeseed(L);
  ...
}

进一步,我们看下luai_makeseed(L);的代码:

static unsigned int luai_makeseed (lua_State *L) {
  char buff[3 * sizeof(size_t)];
  unsigned int h = cast_uint(time(NULL));
  int p = 0;
  addbuff(buff, p, L);  /* heap variable */
  addbuff(buff, p, &h);  /* local variable */
  addbuff(buff, p, &lua_newstate);  /* public function */
  lua_assert(p == sizeof(buff));
  return luaS_hash(buff, p, h);
}

这就是随机种子初始化的地方,可以看到最后一句调用了luaS_hash来递归的生成种子本身,而buff,p,h就是初始值,其中h就是“生成随机种子的种子”,分拆下:

  • 字符串buff包含了heap variable,local variable,address of lua_newstate三种信息
  • p就是buff的长度
  • 而种子h使用时间函数time来初始化,实际上用户可以在 luaconf.h 中配置 luai_makeseed 定义自己的随机方法。

待续

至此,我们分解清楚了global_State->seed的作用,以及luaS_hash函数背后的原理。下一次,我们会继续探索global_State-> l_registry。对技术背后原理的好奇,是前进的最大动力。

[1] https://en.wikipedia.org/wiki/Linear_congruential_generator
[2] https://stackoverflow.com/questions/1579721/why-are-5381-and-33-so-important-in-the-djb2-algorithm
[3] https://stackoverflow.com/questions/10696223/reason-for-5381-number-in-djb-hash-function/13809282#13809282
[4] https://en.wikipedia.org/wiki/List_of_hash_functions#Non-cryptographic_hash_functions

相关文章

网友评论

    本文标题:[lua source code] luaS_hash

    本文链接:https://www.haomeiwen.com/subject/lodbrqtx.html