面向小型MCU的16位整数高效完美哈希函数方案咨询
问题背景
在200 MHz微控制器上开发CANopen slave栈,需要高效匹配16位对象索引(如0x603d)与对象存储的表索引,对象数量在200-400之间。
- 尝试过16位表的完美哈希,但填充率仅约0.6%,且难以找到适配16位整数的完美哈希生成工具(MPHF、BBHash、gperf、CMPH等要么针对字符串,要么算法复杂不适合嵌入式场景)。
- 目前采用暴力搜索seed的方式,基于异或/移位哈希函数构建开放寻址哈希表,但填充因子0.5时仍有3次碰撞。
- 核心要求:零碰撞、100%填充率,哈希函数的ARM 32位汇编指令数少于10条。
当前测试的fatou哈希函数仅需5条ARM指令,但无法实现零碰撞:
eors r0, r0, r1 movw r3, #40763 movt r3, 1117 mul r0, r3, r0 bx lr
尝试过基于双哈希的完美哈希方案,但无法生成唯一桶索引,相关代码如下:
import sys import random from collections import defaultdict def generate_constants(): return random.randint(1, sys.maxsize), random.randint(1, sys.maxsize) class HashTable: def __init__(self, list_of_keys, retry_limit=10): self.m = len(list_of_keys) # Define table size based on the number of keys attempt = 0 while attempt < retry_limit: self.C1, self.C2 = generate_constants() self.graph, self.edges = self.create_graph(list_of_keys) if not self.is_cyclic(self.graph): self.setup_hash_table(self.edges) return attempt += 1 raise Exception("Failed to create an acyclic graph after several attempts.") def setup_hash_table(self, edges): table1 = [None] * self.m table2 = [None] * self.m for key, (index1, index2) in edges.items(): if table1[index1] is None and table2[index2] is None: assign_value = random.randint(0, 100) table1[index1] = assign_value table2[index2] = key - assign_value elif table1[index1] is not None: table2[index2] = key - table1[index1] elif table2[index2] is not None: table1[index1] = key - table2[index2] self.table1 = table1 self.table2 = table2 def h1(self, key): return ((key * self.C1) + (self.C1 >> 5)) % self.m def h2(self, key): return ((key * self.C2) + (self.C2 >> 5)) % self.m def __call__(self, key): return self.table1[self.h1(key)] + self.table2[self.h2(key)] def create_graph(self, list_of_keys): graph = defaultdict(list) edges = {} for key in list_of_keys: index1 = self.h1(key) index2 = self.h2(key) graph[index1].append(index2) graph[index2].append(index1) edges[key] = (index1, index2) return graph, edges def is_cyclic(self, graph): visited = [False] * self.m rec_stack = [False] * self.m def dfs(node, parent): visited[node] = True rec_stack[node] = True for neighbor in graph[node]: if not visited[neighbor]: if dfs(neighbor, node): return True elif rec_stack[neighbor] and neighbor != parent: return True rec_stack[node] = False return False for i in range(self.m): if not visited[i]: if dfs(i, -1): print("Cycle detected involving node", i) return True return False keys = [4, 8, 15, 16, 23, 42] hf = HashTable(keys) for key in keys: print(key, hf(key))
推荐方案
1. 优化暴力搜索的参数化哈希函数
针对16位整数,选择更简单的参数化哈希函数,扩大搜索范围(同时搜索seed和乘数),更容易找到零碰撞的完美哈希。
轻量哈希函数模板
这类函数的ARM汇编指令数通常在5-8条,符合要求:
def simple_hash(key, seed, mul): return ((key ^ seed) * mul) % len(data)
搜索策略改进
- 同时搜索16位seed和奇数乘数(mul取奇数避免哈希分布退化),对象数量仅200-400,暴力搜索计算量完全可控。
- 若表大小为2的幂,可用位运算替代取模,进一步减少指令数。
示例搜索代码:
def find_perfect_hash(data): m = len(data) # 遍历16位seed和奇数mul(0x1到0xffff,步长2) for seed in range(0x10000): for mul in range(1, 0x10000, 2): seen = set() collision = False for key in data: h = ((key ^ seed) * mul) % m if h in seen: collision = True break seen.add(h) if not collision: return seed, mul return None, None # 使用你的数据测试 seed, mul = find_perfect_hash(data) if seed is not None: print(f"找到完美哈希参数:seed=0x{seed:04X}, mul=0x{mul:04X}")
对应的ARM汇编(表大小为512,用AND替代取模):
eors r0, r0, #0x3FBF ; key ^ seed(示例seed) movw r3, #0xABCD ; 加载mul低16位 movt r3, #0x1234 ; 加载mul高16位 mul r0, r3, r0 ; 乘法运算 and r0, r0, #0x1FF ; 取模512(位运算替代) bx lr ; 返回结果
总指令数6条,完全符合要求。
2. 线性探测完美哈希(开放寻址极端优化)
如果找不到单步哈希的完美参数,可使用线性探测完美哈希:预计算每个key的探测偏移量,保证每个key找到唯一空位,填充率100%。
实现步骤
- 选择基础哈希函数(如
fatou)。 - 预计算每个key的探测步数:从初始哈希索引开始向后查找,记录移动步数。
- 存储偏移量表,查找时计算初始哈希后加上偏移量得到最终索引。
示例代码:
def build_perfect_probe_table(data, hfunc, seed): m = len(data) table = [None] * m offsets = {} for key in data: idx = hfunc(key, seed) % m step = 0 while table[idx] is not None: idx = (idx + 1) % m step += 1 table[idx] = key offsets[key] = step return offsets, table # 使用fatou函数和最优seed offsets, table = build_perfect_probe_table(data, fatou, 16431)
查找时的ARM代码:
; r0 = key eors r0, r0, #16431 ; key ^ seed movw r3, #40763 movt r3, 1117 mul r0, r3, r0 ldr r1, =map_size ; map_size = len(data) udiv r0, r0, r1 ; 取模运算 ldr r2, =offsets_table ldrb r2, [r2, r0] ; 获取偏移量 add r0, r0, r2 ; 计算最终索引 bx lr
指令数约8条,保证零碰撞与100%填充率。
3. 修正双哈希完美哈希方案
你尝试的双哈希思路可行,但之前的代码目标错误(应生成唯一索引而非还原key)。修正后的方案通过二分图分配值,实现零碰撞。
修正后的核心代码
import sys import random from collections import defaultdict def generate_odd_constant(max_val): val = random.randint(1, max_val) return val if val % 2 == 1 else val + 1 class PerfectHash: def __init__(self, list_of_keys, retry_limit=100): self.m = len(list_of_keys) self.keys = list_of_keys attempt = 0 while attempt < retry_limit: self.C1 = generate_odd_constant(0xFFFF) self.C2 = generate_odd_constant(0xFFFF) self.graph = defaultdict(list) self.key_to_nodes = {} # 构建二分图:左节点为h1结果,右节点为h2结果 + m(区分左右) for key in list_of_keys: h1 = (key * self.C1) % self.m h2 = (key * self.C2) % self.m self.graph[h1].append(h2 + self.m) self.graph[h2 + self.m].append(h1) self.key_to_nodes[key] = (h1, h2) if self.is_forest(): self.assign_values() return attempt += 1 raise Exception("Failed to generate perfect hash after retries.") def is_forest(self): visited = set() for node in self.graph: if node not in visited: stack = [(node, -1)] visited.add(node) while stack: curr, parent = stack.pop() for neighbor in self.graph[curr]: if neighbor not in visited: visited.add(neighbor) stack.append((neighbor, curr)) elif neighbor != parent: return False return True def assign_values(self): self.h1_val = [0] * self.m self.h2_val = [0] * self.m visited = set() for node in self.graph: if node not in visited and node < self.m: stack = [(node, True, 0)] visited.add(node) while stack: curr, is_left, val = stack.pop() if is_left: self.h1_val[curr] = val for neighbor in self.graph[curr]: h2_idx = neighbor - self.m if h2_idx not in visited: visited.add(h2_idx) # 找到对应key的索引 for k, (h1, h2) in self.key_to_nodes.items(): if h1 == curr and h2 == h2_idx: key_idx = self.keys.index(k) break self.h2_val[h2_idx] = val ^ key_idx stack.append((h2_idx, False, self.h2_val[h2_idx])) else: self.h2_val[curr] = val for neighbor in self.graph[curr]: h1_idx = neighbor if h1_idx not in visited: visited.add(h1_idx) for k, (h1, h2) in self.key_to_nodes.items(): if h1 == h1_idx and h2 == curr: key_idx = self.keys.index(k) break self.h1_val[h1_idx] = val ^ key_idx stack.append((h1_idx, True, self.h1_val[h1_idx])) def get_index(self, key): h1 = (key * self.C1) % self.m h2 = (key * self.C2) % self.m return self.h1_val[h1] ^ self.h2_val[h2] # 测试 keys = [4, 8, 15, 16, 23, 42] ph = PerfectHash(keys) for key in keys: print(f"Key {key} -> Index {ph.get_index(key)}")
对应的ARM汇编:
; r0 = key movw r3, #C1_low movt r3, #C1_high mul r1, r3, r0 ; key * C1 ldr r2, =m udiv r1, r1, r2 ; h1 = (key*C1) % m ldr r1, [r1, h1_val] ; 加载h1_val[h1] movw r3, #C2_low movt r3, #C2_high mul r2, r3, r0 ; key * C2 udiv r2, r2, r2 ; h2 = (key*C2) % m ldr r2, [r2, h2_val] ; 加载h2_val[h2] eors r0, r1, r2 ; 索引 = h1_val ^ h2_val bx lr
指令数约9条,满足要求。
4. 预计算查找表(极端高效方案)
如果微控制器RAM足够(400个16位索引仅占800字节),直接使用16位索引到表索引的查找表是最优选择:
- 构建大小为65536的数组(128KB),数组值为对应对象的表索引,无对象的位置设为无效值(如0xFFFF)。
- 查找时直接通过key作为数组索引读取,仅需1条指令:
ldrh r0, [r0, lookup_table]。
该方案无哈希计算开销,实时性最优,适合CANopen这类对响应速度要求高的场景。
内容的提问来源于stack exchange,提问作者nowox
相关产品推荐
相关产品推荐

