DPDK系列之三十三DPDK并行机制的底层支持

一、背景介绍

在前面介绍了DPDK中的上层对并行的支持,特别是对多核的支持。但是,大家都知道,再怎么好的设计和架构,再优秀的编码,最终都要落到硬件和固件对整个上层应用的支持。单纯的硬件好处理,一个核不够多个核,在可能的情况下把CPU的频率增加,加大缓存等等。在现有水平的范围内,这些都是可以比较容易做到的。
但是另外一个,就是在CPU上如何最终运行指令(也可以叫做固件设计),这个就需要设计人员动脑子了。一般来说,IPC(Instruction Per Clock,一个时钟周期内执行的指令数量,可不要看成进程间通信)的数量越高,CPU运行性能越高(频率和核数相同)。
现代CPU基本使用了越标量(superscalar)体系结构,通过以空间换时间的方式实行了指令级并行运算。不同的架构的处理器,可能在硬件设计本身有所不同,但在追求并行度上,原理基本相同。
在前面的多核编程中,介绍过几种指令,目前常用的基本以SIMD(单指令流多数据流)和MIMD(多指令流多数据流)为主。后者一般是多核和多CPU(当然更高层次的多计算机也算),但在分析本文中更倾向的是SIMD,毕竟一个核心能处理多少更能体现性能和效率。
SIMD其实很容易理解,可以认为是一种并行的批处理。原来只能一次取一条指令处理一条数据,这次可以一条指令处理多条数据。举个最简单的例子,加指令,需要有两次读操作数,而如果使用SIMD,则一次就可以都读进来。其后的处理周期也是如此,那么效率至少增加了一倍。
而这些指令设计和处理会形成一个指令集,它的发展也有一个过程,intel的SIMD指令集主要有MMX, SSE, AVX, AVX-512,主流就是SSE/AVX。AMD的比较复杂,有兴趣可以查找看一下。

二、DPDK中的应用

在DPDK中对SIMD的应用体现在数据的处理上,DPDK提供了一个化化的拷贝memcpy函数,它充分利用了SIMD指令集:

static __rte_always_inline void *
rte_memcpy(void *dst, const void *src, size_t n)
{
	if (!(((uintptr_t)dst | (uintptr_t)src) & ALIGNMENT_MASK))
		return rte_memcpy_aligned(dst, src, n);
	else
		return rte_memcpy_generic(dst, src, n);
}
static __rte_always_inline void *
rte_memcpy_aligned(void *dst, const void *src, size_t n)
{
	void *ret = dst;

	/* Copy size < 16 bytes */
	if (n < 16) {
		return rte_mov15_or_less(dst, src, n);
	}

	/* Copy 16 <= size <= 32 bytes */
	if (n <= 32) {
		rte_mov16((uint8_t *)dst, (const uint8_t *)src);
		rte_mov16((uint8_t *)dst - 16 + n,
				(const uint8_t *)src - 16 + n);

		return ret;
	}

	/* Copy 32 < size <= 64 bytes */
	if (n <= 64) {
		rte_mov32((uint8_t *)dst, (const uint8_t *)src);
		rte_mov32((uint8_t *)dst - 32 + n,
				(const uint8_t *)src - 32 + n);

		return ret;
	}

	/* Copy 64 bytes blocks */
	for (; n >= 64; n -= 64) {
		rte_mov64((uint8_t *)dst, (const uint8_t *)src);
		dst = (uint8_t *)dst + 64;
		src = (const uint8_t *)src + 64;
	}

	/* Copy whatever left */
	rte_mov64((uint8_t *)dst - 64 + n,
			(const uint8_t *)src - 64 + n);

	return ret;
}
static __rte_always_inline void *
rte_memcpy_generic(void *dst, const void *src, size_t n)
{
	__m128i xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7, xmm8;
	void *ret = dst;
	size_t dstofss;
	size_t srcofs;

	/**
	 * Copy less than 16 bytes
	 */
	if (n < 16) {
		return rte_mov15_or_less(dst, src, n);
	}

	/**
	 * Fast way when copy size doesn't exceed 512 bytes
	 */
	if (n <= 32) {
		rte_mov16((uint8_t *)dst, (const uint8_t *)src);
		rte_mov16((uint8_t *)dst - 16 + n, (const uint8_t *)src - 16 + n);
		return ret;
	}
	if (n <= 48) {
		rte_mov32((uint8_t *)dst, (const uint8_t *)src);
		rte_mov16((uint8_t *)dst - 16 + n, (const uint8_t *)src - 16 + n);
		return ret;
	}
	if (n <= 64) {
		rte_mov32((uint8_t *)dst, (const uint8_t *)src);
		rte_mov16((uint8_t *)dst + 32, (const uint8_t *)src + 32);
		rte_mov16((uint8_t *)dst - 16 + n, (const uint8_t *)src - 16 + n);
		return ret;
	}
	if (n <= 128) {
		goto COPY_BLOCK_128_BACK15;
	}
	if (n <= 512) {
		if (n >= 256) {
			n -= 256;
			rte_mov128((uint8_t *)dst, (const uint8_t *)src);
			rte_mov128((uint8_t *)dst + 128, (const uint8_t *)src + 128);
			src = (const uint8_t *)src + 256;
			dst = (uint8_t *)dst + 256;
		}
COPY_BLOCK_255_BACK15:
		if (n >= 128) {
			n -= 128;
			rte_mov128((uint8_t *)dst, (const uint8_t *)src);
			src = (const uint8_t *)src + 128;
			dst = (uint8_t *)dst + 128;
		}
COPY_BLOCK_128_BACK15:
		if (n >= 64) {
			n -= 64;
			rte_mov64((uint8_t *)dst, (const uint8_t *)src);
			src = (const uint8_t *)src + 64;
			dst = (uint8_t *)dst + 64;
		}
COPY_BLOCK_64_BACK15:
		if (n >= 32) {
			n -= 32;
			rte_mov32((uint8_t *)dst, (const uint8_t *)src);
			src = (const uint8_t *)src + 32;
			dst = (uint8_t *)dst + 32;
		}
		if (n > 16) {
			rte_mov16((uint8_t *)dst, (const uint8_t *)src);
			rte_mov16((uint8_t *)dst - 16 + n, (const uint8_t *)src - 16 + n);
			return ret;
		}
		if (n > 0) {
			rte_mov16((uint8_t *)dst - 16 + n, (const uint8_t *)src - 16 + n);
		}
		return ret;
	}

	/**
	 * Make store aligned when copy size exceeds 512 bytes,
	 * and make sure the first 15 bytes are copied, because
	 * unaligned copy functions require up to 15 bytes
	 * backwards access.
	 */
	dstofss = (uintptr_t)dst & 0x0F;
	if (dstofss > 0) {
		dstofss = 16 - dstofss + 16;
		n -= dstofss;
		rte_mov32((uint8_t *)dst, (const uint8_t *)src);
		src = (const uint8_t *)src + dstofss;
		dst = (uint8_t *)dst + dstofss;
	}
	srcofs = ((uintptr_t)src & 0x0F);

	/**
	 * For aligned copy
	 */
	if (srcofs == 0) {
		/**
		 * Copy 256-byte blocks
		 */
		for (; n >= 256; n -= 256) {
			rte_mov256((uint8_t *)dst, (const uint8_t *)src);
			dst = (uint8_t *)dst + 256;
			src = (const uint8_t *)src + 256;
		}

		/**
		 * Copy whatever left
		 */
		goto COPY_BLOCK_255_BACK15;
	}

	/**
	 * For copy with unaligned load
	 */
	MOVEUNALIGNED_LEFT47(dst, src, n, srcofs);

	/**
	 * Copy whatever left
	 */
	goto COPY_BLOCK_64_BACK15;
}

