Rust对比C 之DPDK

Rust 是一种为性能和安全性,尤其是安全并发性而设计的编程语言。它的语法类似于 C++,但它可以使用借用检查器来验证引用来保证内存安全。

DPDK(Data Plane Development Kit 数据平面开发工具包)是一组用于实现 NIC(网络接口控制器)的用户空间驱动程序的库。它提供了一组抽象,允许对复杂的数据包处理管道进行编程。 DPDK 允许在编程网络应用程序时实现高性能。 DPDK 是用 C 编写的,因此如果没有适当准备的 API,在 Rust 中使用它是不方便且不安全的。因此,我们决定为 DPDK 创建 Rust 绑定。

我们不是第一个尝试它的人。我们决定将我们的 API 基于其他项目 — https://github.com/ANLAB-KAIST/rust-dpdk。该项目在编译代码以生成与指定 DPDK 版本的绑定时使用 bindgen。多亏了这一点,将 API 更新到较新的 DPDK 版本并不难。此外,很多高级 API 已经写得很好,所以我们不需要从头开始编写它。最终,我们只为这个库添加了一些功能并修复了一些问题。

与 DPDK 通信的接口的设计方式使程序员不必记住不明显的依赖关系,这些依赖关系通常会导致 DPDK 应用程序出错。检查 l2fwd 来源以供参考。

Rust vs C: l2fwd results comparison

Performance comparison of l2fwd written in C and l2fwd written in Rust.

用 C 编写的 l2fwd 和用 Rust 编写的 l2fwd 的性能比较。

环境描述

我们将两个应用程序在一个裸机环境中与两个 Intel Xeon Gold 6252 CPU 的性能进行了比较。 l2fwd 使用了第一个 CPU(NUMA 节点 0)的一个内核,TRex 流量生成器使用了另一个 CPU(NUMA 节点 1)的 16 个内核。两个应用程序都为其 NUMA 节点使用本地内存,因此它们不会共享可能对性能产生一些影响的资源(例如缓存、内存)。此外,l2fwd 使用连接到 NUMA 节点 0 的 25Gbps 英特尔以太网网络适配器 XXV710 的单个接口和一个用于流量管理的 RX 和 TX 队列。 TRex 使用带有一个 25 Gbps 接口的 NIC,该接口连接到 NUMA 节点 1。

BM CPU 的详细信息:

lshw -class processor
  *-cpu:0
       description: CPU
       product: Intel(R) Xeon(R) Gold 6252 CPU @ 2.10GHz
       vendor: Intel Corp.
       vendor_id: GenuineIntel
       physical id: 13
       bus info: cpu@0
       version: Intel(R) Xeon(R) Gold 6252 CPU @ 2.10GHz
       slot: CPU0
       size: 3338MHz
       capacity: 4GHz
       width: 64 bits
       clock: 100MHz
       capabilities: lm fpu fpu_exception wp vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp x86-64 constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 cdp_l3 invpcid_single intel_pt ssbd mba ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke avx512_vnni md_clear spec_ctrl intel_stibp flush_l1d arch_capabilities cpufreq
       configuration: cores=24 enabledcores=24 threads=48
  *-cpu:1
       description: CPU
       product: Intel(R) Xeon(R) Gold 6252 CPU @ 2.10GHz
       vendor: Intel Corp.
       vendor_id: GenuineIntel
       physical id: c
       bus info: cpu@1
       version: Intel(R) Xeon(R) Gold 6252 CPU @ 2.10GHz
       slot: CPU1
       size: 3699MHz
       capacity: 4GHz
       width: 64 bits
       clock: 100MHz
       capabilities: lm fpu fpu_exception wp vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp x86-64 constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 cdp_l3 invpcid_single intel_pt ssbd mba ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke avx512_vnni md_clear spec_ctrl intel_stibp flush_l1d arch_capabilities cpufreq
       configuration: cores=24 enabledcores=24 threads=48

生成的流量由具有单个 IPv4 的二层数据包和具有不同 IP 地址和 UDP 端口的 UDP 标头组成。当测试更大的数据包大小时,会在数据包的末尾添加额外的数据。有关详细信息,请参阅流量说明。

在测试时,我们还测量了核心利用率。在主函数中,l2fwd 在无限循环中轮询传入的数据包。与传统的中断数据包处理相比,我们可以获得更好的性能

每个循环,l2fwd 尝试使用 rte_eth_rx_burst() 读取最多 32 个数据包。我们可以计算从这些调用中收到的平均数据包数。如果平均值很高,则意味着在大多数循环中 l2fwd 接收并处理了一些数据包。另一方面,如果这个值很低,这意味着 l2fwd 主要是循环而不做任何关键的工作。

负载测试

过载测试包括发送尽可能多的流量,超出 l2fwd 的处理能力。在这种情况下,我们想测试软件性能,因为我们知道 l2fwd 总是有一些工作要做。

请注意,结果中实际发送的数据包大小包括数据包数据和 TRex 添加的额外 13 个字节(前导和 IFG)。

Language pkt size real pkt size test duration TRex TX TRex RX l2fwd TX l2fwd RX l2fwd drops Avg RX burst size
C 64 77 30 1116071337 650199801 650199801 1077181386 426981585 24.54
Rust 64 77 30 1116071337 647077942 647077942 911037787 263959845 32

我们可以看到 Rust 在这个测试中取得了更差的结果。 Rust l2fwd 收到的数据包比 CLANG l2fwd 少 1.2 左右,发送的数据包也更少。此外,我们可以看到,在 Rust l2fwd 的情况下,平均 RX 突发最大(32),在 CLANG l2fwd 的情况下,我们实现了大约 24.5 RX 突发大小。这一切都意味着 C 实现总体上更快,因为它可以处理更多的数据包。

两种应用的掉率都非常高。最可能的原因是我们使用了单个 TX 队列,它无法处理更多数据包。它还解释了为什么 C 实现的丢弃率高于 Rust。两个应用程序都希望发送超出 TX 队列处理能力的数据,但 C 比 Rust 更快,并试图发送更多数据包,这最终导致更大的丢包率。

RFC2544 结果

结论

在测试时,我们注意到每个 RX 突发的平均数据包数总是小于 2。这意味着 l2fwd 在测试期间主要是空闲的,这解释了为什么 C 和 Rust 的结果如此相似。为了看到更多差异,我们应该使用更复杂的应用程序对其进行测试。