返回> 网站首页 

AVX2 - 使用 SIMD 加速最近邻查找

yoours2026-10-12 20:25:00 阅读 71

简介一边听听音乐,一边写写文章。

一、介绍

    SIMD(Single Instruction Multiple Data):一条指令处理多个数据。

    AVX2:Intel/AMD 的 256 位 SIMD 指令集,一个 __m256 寄存器可以放 8 个 float 或 8 个 int32_t。

    使用cpu指令并行计算8个数。


二、AoS 与 SoA

1. 数据是 AoS(Array of Structures) 布局:

内存中:lat0, lon0, lat1, lon1, lat2, lon2, ...


2. AVX2 的加减乘除是按位置逐元素运算的,它希望数据是 SoA(Structure of Arrays):

向量 A: [lat0, lat1, lat2, lat3, lat4, lat5, lat6, lat7]

向量 B: [lon0, lon1, lon2, lon3, lon4, lon5, lon6, lon7]

所以我们需要把交错的 lat/lon 去交错(de-interleave),拆成两个纯向量。这是整个优化中最烧脑的部分。


三、示例解析

    实时gps经纬度与指定gps数组最近距离求解:

int find_nearest_index_avx2(const GeoPoint* points, size_t count,

                            float t_lat, float t_lon, float* out_min_dist) {

    // 空数组边界处理

    if (count == 0) {

        if (out_min_dist) *out_min_dist = FLT_MAX;

        return -1;

    }


    // ========== 1. 预计算经纬度到米的换算系数 ==========

    // 纬度方向:1 度纬度 ≈ 111000 米

    const float LAT_TO_M = 111000.0f;

    // 经度方向:1 度经度 ≈ 111000 * cos(纬度) 米

    const float LON_TO_M = 111000.0f * cosf(t_lat * 3.141592653589793f / 180.0f);


    // 广播成 8 份,变成 AVX2 向量

    const __m256 v_lat_m = _mm256_set1_ps(LAT_TO_M);

    const __m256 v_lon_m = _mm256_set1_ps(LON_TO_M);


    // ========== 2. 目标点广播 ==========

    __m256 v_t_lat = _mm256_set1_ps(t_lat);

    __m256 v_t_lon = _mm256_set1_ps(t_lon);


    // ========== 3. 初始化最小值追踪器 ==========

    // v_min_dist:8 个通道各自记录“到目前为止见过的最小距离平方”

    __m256 v_min_dist = _mm256_set1_ps(FLT_MAX);


    // v_min_idx:8 个通道各自记录“最小距离对应的全局下标”

    __m256i v_min_idx = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);


    // v_base_idx:当前这批 8 个点的起始全局下标,初始为 0

    __m256i v_base_idx = _mm256_setzero_si256();


    // v_eight:常量 8 的向量,用于每轮结束后把基准下标 +8

    const __m256i v_eight = _mm256_set1_epi32(8);


    // deint:AoS→SoA 去交错索引表

    const __m256i deint = _mm256_setr_epi32(0, 1, 4, 5, 2, 3, 6, 7);


    // ========== 4. 主循环:每次处理 8 个点 ==========

    size_t i = 0;

    for (; i + 7 < count; i += 8) {

        /*

         * points 内存布局:

         *   lat0,lon0, lat1,lon1, lat2,lon2, lat3,lon3,

         *   lat4,lon4, lat5,lon5, lat6,lon6, lat7,lon7, ...

         */


        // ---------- 4.1 加载 8 个点到 4 个 128 位寄存器 ----------

        __m128 lo1 = _mm_loadu_ps((const float*)&points[i]);        // lat0, lon0, lat1, lon1

        __m128 hi1 = _mm_loadu_ps((const float*)&points[i + 2]);    // lat2, lon2, lat3, lon3

        __m128 lo2 = _mm_loadu_ps((const float*)&points[i + 4]);    // lat4, lon4, lat5, lon5

        __m128 hi2 = _mm_loadu_ps((const float*)&points[i + 6]);    // lat6, lon6, lat7, lon7


        // ---------- 4.2 拼接成 2 个 256 位向量 ----------

        __m256 v_chunk1 = _mm256_insertf128_ps(_mm256_castps128_ps256(lo1), hi1, 1);

        // 结果:lat0, lon0, lat1, lon1 | lat2, lon2, lat3, lon3

        __m256 v_chunk2 = _mm256_insertf128_ps(_mm256_castps128_ps256(lo2), hi2, 1);

        // 结果:lat4, lon4, lat5, lon5 | lat6, lon6, lat7, lon7


        // ---------- 4.3 AoS → SoA 去交错(最烧脑的一步) ----------

        // _MM_SHUFFLE(z,y,x,w) 编码:结果 = [a[w], a[x], b[y], b[z]]

        __m256 t1 = _mm256_shuffle_ps(v_chunk1, v_chunk2, _MM_SHUFFLE(2, 0, 2, 0));

        // t1 = [lat0,lat1,lat4,lat5, lat2,lat3,lat6,lat7]

        __m256 t2 = _mm256_shuffle_ps(v_chunk1, v_chunk2, _MM_SHUFFLE(3, 1, 3, 1));

        // t2 = [lon0,lon1,lon4,lon5, lon2,lon3,lon6,lon7]


        // 用 permutevar8x32_ps 按 deint 索引重排,得到最终 SoA

        __m256 v_lats = _mm256_permutevar8x32_ps(t1, deint);

        // v_lats = [lat0,lat1,lat2,lat3,lat4,lat5,lat6,lat7]

        __m256 v_lons = _mm256_permutevar8x32_ps(t2, deint);

        // v_lons = [lon0,lon1,lon2,lon3,lon4,lon5,lon6,lon7]


        // ---------- 4.4 计算 8 个点的距离平方 ----------

        __m256 d_lat = _mm256_mul_ps(_mm256_sub_ps(v_lats, v_t_lat), v_lat_m);

        __m256 d_lon = _mm256_mul_ps(_mm256_sub_ps(v_lons, v_t_lon), v_lon_m);

        __m256 dist_sq = _mm256_add_ps(_mm256_mul_ps(d_lat, d_lat),

                                       _mm256_mul_ps(d_lon, d_lon));


        // ---------- 4.5 更新最小距离和对应索引(无分支) ----------

        // 比较:新距离是否比当前最小距离更小

        __m256 cmp_mask = _mm256_cmp_ps(v_min_dist, dist_sq, _CMP_GT_OQ);

        // cmp_mask 每个通道:真 = 0xFFFFFFFF,假 = 0x00000000


        // 更新最小距离:掩码为真 → 取新距离,否则取旧距离

        v_min_dist = _mm256_blendv_ps(v_min_dist, dist_sq, cmp_mask);


        // 更新索引:整数没有 blendv,用位运算实现同样的选择

        __m256i mask_i = _mm256_castps_si256(cmp_mask); // 按位重新解释为整数向量

        __m256i v_cur_idx = _mm256_add_epi32(v_base_idx,

                              _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7));

        v_min_idx = _mm256_or_si256(

                        _mm256_and_si256(mask_i, v_cur_idx),      // 掩码为真 → 新下标

                        _mm256_andnot_si256(mask_i, v_min_idx));  // 掩码为假 → 旧下标


        // 基准下标 +8,准备下一轮

        v_base_idx = _mm256_add_epi32(v_base_idx, v_eight);

    }


    // ========== 5. 水平归约:从 8 个通道中找出最终答案 ==========

    ALIGN32 float min_dists[8];

    ALIGN32 int32_t min_idxs[8];

    _mm256_store_ps(min_dists, v_min_dist);

    _mm256_store_si256((__m256i*)min_idxs, v_min_idx);


    float final_min = min_dists[0];

    int final_idx = min_idxs[0];

    for (int k = 1; k < 8; k++) {

        if (min_dists[k] < final_min) {

            final_min = min_dists[k];

            final_idx = min_idxs[k];

        }

    }


    // ========== 6. 处理尾部不足 8 个的点 ==========

    for (; i < count; i++) {

        float dlat = (points[i].lat - t_lat) * LAT_TO_M;

        float dlon = (points[i].lon - t_lon) * LON_TO_M;

        float dist_sq = dlat * dlat + dlon * dlon;

        if (dist_sq < final_min) {

            final_min = dist_sq;

            final_idx = (int)i;

        }

    }


    // ========== 7. 输出结果 ==========

    if (out_min_dist) *out_min_dist = sqrtf(final_min);

    return final_idx;

}


