MurmurHash3 哈希函数代码解读
逐行解读
我们以 MurmurHash3_x86_32 为例进行详细解读,因为它是最简洁的,但核心思想在其他函数中也普遍存在。
//-----------------------------------------------------------------------------
// MurmurHash3 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
// Note - The x86 and x64 versions do _not_ produce the same results, as the
// algorithms are optimized for their respective platforms. You can still
// compile and run any of them on any platform, but your performance with the
// non-native version will be less than optimal.
#include "murmur3.h" // 包含 murmur3.h 头文件,获取函数原型和类型定义
//-----------------------------------------------------------------------------
// Platform-specific functions and macros
#ifdef __GNUC__ // 如果编译器是 GCC(或兼容 GCC 的编译器,如 Clang)
#define FORCE_INLINE __attribute__((always_inline)) inline // 定义 FORCE_INLINE 宏,强制编译器内联函数
#else // 否则(例如 Visual Studio)
#define FORCE_INLINE inline // 定义 FORCE_INLINE 宏为普通的 inline
#endif
// 32 位左旋转函数
static FORCE_INLINE uint32_t rotl32 ( uint32_t x, int8_t r )
{
return (x << r) | (x >> (32 - r)); // 将 x 左移 r 位,并将 x 右移 (32-r) 位(填充零),然后进行位或操作。这是位旋转的典型实现。
}
// 64 位左旋转函数
static FORCE_INLINE uint64_t rotl64 ( uint64_t x, int8_t r )
{
return (x << r) | (x >> (64 - r)); // 同上,但针对 64 位整数
}
#define ROTL32(x,y) rotl32(x,y) // 宏定义,方便调用 32 位位旋转函数
#define ROTL64(x,y) rotl64(x,y) // 宏定义,方便调用 64 位位旋转函数
#define BIG_CONSTANT(x) (x##LLU) // 用于定义 64 位无符号长整型常量,例如 0x87c37b91114253d5LLU
//-----------------------------------------------------------------------------
// Block read - if your platform needs to do endian-swapping or can only
// handle aligned reads, do the conversion here
#define getblock(p, i) (p[i]) // 默认的块读取宏。直接从指针 p 的第 i 个位置读取。
// 作者注释指出,如果平台需要字节序转换或只能处理对齐读取,
// 则需要在这里进行相应的转换。对于 x86 平台通常不需要。
//-----------------------------------------------------------------------------
// Finalization mix - force all bits of a hash block to avalanche
// 最终混淆函数 - 强制哈希块的所有位雪崩(即微小输入变化导致哈希值剧烈变化)
static FORCE_INLINE uint32_t fmix32 ( uint32_t h )
{
h ^= h >> 16; // h 与 h 右移 16 位的结果进行异或
h *= 0x85ebca6b; // h 乘以一个魔术常数
h ^= h >> 13; // h 与 h 右移 13 位的结果进行异或
h *= 0xc2b2ae35; // h 乘以另一个魔术常数
h ^= h >> 16; // h 与 h 右移 16 位的结果进行异或
return h; // 返回最终的 32 位混淆结果
}
//----------
static FORCE_INLINE uint64_t fmix64 ( uint64_t k )
{
k ^= k >> 33; // k 与 k 右移 33 位的结果进行异或
k *= BIG_CONSTANT(0xff51afd7ed558ccd); // k 乘以一个 64 位魔术常数
k ^= k >> 33; // k 与 k 右移 33 位的结果进行异或
k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); // k 乘以另一个 64 位魔术常数
k ^= k >> 33; // k 与 k 右移 33 位的结果进行异或
return k; // 返回最终的 64 位混淆结果
}
//-----------------------------------------------------------------------------
// MurmurHash3 的 32 位输出版本,针对 x86 架构
void MurmurHash3_x86_32 ( const void * key, int len,
uint32_t seed, void * out )
{
const uint8_t * data = (const uint8_t*)key; // 将输入数据 key 转换为无符号 8 位指针,方便按字节处理
const int nblocks = len / 4; // 计算数据块的数量,每个块 4 字节(32 位)
int i; // 循环计数器
uint32_t h1 = seed; // 初始化 32 位哈希值 h1 为种子
uint32_t c1 = 0xcc9e2d51; // 定义第一个 32 位魔术常数
uint32_t c2 = 0x1b873593; // 定义第二个 32 位魔术常数
//----------
// body (处理主要的数据块)
// blocks 指向数据块的起始位置。
// 注意:`data + nblocks*4` 是一种 C 语言中常见的技巧,它将 `blocks` 指针指向数据的末尾
// 然后通过 `i = -nblocks; i; i++` 循环和 `getblock(blocks, i)` 以负索引从后向前读取,
// 这样可以避免在 `getblock` 内部进行 `(blocks - nblocks)` 这样的计算。
// 在这里 `blocks` 实际上是指向 `data` 的开头, `getblock` 应该直接是 `data[i]`
// 但此处由于指针的加法与数组索引的等效性,可以理解为 `blocks` 是 `data` 的一个 `uint32_t` 视图,
// 其起始地址偏移了 `nblocks * 4` 字节,因此 `getblock(blocks, i)` 等同于 `data[nblocks*4 + i*4]`。
// 但更常见的 Murmur3 实现是 `const uint32_t * blocks = (const uint32_t *)key;` 然后 `for(i = 0; i < nblocks; i++)`。
// 这种负向遍历的方式可能是为了优化某些编译器或架构上的缓存局部性或管道。
const uint32_t * blocks = (const uint32_t *)(data + nblocks*4);
for(i = -nblocks; i; i++) // 循环处理每个 4 字节的数据块
{
uint32_t k1 = getblock(blocks,i); // 获取当前数据块 k1
k1 *= c1; // k1 乘以常数 c1
k1 = ROTL32(k1,15); // k1 左旋转 15 位
k1 *= c2; // k1 乘以常数 c2
h1 ^= k1; // h1 与 k1 进行异或
h1 = ROTL32(h1,13); // h1 左旋转 13 位
h1 = h1*5+0xe6546b64; // h1 乘以 5 并加上一个常数 (进一步混淆)
}
//----------
// tail (处理剩余的不足 4 字节的尾部数据)
const uint8_t * tail = (const uint8_t*)(data + nblocks*4); // tail 指向未处理的剩余字节的起始位置
uint32_t k1 = 0; // 初始化 k1 为 0,用于累积尾部字节
switch(len & 3) // 根据 len % 4 的结果(0, 1, 2, 3)判断剩余字节数
{
case 3: k1 ^= tail[2] << 16; // 如果有 3 个字节,将第三个字节左移 16 位并异或到 k1
case 2: k1 ^= tail[1] << 8; // 如果有 2 个字节,将第二个字节左移 8 位并异或到 k1
case 1: k1 ^= tail[0]; // 如果有 1 个字节,将第一个字节直接异或到 k1
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; // 对累积的 k1 进行与主循环相同的混淆操作,并与 h1 异或
}; // 这里使用了 C 语言 switch 的 fall-through 特性,例如 len & 3 为 3 时,会依次执行 case 3, case 2, case 1 的代码。
//----------
// finalization (最终处理)
h1 ^= len; // h1 与总长度进行异或,这有助于将长度信息也融入哈希值中
h1 = fmix32(h1); // 对 h1 进行最终混淆操作,进一步扩散哈希值
*(uint32_t*)out = h1; // 将最终的 32 位哈希值存储到 out 指向的缓冲区
}
//-----------------------------------------------------------------------------
// MurmurHash3 的 128 位输出版本,针对 x86 架构
void MurmurHash3_x86_128 ( const void * key, const int len,
uint32_t seed, void * out )
{
const uint8_t * data = (const uint8_t*)key; // 输入数据
const int nblocks = len / 16; // 块数量,每个块 16 字节 (128 位)
int i;
uint32_t h1 = seed; // 128 位哈希值由四个 32 位分量 h1, h2, h3, h4 组成
uint32_t h2 = seed;
uint32_t h3 = seed;
uint32_t h4 = seed;
uint32_t c1 = 0x239b961b; // 定义四个 32 位魔术常数
uint32_t c2 = 0xab0e9789;
uint32_t c3 = 0x38b34ae5;
uint32_t c4 = 0xa1e38b93;
//----------
// body (处理主要的数据块)
// blocks 指向数据块的起始位置。与 x86_32 类似,这里也采用负向遍历的指针技巧
const uint32_t * blocks = (const uint32_t *)(data + nblocks*16);
for(i = -nblocks; i; i++) // 循环处理每个 16 字节的数据块
{
// 每个 16 字节块被分成四个 32 位子块 k1, k2, k3, k4
uint32_t k1 = getblock(blocks,i*4+0);
uint32_t k2 = getblock(blocks,i*4+1);
uint32_t k3 = getblock(blocks,i*4+2);
uint32_t k4 = getblock(blocks,i*4+3);
// 对 k1 进行混淆,并与 h1 异或
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
// h1, h2, h3, h4 之间进行复杂的旋转、加法和乘法混淆,使它们相互影响
h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b;
// 对 k2 进行混淆,并与 h2 异或
k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2;
h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747;
// 对 k3 进行混淆,并与 h3 异或
k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3;
h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35;
// 对 k4 进行混淆,并与 h4 异或
k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4;
h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17;
}
//----------
// tail (处理剩余的不足 16 字节的尾部数据)
const uint8_t * tail = (const uint8_t*)(data + nblocks*16); // tail 指向未处理的剩余字节的起始位置
uint32_t k1 = 0; // 初始化 k1, k2, k3, k4 为 0,用于累积尾部字节
uint32_t k2 = 0;
uint32_t k3 = 0;
uint32_t k4 = 0;
switch(len & 15) // 根据 len % 16 的结果判断剩余字节数
{
case 15: k4 ^= tail[14] << 16; // 尾部处理逻辑,利用 switch 的 fall-through 特性,逐字节处理
case 14: k4 ^= tail[13] << 8;
case 13: k4 ^= tail[12] << 0;
k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; // 对 k4 进行混淆并更新 h4
case 12: k3 ^= tail[11] << 24;
case 11: k3 ^= tail[10] << 16;
case 10: k3 ^= tail[ 9] << 8;
case 9: k3 ^= tail[ 8] << 0;
k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; // 对 k3 进行混淆并更新 h3
case 8: k2 ^= tail[ 7] << 24;
case 7: k2 ^= tail[ 6] << 16;
case 6: k2 ^= tail[ 5] << 8;
case 5: k2 ^= tail[ 4] << 0;
k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; // 对 k2 进行混淆并更新 h2
case 4: k1 ^= tail[ 3] << 24;
case 3: k1 ^= tail[ 2] << 16;
case 2: k1 ^= tail[ 1] << 8;
case 1: k1 ^= tail[ 0] << 0;
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; // 对 k1 进行混淆并更新 h1
};
//----------
// finalization (最终处理)
h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len; // 所有哈希分量与总长度异或
// 多个哈希分量之间进行相互混淆,确保它们充分影响彼此
h1 += h2; h1 += h3; h1 += h4;
h2 += h1; h3 += h1; h4 += h1;
// 对每个哈希分量进行最终混淆
h1 = fmix32(h1);
h2 = fmix32(h2);
h3 = fmix32(h3);
h4 = fmix32(h4);
// 再次进行相互混淆
h1 += h2; h1 += h3; h1 += h4;
h2 += h1; h3 += h1; h4 += h1;
// 将最终的四个 32 位哈希分量存储到 out 指向的缓冲区,形成 128 位输出
((uint32_t*)out)[0] = h1;
((uint32_t*)out)[1] = h2;
((uint32_t*)out)[2] = h3;
((uint32_t*)out)[3] = h4;
}
//-----------------------------------------------------------------------------
// MurmurHash3 的 128 位输出版本,针对 x64 架构
void MurmurHash3_x64_128 ( const void * key, const int len,
const uint32_t seed, void * out )
{
const uint8_t * data = (const uint8_t*)key; // 输入数据
const int nblocks = len / 16; // 块数量,每个块 16 字节 (128 位)
int i;
uint64_t h1 = seed; // 128 位哈希值由两个 64 位分量 h1, h2 组成
uint64_t h2 = seed;
uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); // 定义两个 64 位魔术常数
uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f);
//----------
// body (处理主要的数据块)
const uint64_t * blocks = (const uint64_t *)(data); // 直接将 key 视为 64 位块的指针
for(i = 0; i < nblocks; i++) // 循环处理每个 16 字节的数据块
{
// 每个 16 字节块被分成两个 64 位子块 k1, k2
uint64_t k1 = getblock(blocks,i*2+0);
uint64_t k2 = getblock(blocks,i*2+1);
// 对 k1 进行混淆,并与 h1 异或
k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1;
// h1, h2 之间进行复杂的旋转、加法和乘法混淆
h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;
// 对 k2 进行混淆,并与 h2 异或
k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2;
h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;
}
//----------
// tail (处理剩余的不足 16 字节的尾部数据)
const uint8_t * tail = (const uint8_t*)(data + nblocks*16); // tail 指向未处理的剩余字节的起始位置
uint64_t k1 = 0; // 初始化 k1, k2 为 0,用于累积尾部字节
uint64_t k2 = 0;
switch(len & 15) // 根据 len % 16 的结果判断剩余字节数
{
case 15: k2 ^= (uint64_t)(tail[14]) << 48; // 尾部处理逻辑,将字节组装成 64 位值
case 14: k2 ^= (uint64_t)(tail[13]) << 40;
case 13: k2 ^= (uint64_t)(tail[12]) << 32;
case 12: k2 ^= (uint64_t)(tail[11]) << 24;
case 11: k2 ^= (uint64_t)(tail[10]) << 16;
case 10: k2 ^= (uint64_t)(tail[ 9]) << 8;
case 9: k2 ^= (uint64_t)(tail[ 8]) << 0;
k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2; // 对 k2 进行混淆并更新 h2
case 8: k1 ^= (uint64_t)(tail[ 7]) << 56;
case 7: k1 ^= (uint64_t)(tail[ 6]) << 48;
case 6: k1 ^= (uint64_t)(tail[ 5]) << 40;
case 5: k1 ^= (uint64_t)(tail[ 4]) << 32;
case 4: k1 ^= (uint64_t)(tail[ 3]) << 24;
case 3: k1 ^= (uint64_t)(tail[ 2]) << 16;
case 2: k1 ^= (uint64_t)(tail[ 1]) << 8;
case 1: k1 ^= (uint64_t)(tail[ 0]) << 0;
k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; // 对 k1 进行混淆并更新 h1
};
//----------
// finalization (最终处理)
h1 ^= len; h2 ^= len; // 两个哈希分量与总长度异或
// 相互混淆
h1 += h2;
h2 += h1;
// 对每个哈希分量进行最终混淆
h1 = fmix64(h1);
h2 = fmix64(h2);
// 再次相互混淆
h1 += h2;
h2 += h1;
// 将最终的两个 64 位哈希分量存储到 out 指向的缓冲区,形成 128 位输出
((uint64_t*)out)[0] = h1;
((uint64_t*)out)[1] = h2;
}
综合说明
这份 C 语言代码忠实地实现了 MurmurHash3 的核心算法。其设计哲学是通过一系列乘法、位旋转和异或操作,将输入数据的每一个位的信息尽可能地扩散到最终哈希值的每一个位,以达到高质量的哈希值分布和优秀的性能。
关键设计点:
-
分块处理 (
body部分):- 将输入数据切分成固定大小的块(32 位版本是 4 字节,128 位版本是 16 字节)。
- 通过循环,对每个数据块执行一系列的混淆操作。这些操作包括与魔术常数相乘、位旋转和与当前哈希值进行异或。这些魔术常数和旋转位数是经过精心选择的,旨在最大化哈希值的随机性和扩散性。
MurmurHash3_x86_128和MurmurHash3_x64_128版本会使用多个哈希分量(h1,h2,h3,h4或h1,h2),并且这些分量之间会进行相互作用,进一步增加哈希值的复杂性。
-
尾部处理 (
tail部分):- 处理输入数据中不足一个完整块的剩余字节。
- 使用
switch语句的 fall-through 特性,将剩余的字节逐个累积到一个临时变量中(通过位移和异或),然后对这个累积的尾部进行与主循环类似的混淆操作。这种方式非常高效,因为它避免了额外的循环和条件判断。
-
最终混淆 (
finalization部分):- 在处理完所有数据块和尾部后,对最终的哈希值(或多个哈希分量)进行一系列额外的混淆操作,即
fmix32或fmix64函数。 - 这些操作通常包括更多的位移、异或和乘法,目的是进一步消除任何可能存在的模式,确保哈希值的均匀分布。
- 对于 128 位输出的版本,最终的哈希分量会再次进行相互加法和异或,以确保最终输出的 128 位哈希值具有更好的整体分布性。
- 在处理完所有数据块和尾部后,对最终的哈希值(或多个哈希分量)进行一系列额外的混淆操作,即
-
性能优化:
FORCE_INLINE宏: 强制编译器内联rotl和fmix函数,减少函数调用开销。- 位运算和乘法: 哈希函数主要依赖于 CPU 能够快速执行的位运算和整数乘法。
getblock宏: 简化了块的读取,并为平台特定的字节序或对齐问题预留了扩展点。- 针对不同架构优化:
MurmurHash3_x86_32、MurmurHash3_x86_128和MurmurHash3_x64_128针对各自的位数和架构特性(如 x64 的 64 位寄存器和指令)使用了不同的常数、旋转位数和处理流程,以达到最佳性能。
-
种子 (
seed): 允许用户提供一个种子值,这对于生成不同哈希序列(例如在哈希表中使用不同的哈希函数来避免特定攻击)或在分布式系统中确保不同节点产生相同哈希值很有用。 -
非加密性: 代码的注释和实现本身都表明 MurmurHash3 是一个非加密型哈希函数。这意味着它不适用于安全敏感的场景,如密码存储或数据完整性验证(因为它的抗碰撞性不如加密哈希函数)。它的核心价值在于快速、高质量的哈希散布,适用于哈希表、布隆过滤器等场景。
这份代码是学习高性能哈希函数设计的优秀范例,展示了如何通过位操作和数学技巧来高效地将数据“混淆”成均匀分布的哈希值。

浙公网安备 33010602011771号