对于转发层面的关键节点(node),VPP针对处理器架构编译多份代码,在运行时检测处理器架构,动态确定使用的代码分支。VPP提供两种对多处理器架构的支持,除了节点函数外,还可指定任意函数支持多架构。
node节点多架构
编译系统将node函数所在文件编译多次,每次使用不同的编译选项,生成多个node函数版本。每个node的构造函数(constructor)根据处理器硬件选择对应的版本,构造函数将结构vlib_node_registration_t的成员function附上选择的函数版本。
VLIB_NODE_FN (ip4_rewrite_node) (vlib_main_t * vm, vlib_node_runtime_t * node,
vlib_frame_t * frame)
{
if (adj_are_counters_enabled ())
return ip4_rewrite_inline (vm, node, frame, 1, 0, 0);
else
return ip4_rewrite_inline (vm, node, frame, 0, 0, 0);
}
所以在使用宏VLIB_REGISTER_NODE定义节点时(初始化vlib_node_registration_t结构),不要指定function成员。
VLIB_REGISTER_NODE (ip4_rewrite_node) = {
.name = "ip4-rewrite",
.vector_size = sizeof (u32),
...
};
node函数所在文件中,不重要的函数,如错误字符串定义和报文追踪函数(trace),不需要支持多处理器架构。使用"#ifndef CLIB_MARCH_VARIANT...#endif"来去除。
#ifndef CLIB_MARCH_VARIANT
/* Common trace function for all ip4-forward next nodes. */
void
ip4_forward_next_trace (vlib_main_t * vm,
vlib_node_runtime_t * node,
vlib_frame_t * frame, vlib_rx_or_tx_t which_adj_index)
{
}
#endif
CMakeLists.txt文件中需要将ip/ip4_forward.c文件同时添加到VNET_SOURCES和VNET_MULTIARCH_SOURCES链表,add_vpp_library将对其处理。
list(APPEND VNET_SOURCES
ip/ip4_forward.c
)
list(APPEND VNET_MULTIARCH_SOURCES
ip/ip4_forward.c
)
add_vpp_library(vnet
SOURCES ${VNET_SOURCES}
MULTIARCH_SOURCES ${VNET_MULTIARCH_SOURCES}
INSTALL_HEADERS ${VNET_HEADERS}
节点函数调用的子函数(ip4_rewrite_inline),需要使用always_inline修饰符。否则,编译器很可能不会生成多架构代码。可以在vpp运行时用perf top命令检查,比如当前处理器支持avx2,可以看到许多类似"xxx_node_fn_avx2"名字的节点处理函数。如果某个节点的名称为"xxx_inline.isra.1",表明其很可能没有使用always_inline修饰符,而是使用了static inline。
always_inline uword
ip4_rewrite_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_frame_t *frame, int do_counters, int is_midchain,
int is_mcast)
{
任意函数多架构
可以使用CLIB_MARCH_FN修饰需要支持多架构的函数,在调用函数时使用CLIB_MARCH_FN_SELECT选择特定的版本。
CLIB_MARCH_FN (svm_fifo_copy_to_chunk, void, svm_fifo_t *f,
svm_fifo_chunk_t *c, u32 tail_idx, const u8 *src, u32 len,
fs_sptr_t *last)
{
类似于node节点的多架构支持,CLIB_MARCH_FN宏所在源文件也需要多次编译。比如,在生成的vpp程序中,可能出现多个此函数的版本:svm_fifo_copy_to_chunk_avx2、svm_fifo_copy_to_chunk_avx512等。
函数svm_fifo_copy_to_chunk调用了以上的svm_fifo_copy_to_chunk函数,由于svm_fifo_copy_to_chunk为多架构版本,svm_fifo_copy_to_chunk函数不需要进行多架构编译,使用#ifndef CLIB_MARCH_VARIANT禁止多架构编译功能。
CLIB_MARCH_FN_SELECT的开销相等于间接函数调用。
#ifndef CLIB_MARCH_VARIANT
static inline void
svm_fifo_copy_to_chunk (svm_fifo_t *f, svm_fifo_chunk_t *c, u32 tail_idx,
const u8 *src, u32 len, fs_sptr_t *last)
{
CLIB_MARCH_FN_SELECT (svm_fifo_copy_to_chunk) (f, c, tail_idx, src, len,
last);
}
#endif
另外,还可以使用CLIB_MARCH_FN_REGISTRATION和CLIB_MARCH_FN_POINTER来实现函数的多架构编译。
多处理器架构
在文件vpp/src/cmake/cpu.cmake文件中,加入对多架构的支持(add_vpp_march_variant)。对x86_64处理器,默认使用corei7和corei7-avx编译选项。多架构支持增加了haswell、tremont、skylake和icelake四种架构。默认关闭了tremount架构的支持(OFF)。skylake和icelake架构的支持事编译器而定。
if(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64.*|x86_64.*|AMD64.*")
set(VPP_DEFAULT_MARCH_FLAGS -march=corei7 -mtune=corei7-avx)
add_vpp_march_variant(hsw
FLAGS -march=haswell -mtune=haswell
)
add_vpp_march_variant(trm
FLAGS -march=tremont -mtune=tremont
OFF
)
if (GNU_ASSEMBLER_AVX512_BUG)
message(WARNING "AVX-512 multiarch variant(s) disabled due to GNU Assembler bug")
else()
add_vpp_march_variant(skx
FLAGS -march=skylake-avx512 -mtune=skylake-avx512 -mprefer-vector-width=256
)
add_vpp_march_variant(icl
FLAGS -march=icelake-client -mtune=icelake-client -mprefer-vector-width=512
)
endif()
使用cmake内置函数check_c_compiler_flag检查C编译器是否支持指定的flag,对于支持的flag添加到MARCH_VARIANTS,比如最终的值可能为:(hsw; -march=haswell -mtune=haswell;skx; -march=skylake-avx512 -mtune=skylake-avx512 -mprefer-vector-width=256)。MARCH_VARIANTS_DISABLED的值为:(trm; -march=tremont -mtune=tremont)。
check_c_compiler_flag函数参见https://cmake.org/cmake/help/latest/module/CheckCCompilerFlag.html
macro(add_vpp_march_variant v)
if(ARG_FLAGS)
set(flags_ok 1)
set(fs "")
foreach(f ${ARG_FLAGS})
string(APPEND fs " ${f}")
string(REGEX REPLACE "[-=+]" "_" sfx ${f})
if(NOT DEFINED compiler_flag${sfx})
check_c_compiler_flag(${f} compiler_flag${sfx})
endif()
if(NOT compiler_flag${sfx})
unset(flags_ok)
endif()
endforeach()
if(flags_ok)
string(TOUPPER ${v} uv)
if (VPP_MARCH_VARIANT_${uv})
list(APPEND MARCH_VARIANTS "${v}\;${fs}")
else()
list(APPEND MARCH_VARIANTS_DISABLED "${v}\;${fs}")
endif()
endif()
endif()
endmacro()
多架构源文件
使用add_vpp_library添加多架构源文件,最终调用函数vpp_library_set_multiarch_sources。
macro(add_vpp_library lib)
cmake_parse_arguments(ARG
"LTO"
"COMPONENT"
"SOURCES;MULTIARCH_SOURCES;API_FILES;LINK_LIBRARIES;INSTALL_HEADERS;DEPENDS"
${ARGN}
)
if(ARG_MULTIARCH_SOURCES)
vpp_library_set_multiarch_sources(${lib} DEPENDS ${ARG_DEPENDS} SOURCES ${ARG_MULTIARCH_SOURCES})
endif()
为每个架构生成不同的库文件,对于vnet,生成基础的vnet,以及vnet_skx和vnet_hsw。
macro(vpp_library_set_multiarch_sources lib)
cmake_parse_arguments(ARG
""
""
"SOURCES;DEPENDS;FORCE_ON"
${ARGN}
)
set(VARIANTS "${MARCH_VARIANTS}")
foreach(V ${VARIANTS})
list(GET V 0 VARIANT)
list(GET V 1 VARIANT_FLAGS)
set(l ${lib}_${VARIANT})
add_library(${l} OBJECT ${ARG_SOURCES})
set_target_properties(${l} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_compile_definitions(${l} PUBLIC CLIB_MARCH_VARIANT=${VARIANT})
separate_arguments(VARIANT_FLAGS)
target_compile_options(${l} PUBLIC ${VARIANT_FLAGS})
target_sources(${lib} PRIVATE $<TARGET_OBJECTS:${l}>)
endforeach()
endmacro()
如下生成的不同架构的目标文件。vnet_objs.dir目录包括所有源文件的目标文件,而以后缀hsw和skx的目录仅包括需要多架构支持的源文件的目标文件。
$ ls build-root/build-vpp-native/vpp/CMakeFiles/vnet/CMakeFiles/vnet_objs.dir/ip/
ip_in_out_acl.c.o ip4_punt_drop.c.o ip6_punt_drop.c.o ip_path_mtu_node.c.o reass
ip6_forward.c.o ip4_forward.c.o ip6_hop_by_hop.c.o ip4_input.c.o ip6_input.c.o
ip4_mtrie.c.o ip6_link.c.o ip_frag.c.o ip_types.c.o ...
$
$ ls build-root/build-vpp-native/vpp/CMakeFiles/vnet/CMakeFiles/vnet_hsw.dir/ip
ip4_forward.c.o ip4_punt_drop.c.o ip6_hop_by_hop.c.o ip6_punt_drop.c.o ip_path_mtu_node.c.o reass
ip4_input.c.o ip6_forward.c.o ip6_input.c.o ip_in_out_acl.c.o punt_node.c.o
$
$
$ ls build-root/build-vpp-native/vpp/CMakeFiles/vnet/CMakeFiles/vnet_skx.dir/ip
ip4_forward.c.o ip4_punt_drop.c.o ip6_hop_by_hop.c.o ip6_punt_drop.c.o ip_path_mtu_node.c.o reass
ip4_input.c.o ip6_forward.c.o ip6_input.c.o ip_in_out_acl.c.o punt_node.c.o
反编译,可看到如下的三个函数,分别为基础node函数ip4_lookup_node_fn,和skx以及hsw结构版本的函数。
0000000000002ad0 <ip4_lookup_node_fn>:
ip4_lookup_node_fn():
2ad0: 41 57 push %r15
000000000000e5d0 <ip4_lookup_node_fn_skx>:
ip4_lookup_node_fn_skx():
e5d0: 55 push %rbp
000000000000f010 <ip4_lookup_node_fn_hsw>:
ip4_lookup_node_fn_hsw():
f010: 55 push %rbp
NODE节点宏VLIB_NODE_FN
之后用到的两个宏CLIB_MARCH_SFX和CLIB_MULTIARCH_FN等价。为node处理函数增加架构名称后缀,例如通过指定gcc选项-DCLIB_MARCH_VARIANT=hsw,node处理函数增加hsw后缀。
#ifdef CLIB_MARCH_VARIANT
#define __CLIB_MULTIARCH_FN(a,b) a##_##b
#define _CLIB_MULTIARCH_FN(a,b) __CLIB_MULTIARCH_FN(a,b)
#define CLIB_MULTIARCH_FN(fn) _CLIB_MULTIARCH_FN(fn,CLIB_MARCH_VARIANT)
#else
#define CLIB_MULTIARCH_FN(fn) fn
#endif
#define CLIB_MARCH_SFX CLIB_MULTIARCH_FN
节点函数定义宏VLIB_NODE_FN,node节点函数名称首先增加的是_fn字符的后缀,其次是以上的架构后缀(hsw)。__clib_constructor函数为编译器修饰符,指明此为构造函数,其在main函数之前运行。
对于hsw架构,定义了vlib_node_fn_registration_t结构: ( n o d e n a m e ) h s w 。定义了构造函数: (node name)_hsw。定义了构造函数: (nodename)hsw。定义了构造函数:(node name)_multiarch_register_hsw,注册到全局链表。
#define VLIB_NODE_FN(node) \
uword CLIB_MARCH_SFX (node##_fn) (); \
static vlib_node_fn_registration_t CLIB_MARCH_SFX (node##_fn_registration) = { \
.function = &CLIB_MARCH_SFX (node##_fn), \
}; \
static void __clib_constructor CLIB_MARCH_SFX (node##_multiarch_register) (void) \
{ \
extern vlib_node_registration_t node; \
vlib_node_fn_registration_t *r; \
r = &CLIB_MARCH_SFX (node##_fn_registration); \
r->march_variant = CLIB_MARCH_SFX (CLIB_MARCH_VARIANT_TYPE); \
r->next_registration = node.node_fn_registrations; \
node.node_fn_registrations = r; \
} \
uword CLIB_MARCH_SFX (node##_fn)
对于x86_64平台,其中march_variant可能得取值有以下几个:
#if defined(__x86_64__)
#define foreach_march_variant \
_ (hsw, "Intel Haswell") \
_ (trm, "Intel Tremont") \
_ (skx, "Intel Skylake (server) / Cascade Lake") \
_ (icl, "Intel Ice Lake")
#else
#define foreach_march_variant
#endif
typedef enum
{
CLIB_MARCH_VARIANT_TYPE = 0,
#define _(s, n) CLIB_MARCH_VARIANT_TYPE_##s,
foreach_march_variant
#undef _
CLIB_MARCH_TYPE_N_VARIANTS
} clib_march_variant_type_t;
选择节点架构函数
函数vlib_node_get_preferred_node_fn_variant选择优先级最高的架构函数,priority值越大优先级越高。
vlib_node_function_t *
vlib_node_get_preferred_node_fn_variant (vlib_main_t *vm, vlib_node_fn_registration_t *regs)
{
vlib_node_main_t *nm = &vm->node_main;
vlib_node_fn_registration_t *r;
vlib_node_fn_variant_t *v;
vlib_node_function_t *fn = 0;
...
r = regs;
while (r) {
v = vec_elt_at_index (nm->variants, r->march_variant);
if (v->priority > priority) {
priority = v->priority;
fn = r->function;
}
r = r->next_registration;
}
不同架构对应的优先级如下:
static inline int clib_cpu_march_priority_icl ()
{
if (clib_cpu_supports_avx512_bitalg ())
return 200;
return -1;
}
static inline int clib_cpu_march_priority_skx ()
{
if (clib_cpu_supports_avx512f ())
return 100;
return -1;
}
static inline int clib_cpu_march_priority_hsw ()
{
if (clib_cpu_supports_avx2 ())
return 50;
return -1;
}
CLIB_MARCH_FN宏
与以上VLIB_NODE_FN类似,使用CLIB_MARCH_SFX定义多架构函数。不同之处在于使用CLIB_MARCH_FN_SELECT来选择运行时使用的函数。
#define CLIB_MARCH_FN(fn, rtype, _args...) \
static rtype CLIB_MARCH_SFX (fn##_ma) (_args); \
extern rtype (*fn##_selected) (_args); \
extern int fn##_selected_priority; \
CLIB_MARCH_FN_CONSTRUCTOR (fn) \
static rtype CLIB_MARCH_SFX (fn##_ma) (_args)
#define CLIB_MARCH_FN_SELECT(fn) (* fn ## _selected)
如下fn ## _selected为最终选择的函数,fn ## _selected_priority为优先级数值。每个注册的架构都执行优先级判断,选出最优的函数。
#define CLIB_MARCH_FN_CONSTRUCTOR(fn) \
static void __clib_constructor \
CLIB_MARCH_SFX(fn ## _march_constructor) (void) \
{ \
if (CLIB_MARCH_FN_PRIORITY() > fn ## _selected_priority) \
{ \
fn ## _selected = & CLIB_MARCH_SFX (fn ## _ma); \
fn ## _selected_priority = CLIB_MARCH_FN_PRIORITY(); \
} \
} \