xsimd

简介

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变量Archbest方法会返回合适的指令集。它调用了head struct的方法,会返回第一个。

指令集使用

指令集的使用过程如下:

  1. _mm_loadu_ps,从内存中加载数据到寄存器
  2. _mm_add_ps,执行计算
  3. _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变量里。

updatedupdated2023-07-232023-07-23