简介

本章提供了使用DPDK开发高效代码的一些技巧。有关更多的信息,请参考Intel®64和IA-32架构优化参考手册,这是编写高效代码的宝贵参考资料。

1、内存

 本节描述在DPDK环境中开发应用程序时的一些关键内存注意事项。

1.1内存拷贝:不要在数据平面中使用libc

在DPDK中可以通过Linux*应用程序环境获得许多libc函数。这可以简化应用程序的移植和配置平面的开发。然而,这些功能中有许多并不是为性能而设计的。像memcpy()或strcpy()这样的函数不应该在数据平面中使用。要复制小结构,首选的是可以由编译器优化的更简单的技术。有关建议,请参阅来自Intel Press的VTune™Performance Analyzer Essentials出版物。

对于经常调用的特定函数,最好提供一个自制的优化函数,该函数应该声明为静态内联函数。
DPDK API提供了一个优化的rte_memcpy()函数。

1.2 内存分配

libc的其他函数,如malloc(),提供了一种灵活的分配和释放内存的方法。在某些情况下,使用动态分配是必要的,但不建议在数据平面中使用类似malloclike的函数,因为管理碎片堆的成本可能很高,而且分配器可能没有针对并行分配进行优化。
如果您确实需要在数据平面中进行动态分配,那么最好使用固定大小对象的内存池。这个API由librte_mempool提供。这个数据结构提供了几个提高性能的服务,比如对象的内存对齐、对对象的无锁访问、NUMA感知、批量get/put和每lcore缓存。rte_malloc()函数使用与mempool类似的概念。

1.3 对同一内存区域的并发访问

多个lcore对同一内存区域的读写(RW)访问操作会导致大量的数据缓存丢失,这是非常昂贵的。例如,在统计的情况下,通常可以使用每lcore变量。至少有两个解决方案:

  • 使用RTE_PER_LCORE变量。注意,在这种情况下,lcore X上的数据对lcore Y是不可用的。

  • 使用一个结构表(每lcore一个)。在这种情况下,每个结构必须缓存对齐。

如果在同一高速缓存线路中没有RW变量,那么主要读取的变量可以在lcore之间共享,而不会造成性能损失。

跨存储通道分布

现代的内存控制器有几个可以并行加载或存储数据的内存通道。根据内存控制器及其配置的不同,通道的数量和内存在通道上的分布方式也不同。每个通道都有带宽限制,这意味着如果所有内存访问操作都只在第一个通道上执行,那么就存在潜在的瓶颈。
默认情况下,Mempool库在内存通道之间传播对象的地址。

锁定内存页

允许底层操作系统自行加载/卸载内存页。这些页面加载可能会影响性能,因为内核获取它们时进程处于暂停状态。
为了避免这些问题,可以预加载并使用mlockall()调用将它们锁到内存中。

2 多核之间的消息通信

为了在lcore之间提供基于消息的通信,建议使用DPDK ring API,它提供了一个无锁ring实现。
该ring支持批量和突发访问,这意味着只需一个昂贵的原子操作就可以从该ring中读取多个元素(参见ring库)。当使用批量访问操作时,性能会得到极大的提高。
删除消息的代码算法可能类似于以下内容:

#define MAX_BULK 32

 while (1) {
     /* Process as many elements as can be dequeued. */
     count = rte_ring_dequeue_burst(ring, obj_table, MAX_BULK, NULL);
     if (unlikely(count == 0))
         continue;

     my_process_bulk(obj_table, count);
}

3、 PMD驱动

DPDK轮询模式驱动程序(PMD)也可以在批量/突发模式下工作,允许对send或receive函数中的每个调用进行一些代码分解。
避免部分写道。当PCI设备通过DMA写入系统内存时,如果写入操作是在一个完整的高速缓存线路上,而不是部分高速缓存线路上,则成本更低。在PMD代码中,已经采取了尽可能多的操作来避免部分写操作。

4、 锁和原子操作

原子操作意味着在指令之前加上一个锁前缀,导致在执行以下指令时断言处理器的锁#信号。这对多核环境中的性能有很大的影响。
可以通过避免数据平面中的锁定机制来改进性能。它通常可以被其他解决方案(如per-lcore变量)所取代。此外,某些锁定技术比其他技术更有效。例如,Read-Copy-Update (RCU)算法可以频繁地替换简单的rwlocks。

5、编码问题

5.1、Inline函数

小函数可以在头文件中声明为静态内联函数。这避免了调用指令的成本(以及相关的上下文保存)。然而,这种方法并不总是有效的;它取决于许多因素,包括编译器。

5.2、分支预测

Intel®C/ c++ Compiler (icc)/gcc内置的帮助函数likely()和unlikely()允许开发人员指示是否可能采用代码分支。例如:

if (likely(x > 1))
    do_stuff();

