• 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171

    更多相关的代码在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的性能。
    国内的缺少的不是后面的一系列动作,缺少的恰恰是开始那个点,那个用于爆发的创新点。

  • 相关阅读:
    Web开发相关
    【Java开发】 Spring 04:云服务器 Docker 环境下安装 Redis 并连接 Spring 项目实现简单 CRUD
    【探索程序员职业赛道:挑战与机遇】
    英语一和英语二难度差多少?英语二翻译更长为什么说其实更容易?
    两大产品上线“粤复用”,赋能大数据智能行业发展
    JavaScript中 判断网络状态的几种方法
    Java中的运算符
    Linux文件权限
    Vue条件判断及循环遍历(v-if、v-elseif、v-else、v-show、v-for)
    【学习笔记】CF573E Bear and Bowling
  • 原文地址:https://blog.csdn.net/fpcc/article/details/133606049