在浏览器的实现中,处理HTML和CSS涉及大量的字符串操作,这些操作通常包括字符串的比较、查找和匹配。如果使用普通的字符串对这些进行操作,在面临大量DOM元素和CSS规则时会导致效率低下。
例如,当解析CSS时,属性名如color
、margin
、padding
等在内部可以被转换为静态字符串。在后续的样式计算和匹配过程中,只需通过比较这些属性的ID,而不是一遍遍地比较完整的字符串。这种比较是通过简单的指针或整数比较来完成的,这要比字符串的字节级比较快得多。
WTF中的Static Strings
Chromium使用了Web Template Framework (WTF) 提供的一系列高效的字符串处理机制。
WTF模块是一组底层实用工具和类的集合,它为Chromium提供了包括字符串在内的基础设施支持。在字符串处理方面,WTF提供了一个特殊的类别,即静态字符串(Static Strings)。静态字符串是在编译时已知的字符串常量,它们在浏览器的整个生命周期中保持不变。
静态字符串机制的核心在于,它给每一个字符串分配了一个唯一的标识符(ID)。这些字符串在编译时就被收集和注册到一个全局的字符串表中。当代码需要使用这些字符串时,它不是直接操作原始的字符数据,而是通过这些唯一的ID来引用字符串。这种机制类似于字符串池或者字符串的符号表示(symbolic representation)。
如何计算字符串Hash
这个唯一的标识符(ID)本质上是根据字符串内容计算出来的hash。因此,优秀的hash算法可以最大程度避免冲突。在WTF中,hash计算代码如下:
template <typename T, UChar Converter(T)>
static unsigned ComputeHashAndMaskTop8Bits_internal(const unsigned char* data, unsigned length) {
StringHasher hasher;
hasher.AddCharactersAssumingAligned_internal<T, Converter>(data, length);
return hasher.HashWithTop8BitsMasked();
}
// StringHasher 的 AddCharactersAssumingAligned_internal实现如下:
template <typename T, UChar Converter(T)>
void AddCharactersAssumingAligned_internal(const unsigned char* data, unsigned length) {
DCHECK(!has_pending_character_);
static_assert(std::is_trivial_v<T> && std::is_standard_layout_v<T>,
"we only support hashing POD types");
bool remainder = length & 1;
length >>= 1;
while (length--) {
T data_converted[2];
std::memcpy(data_converted, data, sizeof(T)*2);
AddCharactersAssumingAligned(Converter(data_converted[0]), Converter(data_converted[1]));
data += sizeof(T)*2;
}
if (remainder) {
T data_converted;
std::memcpy(&data_converted, data, sizeof(T));
AddCharacter(Converter(data_converted));
}
}
// StringHasher 的 HashWithTop8BitsMasked实现如下:
unsigned HashWithTop8BitsMasked() const {
unsigned result = AvalancheBits();
// Reserving space from the high bits for flags preserves most of the hash's
// value, since hash lookup typically masks out the high bits anyway.
result &= (1U << (sizeof(result) * 8 - kFlagCount)) - 1;
// This avoids ever returning a hash code of 0, since that is used to
// signal "hash not computed yet". Setting the high bit maintains
// reasonable fidelity to a hash code of 0 because it is likely to yield
// exactly 0 when hash lookup masks out the high bits.
if (!result)
result = 0x80000000 >> kFlagCount;
return result;
}
// AvalancheBits 实现如下
unsigned AvalancheBits() const {
unsigned result = hash_;
// Handle end case.
if (has_pending_character_) {
result += pending_character_;
result ^= result << 11;
result += result >> 17;
}
// Force "avalanching" of final 31 bits.
result ^= result << 3;
result += result >> 5;
result ^= result << 2;
result += result >> 15;
result ^= result << 10;
return result;
}
以上代码的逻辑如下:
总体目标是用于计算字符串的哈希值,并在最终结果中屏蔽掉顶部的8位。使用了StringHasher
类来逐步构建哈希值。
下面是逐步分析这个哈希计算过程:
ComputeHashAndMaskTop8Bits_internal
这是外部调用的模板函数。它接受一个指向数据的指针和数据的长度,然后通过调用StringHasher
的AddCharactersAssumingAligned_internal
方法来添加字符数据,并最终调用HashWithTop8BitsMasked
来获取哈希值并屏蔽掉顶部的8位。
AddCharactersAssumingAligned_internal
这个模板方法处理实际的字符数据。它的工作原理如下:
- 它首先检查
has_pending_character_
标志,确保没有挂起的字符。 - 它使用
static_assert
来确保模板类型T
是平凡的(trivial)和具有标准布局(standard layout),这表示着T
是一个简单的数据类型,可以安全地使用内存复制。 - 然后,它检查
length
是否是奇数,并将其保存在remainder
中。length
被除以2,因为我们一次处理两个字符。 - 在一个循环中,它每次处理两个字符。每次迭代中,它使用
std::memcpy
将两个字符的数据复制到data_converted
数组中,然后使用Converter
将字符转换,并使用AddCharactersAssumingAligned
添加转换后的字符到哈希计算中。 - 如果有剩余的一个字符(即原始长度是奇数),则将其添加到哈希中。
HashWithTop8BitsMasked
这个方法返回最终的哈希值,并屏蔽掉顶部的8位。操作步骤如下:
- 它首先调用
AvalancheBits
来完成哈希计算。 - 然后,它屏蔽掉顶部的
kFlagCount
位,通常是8位。 - 它还确保哈希值不会是0,因为0被用来表示“尚未计算哈希值”。如果结果为0,则设置最高位为1。
AvalancheBits
这个方法处理最终阶段的哈希计算,以确保哈希值的每一位都能受到前面位的影响(这称为雪崩效应)。步骤如下:
- 如果有挂起的字符,它将其添加到哈希计算中,并进行一系列的位操作来混合位。
- 然后,它对结果进行一系列的位移和加法操作,以确保最终的哈希值拥有好的分布性和随机性。
这段代码使用了一种有效的方法来逐步计算字符串的哈希值,并通过一系列特定的位操作来确保哈希值的分布性和随机性。
在最后阶段,它屏蔽掉顶部的8位,并确保哈希值不为0。这种哈希机制在处理字符串时非常高效,因为它可以轻松地将字符串映射到一个较小的整数空间,同时减少冲突的可能性。
屏蔽掉顶部的8位通常是为了在哈希值中保留空间用于特定的标志或控制位。在一些数据结构中,需要在同一个整数值中存储哈希值和其他状态或标志信息。另外hash主要的离散空间集中在地位区域,因此屏蔽顶部8位不会造成过多的冲突。
如何让计算Hash更快
hash除了要避免冲突,更重要的是要足够快,否则我们最初希望用hash作为字符串ID来加速性能的想法反而达不到目的了。
关于这段Hash计算的深入设计,在源文件中有所介绍,这是 Paul Hsieh’s SuperFastHash的实现,具体的文章在这里: http://www.azillionmonkeys.com/qed/hash.html
为了方便读者,这篇文章使用AI总结的主要内容如下:
这篇文章讲述了哈希函数的研究和发展。作者在过去的工作中被要求研究哈希函数,并与老板就哈希函数的设计发生了争议。作者主张使用可根据表大小定制的LFSRs或CRCs,而老板则倾向于使用简单的取模质数操作,并引用了30年前Knuth的《计算机程序设计艺术》。尽管作者展示了取模质数方法存在严重碰撞的例子,但老板仍然坚持自己的观点。
争议最终由一位同事通过发现Bob Jenkins的哈希函数得以解决,该函数在碰撞分析方面基于更好的分析,并且性能优于两位的建议。作者在之后的项目中偶尔参考了这个网页,并注意到了“一次一个哈希”和“FNV哈希”这两种方法的添加。对于Bob Jenkins的函数,代码有些混乱,使用了许多神秘的常数,作者并不理解它们是如何构建的。而“一次一个哈希”和“FNV哈希”则非常简洁,魔法常数很少。
Bob Jenkins本人指出FNV哈希的性能超过了他自己的函数,因此作者一开始就接受了这一观点,并开始在所有情况下盲目使用FNV哈希。之后,作者在项目中真正测量了性能后,决定需要认真研究这个问题。
作者注意到,在Athlon XP系统上,Bob Jenkins的函数在性能上基本上超过了所有其他函数(包括FNV函数)。这与Bob Jenkins的说法相矛盾,解释是因为Bob Jenkins在Pentium上进行测量,而Pentium IV的移位操作很慢,这减慢了除了FNV以外的所有哈希函数的速度。而Opteron/Athlon 64架构有一个极大改进的整数乘法器,这表明FNV哈希在这个系统上也应该表现良好。
作者想要理解这些函数的真正性能限制,并看看是否可以通过重新编码来帮助提高性能。但是,由于Bob Jenkins的代码过于复杂,编译器或乱序CPU架构很难找到其中的并行性。相反,CRC和“一次一个哈希”是完全指令依赖的。因此,作者将输入数据分成奇偶字节,并行计算两个CRC和“一次一个哈希”,然后在最后将一个合并到另一个中,这显著提高了这些函数的性能,但仍然没有达到Bob Jenkins哈希的性能。
作者接着尝试了解这些函数的性能瓶颈,发现除了Bob Jenkins的函数外,其他函数都是基于每次消耗一个8位字节的输入数据,并以某种单向方式将每个字节混合到一个32位累加器中,然后可能经过后处理后直接输出。作者尝试使用更少的操作,并将输入片段的大小从8位增加到16位,这在x86架构上有很大的性能影响,因为这些架构支持非对齐的16位字访问。作者编写了一个程序来搜索在需要最终修正前提供最大雪崩效应的参数,并且在寻找可以完成对所有实际输入位的雪崩的参数集时添加了等效于在输入中填充固定数量零的内循环展开的指令。
作者惊讶地发现,在几个小时的工作后,轻易地找到了具有所有这些属性的哈希函数,并通过简单的统计测试验证了它具有与均匀随机映射等效的分布。
真相的时刻在性能测试中到来了——鉴于架构,这是一个预定的结论。作者的哈希函数比Bob Jenkin的函数快了大约66%。
文章还提到了一些更新和反馈,包括在IBM Z/OS(S/390主机)上的测试情况,以及在Power4基础的Linux机器上的测试结果。作者还对SuperFastHash进行了优化,以最大限度地利用现代CPU的管线,并使代码在整数处理上更加统一,从而在语义上更具可移植性。此外,还有关于SuperFastHash的增量版本的讨论。
作者指出,随着新一代CPU(能够进行64位计算)的出现,我们可以期待在未来几年内将有广泛的64位软件开发和工具可用性。作者还提到了内循环中的指令依赖性问题,并暗示通过奇偶字分裂和重组可能会带来显著的性能提升。
最后,作者提供了SuperFastHash的代码,并指出它已被苹果公司的开源WebKit(用于Safari浏览器)采用,并且可能最终会回到Konqueror浏览器中。此外,谷歌的Chrome浏览器基于WebKit,并继续使用这个哈希函数。作者还提供了一些对SuperFastHash进行增量更新的方法。
简而言之,如果想要加快Hash的计算,需要考虑内存对齐的影响、指令顺序无关的设计等因素。
Static Strings在Chromium的应用
Blink内核在初始化的时候,最主要的事情就是将各种关键字用Static Strings初始化。这段代码可以通过如下片段一窥端倪:
CoreInitializer::Initialize
这里面,各种xxx_names就是初始化静态字符串,我们随便挑一个进去看看:
而且在代码中,字符串的hash值、长度值都提前算好了直接hardcode到代码里,进一步提高初始化速度。
这段代码更直观:
void Init() {
static bool is_loaded = false;
if (is_loaded) return;
is_loaded = true;
struct NameEntry {
const char* name;
unsigned hash;
unsigned char length;
};
static const NameEntry kNames[] = {
{ "anonymous", 4545318, 9 },
{ "async", 2556481, 5 },
{ "auto", 4834735, 4 },
{ "circle", 1709685, 6 },
{ "close", 3222970, 5 },
{ "closed", 5707365, 6 },
{ "col", 12850806, 3 },
{ "colgroup", 3733719, 8 },
{ "decimal", 15005416, 7 },
{ "disc", 6260783, 4 },
{ "disclosure-closed", 7859367, 17 },
{ "disclosure-open", 5814334, 15 },
{ "done", 11685723, 4 },
{ "eager", 9356754, 5 },
{ "email", 13948917, 5 },
// ....
};
for (size_t i = 0; i < std::size(kNames); ++i) {
StringImpl* impl = StringImpl::CreateStatic(kNames[i].name, kNames[i].length, kNames[i].hash);
void* address = reinterpret_cast<AtomicString*>(&names_storage) + i;
new (address) AtomicString(impl);
}
}
其他加速查找的技巧
除了用hash,使用状态机也能大大减少搜索空间,加快搜索速度。例如这个函数,根据字符串长度划分为几个case,再根据不同长度划分为新的case,总之,可以快速实现从字符串到Tag枚举的转换:
CORE_EXPORT html_names::HTMLTag lookupHTMLTag(
const UChar* data,
unsigned length) {
DCHECK(data);
DCHECK(length);
switch (length) {
case 1:
switch (data[0]) {
case 'a':
return html_names::HTMLTag::kA;
case 'b':
return html_names::HTMLTag::kB;
case 'i':
return html_names::HTMLTag::kI;
case 'p':
return html_names::HTMLTag::kP;
case 'q':
return html_names::HTMLTag::kQ;
case 's':
return html_names::HTMLTag::kS;
case 'u':
return html_names::HTMLTag::kU;
}
break;
case 2:
switch (data[0]) {
case 'b':
if (data[1] == 'r') {
return html_names::HTMLTag::kBr;
}
break;
case 'd':
switch (data[1]) {
case 'd':
return html_names::HTMLTag::kDd;
case 'l':
return html_names::HTMLTag::kDl;
case 't':
return html_names::HTMLTag::kDt;
}
break;
case 'e':
if (data[1] == 'm') {
return html_names::HTMLTag::kEm;
}
break;
case 'h':
switch (data[1]) {
case '1':
return html_names::HTMLTag::kH1;
case '2':
return html_names::HTMLTag::kH2;
case '3':
return html_names::HTMLTag::kH3;
case '4':
return html_names::HTMLTag::kH4;
case '5':
return html_names::HTMLTag::kH5;
case '6':
return html_names::HTMLTag::kH6;
case 'r':
return html_names::HTMLTag::kHr;
}
break;
case 'l':
if (data[1] == 'i') {
return html_names::HTMLTag::kLi;
}
break;
case 'o':
if (data[1] == 'l') {
return html_names::HTMLTag::kOl;
}
break;
case 'r':
switch (data[1]) {
case 'b':
return html_names::HTMLTag::kRb;
case 'p':
return html_names::HTMLTag::kRp;
case 't':
return html_names::HTMLTag::kRt;
}
break;
case 't':
switch (data[1]) {
case 'd':
return html_names::HTMLTag::kTd;
case 'h':
return html_names::HTMLTag::kTh;
case 'r':
return html_names::HTMLTag::kTr;
case 't':
return html_names::HTMLTag::kTt;
}
break;
// ..... 略