DPDK编程指导——编写高效代码(翻译)
34 writing efficient code 编写有效的代码
34.1 Memory 内存
本节介绍一些关键的内存考虑点,当在DPDK环境开发应用程序时。
34.1.1 Memory Copy: Do not Use libc in the Data Plane 内存拷贝:不要再数据面使用lic
libc中的很多函数不是为性能设计的。例如 memcpy() 或 strcpy() 不应该被用在 data plane。若是拷贝小结构,性能是个简单的技术,可以被编译器优化。请参阅《VTune ™性能优化要点》来自Interl新闻的建议。
对于特殊函数,经常被调用的,这是一个好主意,提供一个自制的优化函数,应该被声明为静态内联。
DPDK API 提供了一个优化的 rte_memcpy() 函数。
34.1.2 Memory Allocation 内存申请
libc中的其他函数,像 mallc(),提供了一种灵活的方式申请和释放内存。在某些情况下,使用动态分配是必须的,但是真的不建议在 data plane 使用 malloc(),因为管理分裂的堆是昂贵的,并且分配器可能未被优化针对并行申请。
如果你真的需要动态分配内存在 data plane,最好使用一个大小固定对象的内存池。librte_mempool提供了这样的API。这种书结构提供了几种提升性能的服务,像对象的内存对齐,无锁访问对象,NUMA意识,批量 get/put 和 per-lcore 缓存。rte_malloc() 函数使用了桶mempools相似的概念。
数据面动态分配内存使用rte_malloc()。
34.1.3 Concurrent Access to the Same Memory Area 并发访问同一内存区域
几个核的读写访问操作对同一内存区域,会产生很多data cache miss,性能消耗非常高。这种情况经常会使用per-lcore变量,例如,在统计的时候。至少有两个情况适合:
1、使用RTE_PER_LCORE变量
2、使用表结构(每个核一个,即每核的),这种情况,每个结构必须是cache-aligned(缓存对齐)
读为主的变量可以被多核共享,而没有性能损耗,如果没有读写变量在同一个cache line中。
34.1.4 NUMA 非统一内存访问
在NUMA系统上,最好访问本地内存,因为访问远地内存慢。
memzone、ring、rte_malloc 和 mempool 提供了在指定的socket上创建池的接口。
有时候,这是一个好主意,复制一份数据(每个socket一份)来提升速度。对于读为主经常被访的变量,让他们只存在于一个socket上不是问题,因为数据会一直保存着cache中。
34.1.5 Distribution Across Memory Channels 分散访问内存通道
现代内存控制器有好几个内存通道可以并行的加载或存储数据。根据内存控制器和它的配置,通道数和通过通道分配内存的方式各不相同。每个通道有带宽限制,这意味着,如果一个通道做了所有的内存访问操作,有一个潜在的瓶颈。
默认情况下,内存库会在几个通道之前传递对象的地址。
34.2 Communication Between lcores 核间通信
为了提供核间基于消息的通信方式,建议使用DPDK ring API,它提供了一个无锁的实现。
ring支持批量和突发(bulk/burst)访问,这意味着它可以从ring里读取几个元素,只消耗一次原子操作。当使用批量访问操作的时候性能有极大的提升。
34.3 PMD Driver
DPDK的Poll Mode Driver同样能工作在批量和突发(bulk/burst)模式。允许代码分解为每个调用者在发送或接收功能上。
避免部分写。当PCI设备通过DMA写到系统内存时,它消耗很少如果写操作实在一个完成的cache line上,而不是cache line的一部分上。再PMD的代码中,已经采取动作尽尽可能的避免部分写(partial write)。
34.3.1 Lower Packet Latency 低数据包延迟
传统上,吞吐和延迟间存在一个权衡。可以调整应用程序实现高吞吐,但数据包平均的端到端的延迟将随之提升。同样,应用程序可以被调整到至始至终的低延迟,成本是吞吐变低。
为了实现更高的吞吐,DPDK尝试聚合处理成本,每个数据包融进了突发的数据包处理中。
使用testpmd应用程序作为例子,burst size可以被设置为16通过命令行。这允许应用程序可以一次请求16个数据包从PMD。testpmd应用程序然后立即尝试传递所有收到的数据包,在这里,是16个数据包。
数据包不会被发送,直到tail指针在被网卡相应的发送队列被更新。这种行为是期望的,当调整为高吞吐,因为RX和TX队里尾指针更新的成本可以跨越16个包。有效的隐藏写入PCIe设备相对较慢的MMIO的成本。
然而,这不是非常可取的有对于低延迟,因为被收到的第一个数据包必须等待另外15个包被接收。他不能被传递知道其他15个包也被处理了,因为网卡不会知道要传送数据包,只到TX尾指针被更新。尾指针不会被更新,知道16个包都被处理了。
为了始终保持低延迟,即使在高负载情况下,应用程序开发者也应该避免批量处理。testpmd可以把burst的值调整为1,这样可以保证一次只处理一个包,提供更低的延迟,但是会降低吞吐。
34.4 Locks and Atomic Operations 锁和原子操作
原子操作意味着在指令之前暗含着lock前缀,这会引起处理器的LOCK信号被断言在执行后续指令期间。这在多核环境下对性能有很大的影响。
通过在数据平面避免锁机制,让性能得到提升。锁总是可以被替代通过其他的方法,例如使用每核变量。此外,一些锁技术更有效比其他的。例如,RCU算法可以频繁的替换简单的读写锁。
34.5 Coding Considerations 编码注意事项
34.5.1 Inline Functions 内联函数
使用 static inline ,小的函数可以在头文件中声明为静态内联。避免call质量的消耗,相关联的上下文的保存。然而这种技术并不是总有效,取决于许多因素,包括编译器。
34.5.2 Branch Prediction 分支预测
使用 likely()、unlikely()
34.6 Setting the Target CPU Type 设置目标CPU类型
DPDK支持CPU特定微体系结构优化,通过DPDK配置文件里的CONFIG_RTE_MACHINE选项。优化程度取决于编译器的优化为特定微体系结构,因此最好使用最新的编译器版本,只要有可能。
如果编译器版本不支持特定的功能集(例如,Intel AVX指令集),生成过程优雅地降低到任何最新的功能集都被编译器支持。
因为生成和运行目标可能不相同,产生的二进制文件也包含一个平台检查,在main()函数之前运行并且检查当前的机器是否适合运行二进制文件。
随着编译器的优化,一套处理器定义被自动添加到生成过程(忽视编译器版本)。这些定义对应目标CPU应该能够支持的着指令集。例如,一个编译的二进制文件对于任何SSE4.2处理器将有RTE_MACHINE_CPUFLAG_SSE4_2定义,因此可以为不同的平台选择相应的编译时代码路径。
编译优化 -O3
35 PROFILE YOUR APPLICATION 分析应用程序
英特尔处理器提供的性能计数器来监控事件。由英特尔提供的一些工具可以用来分析和基准测试应用程序。请参阅从英特尔按VTune性能分析器必备出版物以获取更多信息。
对于DPDK的应用程序,这可以在Linux应用程序只的环境中进行。
应通过事件计数器进行监控的主要情况是:
·Cache misses 高速缓存未命中
·Branch mis-predicts 分支误预测
·DTLB misses
·Long latency instructions and exceptions 长延迟指令和异常
请参阅英特尔性能分析指南,了解应用程序分析的详细信息。