6、设置目标CPU类型

DPDK通过DPDK配置文件中的CONFIG_RTE_MACHINE选项支持特定于CPU微体系结构的优化。优化的程度取决于编译器优化特定微体系结构的能力,因此最好尽可能使用最新的编译器版本。
如果编译器版本不支持特定的特性集(例如,Intel®AVX指令集),构建过程会优雅地降级为编译器支持的任何最新特性集。
由于构建目标和运行时目标可能不相同,因此生成的二进制文件还包含一个在main()函数之前运行的平台检查,并检查当前机器是否适合运行二进制文件。
除了编译器优化之外,还会自动将一组预处理器定义添加到构建流程中(与编译器版本无关)。这些定义对应于目标CPU应该能够支持的指令集。例如,为任何支持sse4.2的处理器编译的二进制文件将定义RTE_MACHINE_CPUFLAG_SSE4_2,从而为不同的平台启用编译时代码路径选择。

DPDK 的定位是基础组件, 提供的是基于X86平台的高效L2包转发方案. 这个方案包含一些最重要的底层组件,PMD/内存管理/初始化/etc.这些组件大多是为了使能硬件,优化硬件的用例.
VPP 的定位是解决方案. VPP 并不依赖特定的包转发框架, 当然在X86平台下, VPP集成DPDK的L2 包转发组件是一个明智选择.但VPP 更接近面向最终产品化的解决方案,尤其是对各类协议的支持更为完善. 调试框架,控制平面的接口都更加完备.
DPDK支持 Run To Completation/Pipe Line 两种模型, 当然取决于开发者怎么使用.
VPP 只支持 Run To Completation, 在IPSEC一些用例中会有比较难以处理的情况(e.g esn processing), 更加依赖网卡的 RSS功能.
VPP 有Honeycomb这样的控制平面接口, 支持netconf/yang. 这个对于产品化是非常重要的.DPDK 没有对应的组件.

一下文章来自博客:
https://www.cnblogs.com/sunnypoem/p/11368500.html

1.性能从何而来。
原文链接:

http://www.360doc.com/content/18/0428/20/53742993_749517107.shtml

https://steeven.iteye.com/blog/2347150 DPDK代码级别性能优化总结

https://www.jianshu.com/p/346bf99b2fb1

https://www.jianshu.com/p/ed914b24f6da

https://blog.csdn.net/Dgh19940/article/details/79603843

架构角度:DPDK的巨页、NUMA、D-cache优化,VPP 的I-cache优化;

算法角度:Bihash,查表lockless;

代码角度:Vector、宏构造函数、结构体cacheline对齐、线程绑核、指令预取、指令优化;

2.路由查找
https://blog.csdn.net/dog250/article/details/6596046

Internet路由之路由表查找算法概述-哈希/LC-Trie树/256-way-mtrie树

  1. constructor__修饰符
    通过一个简单的例子介绍一下gcc的__attribute
    ((constructor))属性的作用。gcc允许为函数设置__attribute__ ((constructor))和__attribute__ ((destructor))两种属性,顾名思义,就是将被修饰的函数作为构造函数或析构函数。程序员可以通过类似下面的方式为函数设置这些属性:

void funcBeforeMain() attribute ((constructor));

void funcAfterMain() attribute ((destructor));

4.unlikely
CPU是以流水线的方式执行程序指令。所谓流水线,可以简单理解为在执行一个指令的同时,读取下一条指令。对于程序中大量出现的if else while for ? :等含有条件判断的情景,CPU需要能够正确提取下一条指令以便流水线可以流畅执行下去。一旦提取的是错误分支的指令,虽然不影响程序运行的结果,但整条流水线都会被清空,再重新读入正确分支的指令,对程序运行效率影响颇大。

CPU一般都有硬件分支预测器,但我们也可以用likely()/unlikely()等方式显示指定,另外在设计程序的时候也以使分支判断具有一定的规律性为好,比如一组经过排序的输入数据。

为了最大限度减小Branch mispredication对性能带来的影响,可以将一些常见的分支判断转换为Branchless的形式。

链接:https://www.jianshu.com/p/ed914b24f6da

long __builtin_expect(long exp, long c);!!©的效果是得到一个布尔值,该函数的作用是更好的分支预测;使用likely() ,执行if后面的语句的机会更大,使用unlikely(),执行else后面的语句的机会更大,原理“It optimizes things by ordering the generated assembly code correctly, to optimize the usage of the processor pipeline. To do so, they arrange the code so that the likeliest branch is executed without performing any jmp instruction (which has the bad effect of flushing the processor pipeline).”

