__builtin_prefetch 函数简介

前言

在高性能的网络转发中,比如 vpp/dpdk,通常对数据包采用批处理。批处理有很多好处:上下文的切换成本,会分摊给每一个数据包;CPU能一次性,将多个数据包所需的数据,加载到高速缓存中处理,减少了等待数据从主内存读取的次数,从而极大提升效率。

本文介绍,预取函数 __builtin_prefetch 。介绍它的使用和作用机理。

然后,做一个预取实验,查看预取,对性能的实际影响情况。

__builtin_prefetch 函数介绍

基本介绍

__builtin_prefetch 是 GCC 编译器提供的一个内置函数,用于通过手动提示CPU提前将数据从主内存加载到缓存中,从而减少缓存缺失(cache miss)带来的访问延迟,以提升程序性能。__builtin_prefetch 函数的原型如下:

void __builtin_prefetch (const void *addr, ...)

它接受一到三个参数:

  • addr,要预取的数据的内存地址(必须提供)
  • rw,访问类型(可选,默认为0)。 0: 预取数据用于读操作;1: 预取数据用于写操作;
  • locality,数据的时间局部性(可选,默认为3)。0: 无局部性,访问后即可从缓存中移除;1: 低局部性;2: 中等局部性;3: 高局部性,应尽可能保留在缓存中;
  • __builtin_prefetch并不直接指定目标缓存级别,而是通过提示性的参数,与CPU协同决定,最终目标通常是尽可能靠近核心的高速缓存(如L1或L2)。比如 locality 为 3,编译器会尝试将数据预取到最接近CPU的高速缓存(如L1数据缓存)中,以期获得最快的访问速度。

__builtin_prefetch 通常预取一个完整的缓存行,其大小取决于具体的处理器,而 64 字节是现代常见处理器架构的典型值。

使用该函数时,需要考虑以下几点:

  • 正确性优先:该函数不会验证地址有效性。如果传入非法地址(如NULL),它通常不会导致程序崩溃,但预取操作本身是无效的。必须确保地址表达式是合法的。
  • 预取时机:预取指令需要提前足够的时间发出,以确保数据在被使用前能完成加载。预取过早可能挤占缓存中有用的数据;预取过晚则可能数据还未到位,无法起到加速作用。
  • 避免过度预取:盲目预取可能会污染缓存,将真正需要的数据挤出去,反而降低性能。
  • 编译器优化的影响:在高优化级别(如 -O3)下,编译器可能已经自动生成了高效的预取指令,此时手动添加的 __builtin_prefetch可能收效甚微甚至适得其反。
  • 平台相关性:这是一个编译器扩展,并非标准C/C++的一部分。虽然GCC和Clang都支持,但其行为和效果会因处理器架构的不同而存在差异。

作用机理

考虑一个问题,如果程序总是顺序运行。即,先访存取数据,然后再执行。那预取没有任何意义,因为它总是要消耗一定的时间。

预取之所以有效,在于 CPU 的流水线技术。流水线技术,在一定程度上,使得不同指令的访存和执行,可以并行。

关于 CPU 流水线的内容,我还是上学那会,从《计算机体系结构》课上知道的点。具体关于流水线的介绍,请参考其他资料。

实验

这个实验参考自:VPP/Missing Prefetches – fd.io

不过实验对象是:Sample plugin for VPP — The Vector Packet Processor

# 使用连个网卡,两个网卡使用xconnet方式连接
set interface state ens161 up
set interface state ens224 up
set interface l2 xconnect ens161 ens224
set interface l2 xconnect ens224 ens161

# 编译 sample plugin
SAMPLE_PLUGIN=yes make build
cp ./build-root/build-vpp_debug-native/sample-plugin/lib/vpp_plugins/sample_plugin.so ./build-root/build-vpp_debug-native/vpp/lib
64/vpp_plugins/

# 使用trex给上面连个网卡打流量

默认情况下,sample plugin 使用了预取函数。sample node,执行一次,耗时约 2.33e2 时钟周期。

Thread 2 vpp_wk_1 (lcore 3)
Time 96.8, 10 sec internal node vector rate 14.85 loops/sec 1453675.30
  vector rates in 3.9435e4, out 3.9435e4, drop 0.0000e0, punt 0.0000e0
             Name                 State         Calls          Vectors        Suspends         Clocks       Vectors/Call  
dpdk-input                       polling         215610621         3817733               0          2.98e4             .02
ens224-output                    active             259829         3817733               0          2.25e2           14.69
ens224-tx                        active             259829         3817733               0          1.42e4           14.69
interface-output                 active             259829         3817733               0          3.74e2           14.69
sample                           active             259829         3817733               0          2.33e2           14.69

把预取函数移除后,sample node,执行一次,耗时约 2.73e2 时钟周期。

Thread 2 vpp_wk_1 (lcore 3)
Time 204.6, 10 sec internal node vector rate 14.61 loops/sec 1587833.16
  vector rates in 1.7476e4, out 1.7476e4, drop 0.0000e0, punt 0.0000e0
             Name                 State         Calls          Vectors        Suspends         Clocks       Vectors/Call  
dpdk-input                       polling         537679641         3575261               0          7.63e4             .01
ens224-output                    active             256434         3575261               0          2.26e2           13.94
ens224-tx                        active             256434         3575261               0          1.33e4           13.94
interface-output                 active             256434         3575261               0          3.74e2           13.94
sample                           active             256434         3575261               0          2.73e2           13.94

也就是说,预取函数带来了 (2.73e2 -2.33e2 )/2.73e2 = 15%的性能提升。

当然,在实际网络场景中,我们还是得看 cache miss 是不是性能瓶颈。是瓶颈的情况下,我们再考虑是否使用预取函数。

其他

上面 sample-plugin 有个比较有意思的地方。

	  src_dst0 = ((u8x16 *) en0)[0];
	  src_dst1 = ((u8x16 *) en1)[0];
	  src_dst0 = u8x16_shuffle (src_dst0, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3,
				    4, 5, 12, 13, 14, 15);
	  src_dst1 = u8x16_shuffle (src_dst1, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3,
				    4, 5, 12, 13, 14, 15);
	  ((u8x16 *) en0)[0] = src_dst0;
	  ((u8x16 *) en1)[0] = src_dst1;

typedef __attribute((vector_size(16))) u8 u8x16

#if defined(__GNUC__) && !defined(__clang__)
#define __builtin_shufflevector(v1, v2, ...)                                  \
  __builtin_shuffle ((v1), (v2), (__typeof__ (v1)){ __VA_ARGS__ })
#endif

#define u8x16_shuffle(v1, ...)                                                \
  (u8x16) __builtin_shufflevector ((u8x16) (v1), (u8x16) (v1), __VA_ARGS__)

关于 __builtin_shuffle 的介绍,可参考 Vector Extensions (Using the GNU Compiler Collection (GCC))

__builtin_shuffle允许在编译时直接对向量类型中的元素进行重新排列或“洗牌”。与在运行时使用循环进行元素重排相比,__builtin_shuffle 是在编译时完成所有重排逻辑的。编译器会直接将其优化为极少数甚至一条特定的 SIMD 指令,因此效率非常高。

我的建议是,不使用这类内置函数。写起来费脑子,维护起来还麻烦。软件层面上,性能丢一点就丢一点。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