背景
- 余弦相似度是通过计算两个向量的夹角余弦值来评估他们的相似度,原理非常简单,应用空间却非常广阔,如人脸特征求相似度,还有NLP领域求文本相似度等等.
- 余弦相似计算在一般cpu上计算量其实并不大,但是如若人脸特征底库达到一定规模时,在求取最高相似度时速度问题就凸显出来了,特别是在ARM这样计算量十分有限的平台.所以很有必要对余弦相似计算进行优化加速.
参考资料
公式
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210527170051529.png#pic_center)
代码实现(未用NEON)
- 下面这段代码应该很好理解, a_data、b_data是输入两个N维的特征向量, score是a_data、b_data两个特征向量的相似度输出.注意 a_data、b_data维度要相等
- 代码中 ab_mult_add:是上面公式的分母
- 代码中 sqrt(a_len * b_len):是上面公式的分子
#include <stdio.h>
#include <iostream>
#include <vector>
#include <math.h>
bool cos_similarity(const std::vector<float>& a_data,
const std::vector<float>& b_data,
float & score)
{
const int data_length = a_data.size();
double ab_mult_add = 0.0f;
double a_len = 0.0f;
double b_len = 0.0f;
if((0 != data_length) and (b_data.size() != data_length))
{
return false;
}
//ab_mult_add:分母
//sqrt(a_len * b_len) :分子
for (std::size_t i = 0; i < data_length; i++)
{
ab_mult_add += (a_data[i] * b_data[i]);
a_len += a_data[i] * a_data[i];
b_len += b_data[i] * b_data[i];
}
score = (float) (ab_mult_add / sqrt(a_len * b_len));
return true;
}
使用NEON加速
- 在明白上一节代码以及公式的情况下,下述代码会好理解一点,不过需要学习一下neon的函数的作用
- 需要注意的是: 我使用float32x4_t类型四个数据做一组进行处理,可能会出现a_data、b_data特征维度不能整除,所以我在代码做了处理(代码55-66行)
#include <stdio.h>
#include <iostream>
#include <vector>
#include <math.h>
#include <arm_neon.h>
double neon_cos_similarity(const std::vector<float>& a_data,
const std::vector<float>& b_data,
float & score)
{
const int data_length = a_data.size();
double ab_mult_add = 0.0f;
double a_len = 0.0f;
double b_len = 0.0f;
// float32x4_t 由4个32位的float组成的数据类型,对它做一次操作,4个float都被用到
float32x4_t ab_mult_add_vec = vdupq_n_f32(0);// 存储的四个 float32 都初始化为 0,寄存器ab_mult_add_vec
float32x4_t a_qua_sum_vec = vdupq_n_f32(0);
float32x4_t b_qua_sum_vec = vdupq_n_f32(0);
if((0 != data_length) and (b_data.size() != data_length))
{
return false;
}
float* a_data_ptr = (float*)a_data.data();
float* b_data_ptr = (float*)b_data.data();
for (int i = 0; i < data_length / 4; ++i) //四个数据为一组.或有剩余数据,下文处理
{
float32x4_t a_data_vec = vld1q_f32(a_data_ptr + 4*i);// 加载 data + 4*i 地址起始的 4 个 float 数据到寄存器tmp_vec
float32x4_t b_data_vec = vld1q_f32(b_data_ptr + 4*i);
ab_mult_add_vec += vmulq_f32(a_data_vec, b_data_vec);//点乘 [a0*b0, a1*b1, a2*b2, a3*b3],并累加
a_qua_sum_vec += vmulq_f32(a_data_vec, a_data_vec);//点乘 [a0*a0, a1*a1, a2*a2, a3*a3],并累加,就是平方和
b_qua_sum_vec += vmulq_f32(b_data_vec, b_data_vec);
}
//将累加结果寄存器中的所有元素相加得到最终累加值
ab_mult_add += vgetq_lane_f32(ab_mult_add_vec, 0);
ab_mult_add += vgetq_lane_f32(ab_mult_add_vec, 1);
ab_mult_add += vgetq_lane_f32(ab_mult_add_vec, 2);
ab_mult_add += vgetq_lane_f32(ab_mult_add_vec, 3);
// std::cout << "neon ab_mult_add = " << ab_mult_add << std::endl;
a_len += vgetq_lane_f32(a_qua_sum_vec, 0);
a_len += vgetq_lane_f32(a_qua_sum_vec, 1);
a_len += vgetq_lane_f32(a_qua_sum_vec, 2);
a_len += vgetq_lane_f32(a_qua_sum_vec, 3);
// std::cout << "neon a_len = " << a_len << std::endl;
b_len += vgetq_lane_f32(b_qua_sum_vec, 0);
b_len += vgetq_lane_f32(b_qua_sum_vec, 1);
b_len += vgetq_lane_f32(b_qua_sum_vec, 2);
b_len += vgetq_lane_f32(b_qua_sum_vec, 3);
// std::cout << "neon b_len = " << b_len << std::endl;
int odd = data_length & 3;//数组长度除有4余数
if(0 < odd)
{
//处理剩余数据
// std::cout << "data_length = " << data_length << ", odd = " << odd << std::endl;
for(int i = data_length - odd; i < data_length; i++)
{
ab_mult_add += (a_data[i] * b_data[i]);
a_len += a_data[i] * a_data[i];
b_len += b_data[i] * b_data[i];
}
}
score = (float) (ab_mult_add / sqrt(a_len * b_len));
return true;
}
加速效果
- 我测试使用的特征维度是256
- 加速效果是非常可观的,毕竟这个是一个调用量非常大的函数
加速前(μs) | 加速后(μs) |
---|
58.892 | 14.990 |
编译信息
- 这里给出CMakeLists.txt中比较重要的配置,参考使用
cmake_minimum_required(VERSION 3.1)
project(dtk_neon_test)
add_definitions(-std=c++11)
# - mfloat-abi=soft 不使用 FPU 和 NEON 指令。只使用核心寄存器集。使用库调用模拟所有浮点操作
# - mfloat-abi=softfp 使用与-mfloat-abi = soft 相同的调用约定,但是适当地使用浮点和 NEON 指令。
#使用此选项编译的应用程序可以链接到软浮动库。如果相关的硬件指令可用,那么您可以使用此选项来提高代码的性能,并且仍然使代码符合软浮动环境
# - mfloat-ABI=hard 适当地使用浮点和 NEON 指令,并更改 ABI 调用约定,以生成更高效的函数调用。
#浮点类型和向量类型可以在 NEON 寄存器中的函数之间传递,这大大减少了复制的数量。这也意味着需要在堆栈上传递参数的调用更少
add_definitions(-mfpu=neon)
add_definitions(-mfloat-abi=hard)
##### 交叉编译 ####
SET(CMAKE_SYSTEM_NAME Linux)
#指定交叉编译器路径
set(TOOLSCHAIN_PATH "/home/Installs/gcc-linaro-5.4.1-2017.05-x86_64_arm-linux-gnueabihf")
set(TOOLCHAIN_HOST "${TOOLSCHAIN_PATH}/bin/arm-linux-gnueabihf")
set(TOOLCHAIN_CC "${TOOLCHAIN_HOST}-gcc")
set(TOOLCHAIN_CXX "${TOOLCHAIN_HOST}-g++")
#告诉cmake是进行交叉编译
set(CMAKE_CROSSCOMPILING TRUE)
set(CMAKE_C_COMPILER ${TOOLCHAIN_CC})
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_CXX})
#库和同头文件查找的路径。
set(CMAKE_FIND_ROOT_PATH "${SYSROOT_PATH}" "${CMAKE_PREFIX_PATH}" "${TOOLSCHAIN_PATH}")
# set(THREADS_PTHREAD_ARG /home/Installs/gcc-linaro-5.4.1-2017.05-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf)
# set(THREADS_PTHREAD_ARG "0" CACHE STRING "Result from TRY_RUN" FORCE)
SET(THREADS_PTHREAD_ARG "2" CACHE STRING "Forcibly set by ToolchainFile.cmake." FORCE)
#Check if compiler accepts -pthread - yes
测试使用的arm cpu信息
processor : 0
model name : ARMv7 Processor rev 5 (v7l)
BogoMIPS : 500.00
Features : half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xc07
CPU revision : 5
processor : 1
model name : ARMv7 Processor rev 5 (v7l)
BogoMIPS : 500.00
Features : half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xc07
CPU revision : 5
processor : 2
model name : ARMv7 Processor rev 5 (v7l)
BogoMIPS : 500.00
Features : half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xc07
CPU revision : 5
Hardware : Artosyn Sirius Family
Revision : 0000
Serial : 0000000000000000