主要是分支预测失误,指令的跳转带来的性能会下降很多。为什么呢?从《深入理解计算机系统》书上摘取,p141:“另一方面,错误预测一个跳转要求处理器丢掉它为该跳转指令后所有指令已经做了的工作,然后再开始用从正确位置处起始的指令去填充流水线,大约会浪费20〜40个时钟周期”;从汇编代码层面理解参考文末第一个引用。

链接:https://www.jianshu.com/p/346bf99b2fb1

5.巨页hugepage
https://www.cnblogs.com/small-office/p/9766536.html 绑核与巨页

https://www.cnblogs.com/031602523liu/p/10537694.html UIO、巨页、CPU亲和性、NUMA简介

https://blog.csdn.net/qq_33611327/article/details/81738195 mmap分析

系统能否支持大页,支持大页的大小为多少是由其使用的处理器决定的。

大页预留之后,接下来则涉及使用的问题。DPDK使用HUGETLBFS来使用大页。首先,它需要把大页mount到某个路径 比如 /mnt/huge,或者/dev/hugepages/,接下来,DPDK运行的时候,会使用mmap()系统调用把大页映射到用户态的虚拟地址空间,然后就可以正常使用了。

由于DPDK是运行在用户空间,而巨页是在内核态的,因此通过mmap实现了用户空间到内核空间的快速访问。

  1. cpu亲和性
    https://www.cnblogs.com/031602523liu/p/10537694.html

pthread_setaffinity_np 设置线程运行在特定CPU核上

CPU的亲和性也就是cpu affinity机制,指的是进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他处理器, 通过处理器关联可以将虚拟处理器映射到一个物理处理器上 ,也就是说把一个程序绑定到一个物理CPU上。

而且在多核运行的机器上,每个CPU本身自己会有缓存,缓存着进程使用的信息,而进程可能会被OS调度到其他CPU上,如此,CPU cache命中率就低了。当一个进程或线程绑定CPU后,程序就会一直在指定的cpu跑,不会由操作系统调度到其他CPU上,减少了cache miss,提高性能和效率。

  1. D-cache & I-cache
    7.1 I-Cache

VPP按组处理报文,解决I-cache抖动问题。

7.2 D-Cache

原文链接:https://blog.csdn.net/Dgh19940/article/details/79603843 DPDK中的Cache优化

1)写接受描述符到内存,填充数据缓冲区指针,网卡接收到报文后就根据该地址把报文内容填进去。

2)从内存中读取接收描述符(到接收到报文时,网卡会更新该结构)(内存读),从而确认是否收到报文。

3)从接收描述符确认收到报文时,从内存中读取控制结构体的指针,再从内存中读取控制结构体,把从接收描述符中读取的信息填充到该控制结构体(内存读)。

4)更新接收队列寄存器,表示软件接收到了新的报文。

5)从内存读取报文头部(内存读),决定转发端口。

6)从控制结构体把报文信息填入到发送队列发送描述符中,更新发送队列寄存器。

7)从内存中读取发送描述符(内存读),检查是否有包被硬件发送出去。

8)如果有的话,则从内存中读取相应控制结构体(内存读),释放数据缓冲区。

可以看出处理一个报文的过程中,需要6次读取内存(上文(内存读))。

换句话说要保证在80个时钟周期处理完一个报文DPDK就必须保证要读取的数据Cache命中,否则一旦Cache不命中,性能会严重下降。

7.3 Cache一致性

当定义的数据结构或者分配了数据缓冲区之后,内存中就有了一个地址和其相对应,然后程序进行读写。在读的过程中,首先是内存加载到Cache,随后送到处理器内部的寄存器;在写操作的时候则是从寄存器送到Cache,最后由总线回写到内存。

这样会出现两个问题:

1)数据结构/数据缓冲区对应的Cache Line是否对齐?如果不是的话,即使数据区域小于Cache Line的话也会占用两个Cache Line;另外假如上一个CacheLine属于另一个数据结构且被另一个处理器核处理,数据如何同步呢?

答案:结构体对齐__rte_cache_aligned;

Doing this can often make copy operations more efficient, because the compiler can use whatever instructions copy the biggest chunks of memory when performing copies to or from the variables or fields that you have aligned this way.

2)假设数据结构/缓冲区的起始地址是CacheLine对齐的,但是有多个核同时对该内存进行读写,如何解决冲突?

答案:DPDK解决方案很简单,首先避免多个核访问同一个内存地址或者数据结构。每个核尽量避免与其他核共享数据,从而减少因为错误的数据共享导致的Cache一致性开销。

  1. Bihash
  2. 数据预取
    __builtin_prefetch()

https://www.cnblogs.com/dongzhiquan/p/3694858.html

VPP中大量报文处理的数据处理操作

p4 = vlib_get_buffer(vm, from[4]);

vlib_prefetch_buffer_header (p4, LOAD);

CLIB_PREFETCH (p4->data, CLIB_CACHE_LINE_BYTES, STORE);

Logo

更多推荐