更多相关的代码在rte_memcpy.h和rte_memcpy.c中,注意,它包含不同CPU架构平台的多个版本,不要搞混。
从上面的代码可以看到,影响拷贝速度的有以下几点:
1、字节对齐和数据的加载存储。
这个大家都明白,除了字节对齐速度加快外,而且DPDK中还对不同的字节对齐以及长度进行了控制,充分发挥SIMD的优势(说直白一点就是在条件允许的情况下,一次拷贝数量多【16字节:128位】,这个和平台支持有关)
2、函数和库调用开销,库函数需要调用过程,这个也浪费时间。这个库调用过程在编译选择优化的过程中,优化难度也比较大,不如在DPDK中直接调用,特别是使用
static __rte_always_inline(静态内联)时,这在网上有很多优化的比较,自己也可以试一试。
3、整体上来说,数据量越大,上面的优化越优势越大;否则优势则不明显。
上述的比较是针对库glibc以及DPDK相比而言的,至于个人优化过的则不在此范畴之内。另外,随着技术的进步,如果用高版本的glibc并开启优化后,可能效果差别也不大,这个没有进行比较。
有兴趣可以看看rte_mov256等几个函数。
需要说明的是,对于某一类函数,没有普遍最优之说。只有场景条件限制下的最合适。也就是说,DPDK的拷贝函数不代表此函数比glibc中的拷贝函数优秀,只是说明此函数在DPDK的应用场景下更合适。
最后总结一下,针对内存拷贝的优化点:
1、减少拷贝过程中的附加处理如字节对齐
2、在平台允许情况下使用最大带宽(拷贝最大数量)
3、使用平坦顺序内存并使用分支预测(减少分支跳转,如是否有范围重叠等)
4、有可能的情况下使用non-temporal访存执令
5、使用加速拷贝的一些指令(string操作指令等)。
6、处理大内存(M以上)和小内存(K以下)的不同场景(这个在一些常用框架中都会处理)

三、总结

性能和效率的提升,是一个系统工程。它可能会从一个点开始,然后不断的影响别的点,然后这些点又互相影响,最后蔓延到整个系统,形成一个量变到质变的过程。计算机应用也不外乎这样。
DPDK中通过Linux内核的一些设计(如大页),通过一种工程优化的手段来提高网络通信的效率,但反过来,内核也会借鉴DPDK的一些特点来吸收到内核中去。同样,DPDK的出现对硬件本身的设计也提出了虚拟化的相关等要求。硬件水平的提高又可以提高DPDK的性能。
国内的缺少的不是后面的一系列动作,缺少的恰恰是开始那个点,那个用于爆发的创新点。

你可能感兴趣的:(网络开发,DPDK,网络)