问题定义
设线段 由顶点 和顶点 组成,记为 。
设线段集 包含 条线段:
目标:线段 与线段集 中的每条线段求交,得到以下信息:
- 交点坐标数组:
- 直线相交判定数组:,为 表示 所在直线与 所在直线存在交点,且交点落在两条线段上
- 线段重叠判定数组:,为 表示 与 共线且有重叠部分
- 线段集大小
本文从纯标量实现出发,逐步引入 AVX2(256-bit,一次处理 4 个 double)和 AVX-512F(512-bit,一次处理 8 个 double)指令进行矢量化优化,并介绍两项关键优化——无除法相交判定和重叠计算跳过——最终在 AMD Zen4 平台(WSL2 环境)上完成性能验证。
数学基础
直线参数方程
线段上的任意一点可表示为参数方程。对于线段 :
其中 ,,参数 。
同理,线段 上的任意一点:
其中 ,,参数 。
求交点参数
联立两参数方程:
用 Cramer 法则求解。行列式:
当 时两条直线不平行,存在唯一交点。参数 的分子:
参数 的分子:
则 ,。
相交与重叠判定
- 直线相交:
- 线段重叠:(两直线平行),且至少一个端点落在对方线段上
其中"点是否在线段上"的判定函数 contain:已知线段起点和方向向量,通过反解参数 ,检查 且两个坐标方向解出的 一致。
关键洞察:无除法判定
这是本文最核心的优化。,要判定 ,可以完全避免除法:
同理适用于 。这一变换将除法(延迟 ~14–20 CPU 周期)替换为比较(延迟 ~3 周期),对于绝大多数不相交的线段对(实际测试中 ),完全消除了除法的开销。
完整代码
头文件与辅助函数
1 |
|
串行版本(含无除法判定优化)
1 | // ===================================================================== |
AVX2 版本(4-wide SIMD,含无除法判定优化)
AVX2 使用 256-bit 的 YMM 寄存器,每个寄存器可容纳 4 个双精度浮点数(__m256d)。循环步长由标量的 i++ 变为 i += 4,每次迭代同时处理 4 条线段的求交。
1 | // ===================================================================== |
AVX-512F 版本(8-wide SIMD,含无除法判定优化)
AVX-512F 是 Intel 在 2013 年引入的 512-bit SIMD 指令集。相比 AVX2,它的关键改进包括:
- 512-bit ZMM 寄存器:每寄存器容纳 8 个
double(__m512d) - 掩码寄存器
__mmask8:比较指令直接返回 8-bit 掩码,用位运算组合,比向量掩码更高效 - 32 个向量寄存器(AVX2 仅 16 个),减少寄存器溢出压力
注意:AMD Zen4 架构通过"双泵"(double-pumping)两个 256-bit 单元实现 AVX-512F,吞吐量与 AVX2 相近。但掩码优化和循环次数减半仍带来约 19% 的额外提升。Intel 原生 512-bit 实现可获得更高收益。
1 | const __m512d zero512 = _mm512_set1_pd(0); |
测试框架
1 | // =========================== |
数据生成脚本
使用 Python 生成两个"多边形"的顶点数据:
- s.txt(较大,20000 顶点):参数方程 的螺旋线,确保与 c.txt 产生丰富交点
- c.txt(较小,5000 顶点):锯齿形折线 ,从左到右横向穿过螺旋线
1 | import math |
将脚本保存为 gen_data.py,运行前先创建目录:
1 | mkdir -p poly |
编译与运行
编译环境要求
- 编译器:GCC 9+ 或 Clang 10+(需支持 AVX2 和 AVX-512F intrinsics)
- CPU:Intel Haswell+ (AVX2) / Skylake-X+ (AVX-512F),或 AMD Zen4+ (AVX2 + AVX-512F)
- 操作系统:Linux / WSL2
编译命令
1 | # 编译(需同时开启 AVX2 和 AVX-512F) |
运行
1 | # 完整测试(功能验证 + 性能基准) |
性能结果与分析
测试环境
| 项目 | 配置 |
|---|---|
| CPU | AMD Ryzen 7 7840HS (Zen4, 8C/16T, 3.8GHz 基频) |
| L1 Cache | 256KB (data) + 256KB (instruction) per core |
| L2 Cache | 1MB per core (8MB total) |
| L3 Cache | 16MB (shared) |
| 内存 | LPDDR5-6400 |
| 环境 | WSL2 (Hyper-V), GCC 11.4 |
注意:AMD Zen4 的 AVX-512F 通过双泵(double-pumping)两个 256-bit 单元实现,吞吐量 AVX2。在 Intel 原生 512-bit CPU 上,AVX-512F 的加速比预期更高。
测试结果
1. 内存批量测试(40,000 线段 50 次 200 万次求交)
在此测试中,所有 线段与 平行(),必须执行 overlap 计算。数据完全在 L2 缓存内(~640KB)。
| 方法 | 耗时 | 加速比 (vs seq) | 加速比 (vs avx2) |
|---|---|---|---|
| Sequential | 0.0165s | (基线) | — |
| AVX2 (4-wide) | 0.0045s | — | |
| AVX-512F (8-wide) | 0.0041s |
2. 多边形求交测试( 亿对)
使用螺旋线与折线数据进行真实多边形求交。交点数量 3,780 个,相交率仅 —— 的除法被无除法判定优化消除。
| 方法 | 耗时 | 加速比 (vs seq) | 加速比 (vs avx2) |
|---|---|---|---|
| Sequential | 0.2211s | (基线) | — |
| AVX2 (4-wide) | 0.1914s | — | |
| AVX-512F (8-wide) | 0.1550s |
为什么加速比达不到 / ?
这是计算特征决定的硬件限制,与实现质量无关。
1. 计算密集度极低(内存瓶颈)
每次 SIMD 迭代加载 个数组() 或 个 double,运算仅 条向量指令。算术强度(FLOPS/Byte)极低,瓶颈在内存带宽而非计算单元。SIMD 增加计算宽度但无法加速数据加载。
2. 标量乱序执行有天然优势
各线段对之间完全独立,无数据依赖。现代 CPU 的乱序执行窗口(+ op)可以同时执行多个线段对的标量计算,部分重叠延迟。SIMD 将元素"捆绑"在一起,反而限制了调度灵活性。
3. 多边形测试的额外限制
- 数据量 MB(L2 缓存边界),缓存未命中比内存测试多
- 无除法判定消除了除法瓶颈,但增加的比较指令使总指令数变多
- 不相交意味着大部分计算在"快速路径"(无除法)上,瓶颈转移到乘加和比较
4. AMD Zen4 的 AVX-512 实现
Zen4 用两个 256-bit 单元拼接执行 512-bit 指令,吞吐量与 AVX2 同。AVX-512F 的 提升主要来自:掩码寄存器(减少向量 标量转换)、循环次数减半(更少的分支和迭代开销)、32 个寄存器(减轻溢出压力)。
在 Intel 原生 512-bit CPU(如 Xeon Platinum)上,AVX-512F 预期可达 于 AVX2。
WSL2 影响
WSL2 环境下 clock() 计时存在显著波动(同条件下最大偏差 ),原因是 Hyper-V 虚拟化下 vCPU 由 Windows 宿主导调度。对纯计算吞吐量影响较小(),但影响了计时精度。建议在裸机 Linux 上做更精确的 benchmark。
关键优化总结
| 优化项 | 原理 | 效果 |
|---|---|---|
| 无除法判定 | 消除 的除法 | |
| 重叠计算跳过 | _mm256_movemask_pd 检查 ,全非零则跳过 |
多边形数据中几乎永不触发 |
| 交点坐标延迟计算 | 只有 intersect true 时才做除法和坐标计算 | 除法从 对 对 |
| 64 字节对齐 | _mm_malloc(..., 64) |
满足 AVX-512F 对齐要求 |
| 数组填充到 的倍数 | 分配时 pad 到 的倍数 | 避免 SIMD 越界读取 |
总结
本文以线段组求交为切入点,展示了从纯标量 AVX2 AVX-512F 的 SIMD 优化全流程。两项关键优化——无除法相交判定和重叠计算跳过——将除法从每条线段对的必执行路径上消除,使得在真实多边形数据上的加速比从最初的负值(SIMD 反而更慢)扭转为 AVX2 、AVX-512F 。
加速比未达 / 的根本原因在于该算法的算术强度太低(数据加载开销与计算量相当),这是硬件限制,并非实现不足。在 的"重计算"场景下,AVX2 可获得 加速,AVX-512F 可获得 加速,验证了 SIMD 对计算密集型代码的巨大价值。
如需进一步提升多边形求交的整体吞吐量,方向在于增大单次处理的数据量(将多个源线段一起 SIMD 处理)以提升计算密度,或使用空间索引(如 R-tree)减少需要求交的线段对数量。


