DPDK原理分析——Hash库(librte_hash)

Hash表和算法是高性能流量处理中常用技术,在DPDK中也提供了相应的高性能实现用于支持流表、转发规则表等设施的开发。DPDK18的Hash库实现方式与其早期版本(1.6)时期的实现有了根本性的差异,本文介绍的实现基于18.05版本代码。

Hash算法原理——Cuckoo Hash

DPDK中的Hash库采用的hash算法和数据结构基于CuckooHash(https://www.jianshu.com/p/68220564f341),哈希表中的节点采用一个连续数组保存,其原理是每个节点key可以对应两个哈希散列位置(主、从)。查找和删除key时需要查找两个位置来确定key是否存在;插入时将key插入到主位,如果主位已经被key2占用,则将占用主位的key2移动到其另一位置存储,如果key2的另一位置也已经被key3占用,则再将key3移动到其另一位置存储......直到最后一次移动不再冲突为止,如果冲突次数达到一定阈值,则判定哈希表已满。

具体实现方式

在DPDK的实现中,每个节点的主从存储位置被实现为两个桶数组。哈希结构由两部分构成:

一部分是一系列的哈希桶,每个桶中可以保存8个节点的主次signature和key_index;

另一部分则是保存节点key和内容的数组,通过key_index下标进行索引,其中节点内容部分共8字节,可以直接保存节点信息,也可以保存一个指针来指向节点的数据结构。

在查找一个key时,流程如下:

  1. 首先通过散列算法获得两个4字节的signature和对应的两个桶。主signature散列算法可以是CRC、JHash或自定义算法;次signature是对主signature经过一个变换产生的。
  2. 在桶中查找signature匹配的节点信息
  3. 根据key_index索引到完整的key
  4. 比对key是否一致,如果一致则查找成功

可见,一次成功的查找至少要访问两个数据结构各一次,分别比对signature和key。一次失败的查找则要访问主从桶各一次,比对signature。

代码分析

数据结构

/** A hash table structure. */
struct rte_hash {
	char name[RTE_HASH_NAMESIZE];   /**< Name of the hash. */
	uint32_t entries;               /**< Total table entries. */
	uint32_t num_buckets;           /**< Number of buckets in table. */

	struct rte_ring *free_slots;
	/**< Ring that stores all indexes of the free slots in the key table */
	uint8_t hw_trans_mem_support;
	/**< Hardware transactional memory support */
	struct lcore_cache *local_free_slots;
	/**< Local cache per lcore, storing some indexes of the free slots */
	enum add_key_case add_key; /**< Multi-writer hash add behavior */

	rte_spinlock_t *multiwriter_lock; /**< Multi-writer spinlock for w/o TM */

	/* Fields used in lookup */

	uint32_t key_len __rte_cache_aligned;
	/**< Length of hash key. */
	rte_hash_function hash_func;    /**< Function used to calculate hash. */
	uint32_t hash_func_init_val;    /**< Init value used by hash_func. */
	rte_hash_cmp_eq_t rte_hash_custom_cmp_eq;
	/**< Custom function used to compare keys. */
	enum cmp_jump_table_case cmp_jump_table_idx;
	/**< Indicates which compare function to use. */
	enum rte_hash_sig_compare_function sig_cmp_fn;
	/**< Indicates which signature compare function to use. */
	uint32_t bucket_bitmask;
	/**< Bitmask for getting bucket index from hash signature. */
	uint32_t key_entry_size;         /**< Size of each key entry. */

	void *key_store;                /**< Table storing all keys and data */
	struct rte_hash_bucket *buckets;
	/**< Table with buckets storing all the	hash values and key indexes
	 * to the key table.
	 */
} __rte_cache_aligned;

一个哈希表的数据结构如上,其中包括节点最大数量、哈希桶数量、哈希key的长度、key的散列值算法等。该结构通过rte_hash_create创建和初始化,其中最重要的数据结构是:

struct rte_ring *free_slots,表示哈希节点数组中空闲的节点;

void *key_store,保存实际的key和节点信息的数组

struct rte_hash_bucket *buckets,哈希桶数组,保存key的signature和在key_store中的下标位置

每个成员的意义和功能可以结合上面的注释以及rte_hash_create函数中的初始化逻辑来深入了解。

关键逻辑

哈希表中的节点查找和删除逻辑都比较简单,这里不再分析。节点插入的操作相对复杂一点,关键逻辑在于发现节点对应的主次桶都已经填满时的已有节点移动逻辑。这个逻辑在make_space_bucket函数中实现。

make_space_bucket函数主要分为两步:

1. 桶中是否有节点对应的次桶中还有空间?如果有的话就将该节点移动它的次桶中。代码如下:

	/*
	 * Push existing item (search for bucket with space in
	 * alternative locations) to its alternative location
	 */
	for (i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
		/* Search for space in alternative locations */
		next_bucket_idx = bkt->sig_alt[i] & h->bucket_bitmask;
		next_bkt[i] = &h->buckets[next_bucket_idx];
		for (j = 0; j < RTE_HASH_BUCKET_ENTRIES; j++) {
			if (next_bkt[i]->key_idx[j] == EMPTY_SLOT)
				break;
		}

		if (j != RTE_HASH_BUCKET_ENTRIES)
			break;
	}

	/* Alternative location has spare room (end of recursive function) */
	if (i != RTE_HASH_BUCKET_ENTRIES) {
		next_bkt[i]->sig_alt[j] = bkt->sig_current[i];
		next_bkt[i]->sig_current[j] = bkt->sig_alt[i];
		next_bkt[i]->key_idx[j] = bkt->key_idx[i];
		return i;
	}

2. 如果没有节点的次桶有空间了,则只能强行移动一个节点到次桶,再在次桶中再移动一个节点。这里会递归调用make_space_bucket函数来移动次桶中的节点。如果桶中的每个节点都已经被强行移动过,或者此次操作移动的节点数达到上限,则认为哈希表已满,操作失败。代码如下:

	/* Pick entry that has not been pushed yet */
	for (i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++)
		if (bkt->flag[i] == 0)
			break;

	/* All entries have been pushed, so entry cannot be added */
	if (i == RTE_HASH_BUCKET_ENTRIES || ++(*nr_pushes) > RTE_HASH_MAX_PUSHES)
		return -ENOSPC;

	/* Set flag to indicate that this entry is going to be pushed */
	bkt->flag[i] = 1;

	/* Need room in alternative bucket to insert the pushed entry */
	ret = make_space_bucket(h, next_bkt[i], nr_pushes);
	/*
	 * After recursive function.
	 * Clear flags and insert the pushed entry
	 * in its alternative location if successful,
	 * or return error
	 */
	bkt->flag[i] = 0;
	if (ret >= 0) {
		next_bkt[i]->sig_alt[ret] = bkt->sig_current[i];
		next_bkt[i]->sig_current[ret] = bkt->sig_alt[i];
		next_bkt[i]->key_idx[ret] = bkt->key_idx[i];
		return i;
	} else
		return ret;

小结

综合上述分析,可以发现,DPDK实现的hash库的最大优点,在于其一次查找的时间是有上限的,一般情况下最多产生3次随机内存访问:主桶、从桶、key节点。同时通过CuckooHash算法,很大程度上避免了普通的基于数组和桶的哈希表在同一个桶冲突严重的情况下节点会插入失败的问题。官方实验结果显示只有当哈希表节点使用率达到95%时才会出现插入失败,在50%以下时基本可以保证绝大部分节点都保存在主桶中。此外,通过signature和key两阶段比较的方法减少了key比较的次数,在key比较长时能提升一定的性能。

但这个方法的逻辑相当复杂,在表完全为空时,一次失败的查找也要访问两次内存,还要基于ring来获取释放数组索引资源,其中实现的本核心缓存逻辑感觉也不太合理。综合来看其性能未必高于普通的基于大规模哈希数组和链表的哈希表结构。采用时需要经过具体测试评估。

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值