简介
xsimd
是由c++
语言编写的,支持跨平台,跨CPU
架构的向量化计算库。它封装了CPU
提供的simd
向量化计算函数,屏蔽了底层细节,使得用户很方便的使用simd
特性,提升计算性能。
Define 语句
c++
的define
语句有两种形式,如下所示
1
2
3
#define PI 3.1415
#define XSIMD_HPP
第一种方式用于定义常量,第二种方式用于后面的if
判断。
1
2
3
4
5
6
7
#ifndef XSIMD_HPP
#define XSIMD_HPP
......
#endif
指令集预定义
编译器会自动设置一些预定义变量,表示机器是否支持对应的指令集,比如__SSE2__
变量表示机器是否支持SSE2
指令集。
config/xsmid_config.hpp
文件,根据是否支持指令集,定义了很多变量。如下所示,如果机器支持SSE2
指令集,那么XSIMD_WITH_SSE2
变量的值为 1。同理也定义了XSIMD_WITH_SSE3
变量。
1
2
3
4
5
6
7
8
9
10
11
#ifdef __SSE2__
#define XSIMD_WITH_SSE2 1
#else
#define XSIMD_WITH_SSE2 0
#endif
#ifdef __SSE3__
#define XSIMD_WITH_SSE3 1
#else
#define XSIMD_WITH_SSE3 0
#endif
这些变量会被用来控制向量化计算的实现。xsimd
使用batch
类表示处理的数据,支持常见的各种运算,这些运算的实现都是定义在namespace kernel
。对于每个指令集,都有对应的hpp
文件实现了这些运算。在arch/xsimd_isa.hpp
文件中,会根据这些上一步生成的变量,选择是否include
对应的hpp
文件。
下面以XSIMD_WITH_SSE2
为例,如果为 1, 才会include xsimd_sse2.hpp
。
1
2
3
#if XSIMD_WITH_SSE2
#include "./xsimd_sse2.hpp"
#endif
继续看看xsimd_sse2.hpp
文件的方法声明,里面定义了add
方法。__mm_add_ps
是操作系统提供的方法,用于向量化计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace xsimd
{
namespace kernel {
// + 操作
template < class A >
inline batch < double , A > add ( batch < double , A > const & self , batch < double , A > const & other , requires_arch < sse2 > )
{
return _mm_add_ps ( self , other );
}
}
}
指令集选择
arch/xsimd_scala.hpp
文件定义了各种数据类型的常见计算方法
arch/xsimd_arch.hpp
文件定义了常见机器架构支持的指令集集合
1
2
3
4
using all_x86_architectures = arch_list < avx512bw , avx512dq , avx512cd , avx512f , fma3 < avx2 > , avx2 , fma3 < avx > , avx , fma4 , fma3 < sse4_2 > , sse4_2 , sse4_1 , /*sse4a,*/ ssse3 , sse3 , sse2 > ;
using all_sve_architectures = arch_list < detail :: sve < 512 > , detail :: sve < 256 > , detail :: sve < 128 >> ;
using all_arm_architectures = typename detail :: join < all_sve_architectures , arch_list < neon64 , neon >>:: type ;
using all_architectures = typename detail :: join < all_arm_architectures , all_x86_architectures >:: type ;
以all_x86_architectures
为例,它表示x86
架构能支持的指令集列表。并且这些列表是按照优先级排列的。当我们没有指定使用哪种指令集来执行,xsimd
库会帮我们自动选择优先级高的。
arch/xsimd_arch.hpp
的源码比较复杂,很多采用了template
语法。以arch_list
为例,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template < class ... Archs >
struct arch_list {
template < class Arch >
using add = arch_list < Archs ..., Arch > ;
template < class Arch >
static constexpr bool contains () noexcept
{
return detail :: contains < Arch , Archs ... >:: value ;
}
using best = typename detail :: head < Archs ... >:: type ;
}
struct arch_list
它没有定义任何成员,只使用了template
变量Archs
存储了指令集列表,
add
方法也是增加了指定的指令集,传参是通过template
变量Arch
。然后创建了新的arch_list
实例。
contains
方法用来判断是否包含指定的指令集,传参是通过template
变量Arch
。
best
方法会返回合适的指令集。它调用了head struct
的方法,会返回第一个。
指令集使用
指令集的使用过程如下:
_mm_loadu_ps
,从内存中加载数据到寄存器
_mm_add_ps
,执行计算
_mm_store_ps
,将结果存储到内存
下面是xsimd
官网的例子,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "xsimd/xsimd.hpp"
#include <cstddef>
#include <vector>
void mean ( const std :: vector < double >& a , const std :: vector < double >& b , std :: vector < double >& res )
{
using b_type = xsimd :: batch < double , xsimd :: avx > ;
std :: size_t inc = b_type :: size ;
std :: size_t size = res . size ();
// size for which the vectorization is possible
std :: size_t vec_size = size - size % inc ;
for ( std :: size_t i = 0 ; i < vec_size ; i += inc )
{
b_type avec = b_type :: load_unaligned ( & a [ i ]);
b_type bvec = b_type :: load_unaligned ( & b [ i ]);
b_type rvec = ( avec + bvec ) / 2 ;
rvec . store_unaligned ( & res [ i ]);
}
// Remaining part that cannot be vectorize
for ( std :: size_t i = vec_size ; i < size ; ++ i )
{
res [ i ] = ( a [ i ] + b [ i ]) / 2 ;
}
}
首先调用batch::load_unaligned
方法,将数据加载到寄存器。
然后调用batch
的运算操作符,c++
已经重载运算符了,底层仍然调用了_mm_add_ps
和_mm_div_ps
方法。
最后调用batch::store_unaligned
方法,将结果存储到内存res
变量里。