余弦相似度使用NEON指令加速

背景

  • 余弦相似度是通过计算两个向量的夹角余弦值来评估他们的相似度,原理非常简单,应用空间却非常广阔,如人脸特征求相似度,还有NLP领域求文本相似度等等.
  • 余弦相似计算在一般cpu上计算量其实并不大,但是如若人脸特征底库达到一定规模时,在求取最高相似度时速度问题就凸显出来了,特别是在ARM这样计算量十分有限的平台.所以很有必要对余弦相似计算进行优化加速.

参考资料

公式

在这里插入图片描述

代码实现(未用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.89214.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
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值