手写 SIMD 距离内核前,先验证精度、尾维与基准公平性
写距离内核最容易绕过的一步,是先确认它算的是哪个距离。平方 L2 不开方,内积越大越相似,余弦还要求处理范数;三者不能共用一个“分数越小越好”的接口而不写契约。
Faiss 的度量说明明确:它返回平方 L2;归一化后的向量,平方 L2 与内积满足 d²=2−2·dot。这个数学关系可以用来核对排序,不能拿来保证哪个实现更快。
概念示意图,非控制台截图、跑分或真实事件记录。
先把尾巴算进去
下面的 C 程序保留两条路径。编译目标明确支持 AVX2/FMA 时,每轮处理32个float;其他目标使用四个标量累加器。最后的尾部循环不能省:维度769不是768的一个无关紧要的近似。
这不是运行时CPU分派库。在x86上用AVX2编译的二进制,仍要由部署系统保证指令集可用。本文本机是arm64,因此只验证回退路径,不能把结果说成“验证了AVX2性能”。
#include <stdio.h> #include <math.h> #include <stddef.h> #if defined(__AVX2__) && defined(__FMA__) #include <immintrin.h> #endif static float distance2(const float *a,const float *b,size_t n){ size_t i=0; float s=0; #if defined(__AVX2__) && defined(__FMA__) __m256 x0=_mm256_setzero_ps(),x1=x0,x2=x0,x3=x0; for(;n-i>=32;i+=32){ __m256 d0=_mm256_sub_ps(_mm256_loadu_ps(a+i),_mm256_loadu_ps(b+i)); __m256 d1=_mm256_sub_ps(_mm256_loadu_ps(a+i+8),_mm256_loadu_ps(b+i+8)); __m256 d2=_mm256_sub_ps(_mm256_loadu_ps(a+i+16),_mm256_loadu_ps(b+i+16)); __m256 d3=_mm256_sub_ps(_mm256_loadu_ps(a+i+24),_mm256_loadu_ps(b+i+24)); x0=_mm256_fmadd_ps(d0,d0,x0);x1=_mm256_fmadd_ps(d1,d1,x1); x2=_mm256_fmadd_ps(d2,d2,x2);x3=_mm256_fmadd_ps(d3,d3,x3); } float lanes[8];_mm256_storeu_ps(lanes,_mm256_add_ps(_mm256_add_ps(x0,x1),_mm256_add_ps(x2,x3))); for(int j=0;j<8;j++)s+=lanes[j]; #else float sums[4]={0}; for(;n-i>=4;i+=4)for(int j=0;j<4;j++){float d=a[i+j]-b[i+j];sums[j]+=d*d;} for(int j=0;j<4;j++)s+=sums[j]; #endif for(;i<n;i++){float d=a[i]-b[i];s+=d*d;} return s; } int main(void){ float a[769],b[769];size_t dims[]={0,1,3,4,31,32,33,767,768,769}; for(int i=0;i<769;i++){a[i]=(i%17-8)*0.125f;b[i]=(i%11-5)*0.25f;} for(size_t c=0;c<sizeof(dims)/sizeof(dims[0]);c++){ size_t n=dims[c];double ref=0; for(size_t j=0;j<n;j++){double d=(double)a[j]-b[j];ref+=d*d;} if(fabs(distance2(a,b,n)-ref)>1e-4*(1+ref))return 1; } puts("PASS 10 dimensions; correctness only");return 0; }一份跑分至少要公平到什么程度
先用相同编译器、优化级别和输入检查标量、自动向量化、手写intrinsics。不能给标量用-O0,给向量版用-O3,然后庆祝“硬件提速”。还要检查结果是否被使用,防止整段计算被编译器删除。
第二步才是工作集。重复读取两个小数组,主要看到热缓存里的算术;随机读取远超缓存的向量集合,访存可能占主导。10月21日ClickHouse的Vera CPU实验在Memory表上考察线程扩展,并解释内存带宽限制。它是另一套硬件和负载,不能替本段AVX2代码提供成绩。
FMA通常只有一次最终舍入,但不意味着整段累加没有误差,更不意味着在所有CPU上“单周期完成”。对齐也不是一个二值开关:未对齐指令、实际地址、跨缓存行和跨页是不同条件,应分别测。
本地核验与尚未测的部分
本轮在macOS arm64、系统Clang上编译运行上面的完整程序,验证十种维度与double参考结果;没有记录耗时作为性能结论。AVX2/FMA分支、运行时分派、NaN/Inf策略和真实检索工作集仍未实测。想写吞吐倍数,得等这些条件、命令和原始时序齐了再写。数字可以先空着,尾维不能。
封面表达一般距离计算,带有开方符号;本文代码实现平方L2,排序不需要开方。内图按本文的平方和路径绘制。
本文的本地检查仅覆盖明确列出的合成示例;外部研究与产品文档不作为本人性能实测。
复现命令:将完整代码保存为 distance.c,运行 cc -std=c11 -O2 -Wall -Wextra -Werror distance.c -o distance && ./distance。核验环境:Apple Clang 21.0.0、macOS 27.0.1 arm64;输出 PASS 10 dimensions; correctness only。