四、函数详解

1. _mm256_sub_ps(a, b)

逐通道做 a - b,一次算 8 个 float 减法。

a = [10,20,30,40,50,60,70,80]

b = [ 1, 2, 3, 4, 5, 6, 7, 8]

c = _mm256_sub_ps(a, b); // [9,18,27,36,45,54,63,72]


2. _mm256_add_epi32(a, b)

逐通道做 32 位整数加法,一次算 8 个 int32_t。

a = [1,2,3,4,5,6,7,8]

b = [10,20,30,40,50,60,70,80]

c = _mm256_add_epi32(a, b); // [11,22,33,44,55,66,77,88]


3. _mm256_setr_epi32(0,1,2,3,4,5,6,7)

创建一个整数向量,参数从低到高填入。setr 的 r 表示 reversed,参数顺序符合直觉(低→高)。对应的 _mm256_set_epi32 则是从高到低填,容易搞混。


4. _mm256_castps_si256(x)

按位重新解释,把 __m256(8 个 float)看成 __m256i(8 个 int32),二进制内容不变,不是数值转换。零开销。


5. 无分支索引更新

v_min_idx = (mask & v_cur_idx) | (~mask & v_min_idx);

mask 全 1 → 取 v_cur_idx(更新)

mask 全 0 → 取 v_min_idx(保留)

等价于 if (mask) cur = new; else cur = old;,但一次处理 8 个通道,且没有分支。


五、示例总结

        分为两部分

        1. 原始数据内存结构

            lat0,lon0,lat1,lon1,lat2,lon2,lat3,lon3,lat4,lon4,lat5,lon5,lat6,lon6,lat7,lon7,

        转换两个通道

        0通道

            lat0,lat1,lat2,lat3, lat4,lat5,lat6,lat7

        1通道

            lon0,lon1,lon2,lon3, lon4,lon5,lon6,lon7


        2. 数值比较

            保留通道每个数据最小值,保留通道每个数据的最小值的索引

            循环与对应位置的历史最小值对比,同时保留最小值索引


        3. 常规方法取出最小值和索引


微信小程序扫码登陆

文章评论

71人参与,0条评论