上一篇主要是讲了 Table
和 MetaMethod
的一些设计实现,谈论到了 Lua 会对 元方法的字符串名字作缓存,同时提到了 Lua 字符串分为长短字符串。这一篇主要是谈论一下 Lua 的长短字符串是怎么设计的?为什么要分长短这两种类型?
TString 结构
可以看到字符串内部会记录哈希值,每个字符串被创建出来就不能被改写,因此为了节约内存,Lua会复用相同的字符串,但是逐字节比较太慢了,因此会预处理将字符串hash,存入字符串的 hash
字段中。
字符串的实际内容会追加到 TString
的后面。
1 | typedef struct TString { |
短字符串全局只有一份,Lua解释器会将其存到 stringtable
这个结构中。字符串 hash
会根据 global_State
的 seed
进行哈希。
1 | typedef struct stringtable { |
为什么字符串要分长短?
LUAI_MAXSHORTLE
作为分界来区分长短字符串,默认为40字节
1 |
|
其实在 Lua 5.3 之前,字符串并不分长短,之所以现在要分主要是因为 Hash Dos
攻击。
Lua 中的字符串会进行 Hash,然后将其放入 strt 中,如果发生了冲突,就会用最简单的开链法,将相同Hash值的字符串串起来。
Lua 5.2.0 中 创建字符串的规则比较简单,凡是阅读过源码的,都能大量构造出相同哈希值的字符串,导致 Lua解释器不得不根据链表上的字符串逐一比对字符,最终会因为比较字符串耗尽 CPU
资源。因此 Lua 5.2.1 之后才会采用 global_State 的 seed 去随机构造哈希。
1 | TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { |
Lua 5.3.6 随机生成随机数种子。
1 |
|
随机数种子生成规律非常有趣 它根据以下几点进行随机生成
- 根据 lua_State 的地址
- 根据 虚拟机的 运行时间
- 根据 luaO_nilobject 常量的地址
- 根据 lua_newstate 函数的地址
最后调用了 luaS_hash()
来创建 hash seed,这个函数即用来hash字符串,同时又用来创建 hash seed。
1 |
|
可以看到 luaS_hash
对字符串 hash 的时候,如果字符串过长,就会跳过部分字符来提高性能。
当冲突的字符串越来越多的时候,查询相同字符串的效率会越来越差,不过没关系,当字符串的数量 > strt的大小,会分配一个原strt两倍大小的哈希表。同时 将原有重新进行 Hash,放入新的哈希表中。同理,当字符串的数量 < strt的大小 / 4 的时候,strt 就会缩小为 原先的一半。
1 | static void checkSizes(lua_State* L, global_State* g) { |
创建字符串
1 | TString* luaS_newlstr(lua_State* L, const char* str, size_t l) { |
创建短字符串
短字符串会直接进行 hash
若冲突则用开链法链起来。
1 | static TString* internshrstr(lua_State* L, const char* str, size_t l) { |
创建长字符串
没有立即进行 hash
而是留到之后,再进行 hash
。
1 | TString* luaS_createlngstrobj(lua_State* L, size_t l) { |
在源码中,我只找到一处 对长字符串进行 hash
,就是在上一篇的 table
中,当要对 字符串key 进行 hash
的时候才 hash
(它都需要哈希了才哈希,是否可以看作 Lua 并不想对长字符串进行哈希呢?)
1 | static Node *mainposition (const Table *t, const TValue *key) { |