2019-09-11 15:04:46 LEILEI18A 阅读数 62
  • 玩转深度学习实战教程

    玩转深度学习视频培训课程,详细讲解深度学习的原理和利用深度学习框架TensorFlow进行项目实战。课程通过Kaggle竞赛平台的Titanic问题讲解TensorFlow的基本用法以及问题处理的常用技巧,讲解深度学习图像领域的卷积神经网络CNN和多个经典的网络架构、CNN的应用,讲解自然语言处理领域的RNN、LSTM以及它们的多种变种结构,构建语言模型和对话机器人,介绍损失函数和优化深度学习算法在TensorFlow中的实现。

    3209 人正在学习 去看看 王而川

                                       如何改进和优化深度学习方法?

1. 学术上

 

2. 工程上

    在训练集训练,再和训练集一起划分的测试集上测试,效果很好,但是实际测试效果很差,需要从多方面进行分析,然后才能改进。

    从 数据集情况/数据预处理方式/网络结构/损失函数/后处理等方面进行分析。

首先,分析数据集应用场景是否接近;是否数据集数量太少,导致过拟合;

然后,分析训练集是否足够多,包含更多的情况(颜色、视角、图片分辨率、图片质量等),包含情况越多,泛化性一般越好些;

然后,逐一分析一些典型图片,对比原始图片,标签对应图片,预测类别对应的图片,从颜色/视角/光亮度等对三者进行分析;

         判断是否为数据集的问题,无论是与不是,都要进一步分析数据预处理方式,要怎么预处理增加一些“情况”,增加亮度等。

 

其次,分析网络结构问题(后续更新这一步骤);

其次,分析后处理问题。

 

 

2018-03-15 17:24:29 u013498583 阅读数 287
  • 玩转深度学习实战教程

    玩转深度学习视频培训课程,详细讲解深度学习的原理和利用深度学习框架TensorFlow进行项目实战。课程通过Kaggle竞赛平台的Titanic问题讲解TensorFlow的基本用法以及问题处理的常用技巧,讲解深度学习图像领域的卷积神经网络CNN和多个经典的网络架构、CNN的应用,讲解自然语言处理领域的RNN、LSTM以及它们的多种变种结构,构建语言模型和对话机器人,介绍损失函数和优化深度学习算法在TensorFlow中的实现。

    3209 人正在学习 去看看 王而川

深度学习最全优化方法总结比较(SGD,Adagrad,Adadelta,Adam,Adamax,Nadam)

转载自:https://zhuanlan.zhihu.com/p/22252270

SGD
此处的SGD指mini-batch gradient descent,关于batch gradient descent, stochastic gradient descent, 以及 mini-batch gradient descent的具体区别就不细说了。现在的SGD一般都指mini-batch gradient descent。

SGD就是每一次迭代计算mini-batch的梯度,然后对参数进行更新,是最常见的优化方法了。即:

gt=θt1f(θt1)

Δθt=ηgt

其中,η是学习率,gt是梯度

SGD完全依赖于当前batch的梯度,所以\eta可理解为允许当前batch的梯度多大程度影响参数更新

缺点:(正因为有这些缺点才让这么多大神发展出了后续的各种算法)

选择合适的learning rate比较困难
- 对所有的参数更新使用同样的learning rate。对于稀疏数据或者特征,有时我们可能想更新快一些对于不经常出现的特征,对于常出现的特征更新慢一些,这时候SGD就不太能满足要求了

SGD容易收敛到局部最优,并且在某些情况下可能被困在鞍点【原来写的是“容易困于鞍点”,经查阅论文发现,其实在合适的初始化和step size的情况下,鞍点的影响并没这么大。感谢@冰橙的指正】

Momentum
momentum是模拟物理里动量的概念,积累之前的动量来替代真正的梯度。公式如下:

mt=μmt1+gt

Δθt=ηmt

其中,μ是动量因子

特点:

下降初期时,使用上一次参数更新,下降方向一致,乘上较大的μ能够进行很好的加速
下降中后期时,在局部最小值来回震荡的时候,gradient0μ使得更新幅度增大,跳出陷阱
在梯度改变方向的时候,μ能够减少更新

总而言之,momentum项能够在相关方向加速SGD,抑制振荡,从而加快收敛

Nesterov
nesterov项在梯度更新时做一个校正,避免前进太快,同时提高灵敏度。
将上一节中的公式展开可得:

Δθt=ημmt1ηgt

可以看出,mt1
并没有直接改变当前梯度gt,所以Nesterov的改进就是让之前的动量直接影响当前的动量。即:

gt=θt1f(θt1ημmt1)

mt=μmt1+gt

Δθt=ηmt

所以,加上nesterov项后,梯度在大的跳跃后,进行计算对当前梯度进行校正。如下图:

这里写图片描述

momentum首先计算一个梯度(短的蓝色向量),然后在加速更新梯度的方向进行一个大的跳跃(长的蓝色向量),nesterov项首先在之前加速的梯度方向进行一个大的跳跃(棕色向量),计算梯度然后进行校正(绿色梯向量)

其实,momentum项和nesterov项都是为了使梯度更新更加灵活,对不同情况有针对性。但是,人工设置一些学习率总还是有些生硬,接下来介绍几种自适应学习率的方法

Adagrad
Adagrad其实是对学习率进行了一个约束。即:

nt=nt1+gt2

Δθt=ηnt+ϵgt

此处,对gt从1到t进行一个递推形成一个约束项regularizer1r=1t(gr)2+ϵϵ用来保证分母非0

特点:

前期gt较小的时候, regularizer较大,能够放大梯度
后期gt较大的时候,regularizer较小,能够约束梯度
适合处理稀疏梯度

缺点:
由公式可以看出,仍依赖于人工设置一个全局学习率
η设置过大的话,会使regularizer过于敏感,对梯度的调节太大
中后期,分母上梯度平方的累加将会越来越大,使gradient\to0,使得训练提前结束

Adadelta
Adadelta是对Adagrad的扩展,最初方案依然是对学习率进行自适应约束,但是进行了计算上的简化。
Adagrad会累加之前所有的梯度平方,而Adadelta只累加固定大小的项,并且也不直接存储这些项,仅仅是近似计算对应的平均值。即:

nt=νnt1+(1ν)gt2

Δθt=ηnt+ϵgt

在此处Adadelta其实还是依赖于全局学习率的,但是作者做了一定处理,经过近似牛顿迭代法之后:

E|g2|t=ρE|g2|t1+(1ρ)gt2

Δxt=r=1t1ΔxrE|g2|t+ϵ

其中,E代表求期望。

此时,可以看出Adadelta已经不用依赖于全局学习率了。

特点:

训练初中期,加速效果不错,很快
训练后期,反复在局部最小值附近抖动

RMSprop
RMSprop可以算作Adadelta的一个特例:

ρ=0.5时,E|g2|t=ρE|g2|t1+(1ρ)gt2就变为了求梯度平方和的平均数。

如果再求根的话,就变成了RMS(均方根):

RMS|g|t=E|g2|t+ϵ
此时,这个RMS就可以作为学习率\eta的一个约束:

Δxt=ηRMS|g|tgt
特点:

其实RMSprop依然依赖于全局学习率
RMSprop算是Adagrad的一种发展,和Adadelta的变体,效果趋于二者之间
适合处理非平稳目标
- 对于RNN效果很好

Adam
Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。公式如下:

mt=μmt1+(1μ)gt

nt=νnt1+(1ν)gt2

mt^=mt1μt

nt^=nt1νt

Δθt=mt^nt^+ϵη

其中,mtnt分别是对梯度的一阶矩估计和二阶矩估计,可以看作对期望E|gt|E|gt2|的估计;mt^nt^是对mtnt的校正,这样可以近似为对期望的无偏估计。
可以看出,直接对梯度的矩估计对内存没有额外的要求,而且可以根据梯度进行动态调整,而mt^nt^+ϵ对学习率形成一个动态约束,而且有明确的范围。

特点:

结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点
对内存需求较小
为不同的参数计算不同的自适应学习率
也适用于大多非凸优化
- 适用于大数据集和高维空间

Adamax
Adamax是Adam的一种变体,此方法对学习率的上限提供了一个更简单的范围。公式上的变化如下:

nt=max(νnt1,|gt|)

Δx=mt^nt+ϵη

可以看出,Adamax学习率的边界范围更简单

Nadam
Nadam类似于带有Nesterov动量项的Adam。公式如下:

gt^=gt1Πi=1tμi

mt=μtmt1+(1μt)gt

mt^=mt1Πi=1t+1μi

nt=νnt1+(1ν)gt2

nt^=nt1νtmt¯=(1μt)gt^+μt+1mt^

Δθt=ηmt¯nt^+ϵ

可以看出,Nadam对学习率有了更强的约束,同时对梯度的更新也有更直接的影响。一般而言,在想使用带动量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果。

经验之谈
对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值
SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下,结果更可靠
如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
Adadelta,RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多。
在想使用带动量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果

2018-05-17 23:47:22 weixin_38208741 阅读数 272
  • 玩转深度学习实战教程

    玩转深度学习视频培训课程,详细讲解深度学习的原理和利用深度学习框架TensorFlow进行项目实战。课程通过Kaggle竞赛平台的Titanic问题讲解TensorFlow的基本用法以及问题处理的常用技巧,讲解深度学习图像领域的卷积神经网络CNN和多个经典的网络架构、CNN的应用,讲解自然语言处理领域的RNN、LSTM以及它们的多种变种结构,构建语言模型和对话机器人,介绍损失函数和优化深度学习算法在TensorFlow中的实现。

    3209 人正在学习 去看看 王而川

如何从系统层面优化深度学习计算?

搜狐科技05-1717:18

编者按:在图像、语音识别、自然语言处理、强化学习等许多技术领域中,深度学习已经被证明是非常有效的,并且在某些问题上已经达到甚至超越了人类的水平。然而,深度学习对于计算能力有着很大的依赖,除了改变模型和算法,是否可以从系统的层面来优化深度学习计算,进而改善计算资源的使用效率?本文中,来自微软亚洲研究院异构计算组资深研究员伍鸣与大家分享他对深度学习计算优化的一些看法。

深度学习在近几年里取得了巨大的进步,它已经或者是有望成功地被应用在我们许多生活场景中,比如自动驾驶、安防、翻译、医疗等等。可以说,计算机的计算和通信能力的大幅提升是促使深度学习成功的重要因素。

  深度学习为什么依赖于超大的计算能力?

首先,深度学习本质上是基于统计的科学,所以大规模的样本数据对于深度学习的效果是至关重要的。其次,更大规模和更复杂的神经网络模型已经被证明非常有效,并在产品中有广泛的使用,这同时也产生了对计算能力的更大要求和消耗。举个例子,具有8层神经元的AlexNet网络2012年在ImageNet数据集上取得16%的错误率,该网络的一次迭代运行大约需要1.4 GFLOP的计算量。而微软提出的使用152层神经元的残差网络(ResNet)于2015年在该数据集上取得3.5%的错误率,其一次迭代的计算量大约是22.6GFLOP,是AlexNet的16倍。在当今的生产环境中,图像、语音以及自然语言处理相关的模型,例如人脸识别、语音转文字、机器翻译等,即使给予相当多的计算资源,很多仍需要几周的时间才能完成训练。

再次,深度学习模型是迅速迭代的。在AI领域,每年学术界和工业界都会提出大量的新模型。对每一个实际的问题,开发者需要不断尝试不同的模型和算法,甚至对于同一种模型算法,也需要去反复调试超参数以获得最好的预测效果。可想而知,如果模型的每次训练都要几周的时间,那么寻找最优模型的过程会非常漫长和痛苦。

另外,模型的线上推理具有更加极致的性能要求。线上的服务具有硬性的服务等级协议(SLA),所以在实际部署大型模型时,需要手工重新优化在深度学习框架(如TensorFlow)上已经训练好的模型,导致大量额外工程开销的产生。

由此可见,进一步优化深度学习计算对于深度学习的快速发展和成功应用起着至关重要的作用。

  深度学习计算优化的挑战和机会

目前,优化深度学习的计算存在以下几个主要的挑战:

1)单机单计算单元(如GPU)的资源限制往往不能满足对大规模数据和模型的处理要求,那么就需要使用多机多计算单元来横向扩展计算的规模。如何才能最大限度地减少通信的开销从而最大化多机的并行度?

2)如何优化神经网络的计算使得它能够把单个硬件计算单元的效率发挥到极致?

3)虽然许多硬件计算单元(GPU、FPGA等)的计算能力很强大,但是它们的内存资源(即设备内存)非常稀缺。当它们不能提供模型运行所需要的内存资源时,要么运算不能够进行下去,要么就需要将计算所需的数据在主存和设备内存之间倒来倒去,带来很大的运行开销。如何才能更好地利用有限的设备内存资源从而不给计算效率带来负面的影响?

4)深度学习开发者和研究人员通常只想关注神经网络模型和算法本身,并不想被复杂的优化问题分散精力。这意味着深度学习框架这样的系统软件最好能够实现自动优化,而对模型开发者透明。那么,如何对特定的优化做合理的抽象使其更加灵活通用、更加容易地集成在系统框架中便是需要认真考虑的问题。

事实上,任何方面的优化问题都可以从模型算法和系统两个角度来看待。一方面,我们可以通过改变模型和算法来优化其对计算资源的使用效率从而改进其运行速度。这样的优化对特定的算法往往非常有效,但却不容易扩展应用到其它算法中。而另一方面,也就是微软亚洲研究院异构计算组正在进行的研究,则是在系统中实施模型算法无关的优化,这样的优化,通常可以为更多的应用带来性能的好处,同时也符合我们在前文提到的透明性的要求。

  以系统优化助力深度学习计算

为了能够更好地理解系统这一层面的优化,我们先来简单介绍一下深度学习框架系统的背景知识。当今工业界流行的深度学习系统(包括TensorFlow、PyTorch、CNTK、MxNet、Caffe等)大都采用分层的体系结构设计。在前端提供高级语言(例如Python)的接口抽象,允许用户方便地描述神经网络结构,也就是深度学习的模型。描述好的模型在被系统运行前,首先会被转换成数据流图(Data-flow Graph)。在这个数据流图中,节点是特定的矩阵操作(也就是Operator,如Sigmoid、Matrix Multiplication等),而连接不同节点的边则是操作节点的输入和输出矩阵。这个数据流图也可以被看成是深度学习计算的中间表达。然后,深度学习系统的后端将这个数据流图映射到实际硬件上进行高效地执行,而大部分系统层面的优化就是在这个阶段完成的。

  加速分布式深度学习训练

分布式训练的主要瓶颈在于多机之间的通信开销。如今计算机网络的硬件技术已经有了很大的发展,InfiniBand的RDMA网卡(Remote Direct Memory Access,这是一种硬件的网络技术,它使得计算机访问远程的内存时无需远程机器上CPU的干预)已经可以提供50~100Gbps的网络带宽和微秒级的传输延迟。目前许多以深度学习为目标应用的GPU机群都部署了这样的网络。然而深度学习系统如何才能充分利用好硬件提供的通信能力使分布式的训练获得更大的性能提升呢?另外,使用RDMA的软件接口进行通信能够绕过TCP/IP协议栈,减少了操作系统内核态的运行开销。在这样的网络通信技术的支持下,任何与通信相关的计算处理的开销都会变得非常显著,而这正是许多原先基于TCP/IP而设计的网络通信机制中所存在的问题。

RPC(Remote Procedure Call,远程过程调用)是一个被广泛使用的多机之间的通信抽象原语,它的主要设计目标是通用性。在没有考虑RDMA的情况下,很多深度学习框架都会采用RPC的机制(例如gRPC)来实现多机之间的通信。然而,RPC需要维护一个内部的私有缓存,从而不得不引入用户数据的存储空间和内部缓存之间的数据拷贝。这种内存拷贝的开销在使用RDMA网络的情况下会变得非常明显。我们通过micro-benchmark观察到,跟使用基于TCP/IP的gRPC相比,直接通过RDMA的接口传输消息(对不同的消息大小)可以有2到10倍的性能提升。

那么针对深度学习的应用负载,如何才能更好地利用RDMA硬件的能力?首先,我们来分析一下深度学习应用的几个特点:

1)Tensor是深度学习计算中最主要的数据结构,大量的计算开销都是花在对Tensor的处理上。Tensor是一种比较简单的数据结构,主要由meta-data和payload两部分组成。Payload就是基本元素的数组,而meta-data就是Tensor的shape信息,也就是维度和每一维的大小。这种简单的数据结构在传输的时候其实不太需要复杂的序列化和反序列化的功能。

2)在相当多的情况下,Tensor是稠密的,并且其大小也是比较大的,也就是说在传输这样的Tensor的时候并不需要对其进行额外的批处理。

3)深度学习的训练过程是迭代的。每个迭代处理一个mini-batch。在不同的迭代之间,数据流图和很多Tensor的shape信息并不发生改变,并且其中不少的shape信息是可以在运行时前就静态决定的。

基于以上几个特点,我们可以对数据流图进行分析,找到那些可以静态决定shape信息的Tensor,以便在运行前,在接收端预先为其分配RDMA可访问的内存空间,并将其相应的可远程访问的地址传送给发送端。这样一来,在运行时,发送端可以通过单边的RDMA请求将Tensor的数据直接传输到接收端,从而完全避免了没有必要的额外内存拷贝,达到零拷贝的通信过程。我们将这种机制在TensorFlow上进行实验,和基于TCP/IP的gRPC相比,这一方法在一系列典型模型上均取得了多倍的性能改进。甚至和针对RDMA优化过的gRPC相比,我们的方法仍然能够取得超过50%的性能提升。

另外,我们在分布式深度学习方向上关注的另一个问题是如何自动地对资源无关的数据流图做优化的分布式执行,也就是自动划分数据流图中的计算任务并为其分配相应的计算资源,以使计算效率最优化。Google的Jeff Dean团队在这个方向上已经做了很好的先驱性工作。但局限于模型并行和单机多卡的运行环境,目前这仍然是一个非常重要并且大有可为的方向,需要结合数据并行,分布式及异构环境来综合考虑。

  提升单个计算单元的运算效率

前面提到过,使用深度学习框架来实现的模型算法,在运行时前会被转换成数据流图。不少具有实际应用价值的模型都非常复杂,由它们所转换出来的数据流图通常是由成千上万的操作节点构成,其中包含了很多运算量非常小的节点,也就是说它们的输入矩阵的大小很小,或者是其计算逻辑的复杂度相对于对输入数据访问的复杂度来说很低。大量这样的操作节点会引入以下一些运行时开销,并且这样的开销会非常显著。

1)深度学习系统运行时需要根据数据流图中节点的依赖关系来调度节点的执行。调度每个节点的系统开销和操作节点计算量的大小并没有直接关系,因此对于由许多小的操作节点构成的计算流图来说,系统调度所带来的额外开销就会相对比较大;

2)对于在GPU上运行的计算来说,每个操作节点的实现都对应着一个GPU的内核函数,而这个内核函数的每一次执行需要CPU调用显卡驱动来启动,因此也带来了常数量级的额外开销。这个开销相对于计算量小的内核函数的执行来说是非常明显的;

3)计算量小的操作节点往往难以挖掘出足够的数据并行性,因此不能充分利用处理器硬件中的计算资源。

解决这一问题的主要思路是内核融合(Kernel Fusion)。一些手工的优化方法就运用了这一思想,比如NVIDIA基于CuDNN的RNN库函数。它把整个循环神经网络实现成一个GPU的内核函数,因此获得了非常好的性能。然而它的缺点也非常明显,那就是不够灵活和通用,无法应用在其它网络或一些变种的循环神经网络中。而我们更加关注的是如何在深度学习的系统中自动地对任意的网络模型实施优化。

目前在学术界和工业界已经存在一些系统采用编译的方法生成融合的内核代码,比如TVM、Halide和Taco等。这些系统使用Tensor Algebra作为前端表示方法,每个Tensor Algebra表达式进而可以被编译成相应的内核代码。而Tensor Algebra可以作为更低一层的中间表达被集成到深度学习系统中,也就是说高层的数据流图可以先转换成由Tensor Algebra表达式组成的代码块,再被编译成可执行的代码。然而,这些系统对于可以进行融合的操作节点有很多限制,不能很好地融合多个非pointwise的操作,例如多个矩阵乘操作。然而,我们发现如果打破这一限制从而融合更多操作节点是可以带来更多显著的性能提升的。

在GPU的运行环境下融合多个非pointwise的操作具有一定的挑战性,因为非pointwise的操作中输入矩阵的每个元素都可能依赖于前一个操作的输出矩阵中的许多不同位置的元素值,所以在这两个操作之间需要插入Barrier同步原语。而在GPU中实现Barrier需要保证该内核的所有线程块在运行时都是保持活动状态的,这意味着我们必须要求融合后的内核采用有限个数的线程块,但同时又能够处理远超过线程块数量的数据块。

为了解决这一问题,我们尝试采用persistent-thread的线程块模型,也就是说在融合后的内核的整个生命周期启动固定数目的线程块并让它们保持活动状态。我们的优化系统在产生融合的内核代码的过程中类似于解决一个装箱(bin-pack)问题,即把待融合的子数据流图中的每一个操作节点所要处理的数据块分派给适当的活动线程块,从而使得每个线程块的负载尽可能均衡,并且保持操作节点的运算在原数据流图中的并行性。

为了生成优化的GPU内核函数,一个重要的考虑因素是线程块和数据块的合理划分。然而这又依赖于一些非常复杂的因素,比如操作节点运算中计算和访存复杂度的比率、GPU的shared memory的大小、寄存器文件的大小及分配方法等等。因此一个最优的选择是很难通过静态的方法决定的。幸运的是,深度学习的迭代性以及需要相当多的迭代才能收敛的特性使得我们可以利用早期的迭代过程来收集运行时的动态信息以帮助优化系统做更明智的决定。

  克服设备内存资源限制

设备内存的大小往往限制了可以处理的模型规模,解决这一问题的一个思路是对模型进行压缩和量化。如今学术界和工业界已经有大量的研究工作提出不同的压缩和量化的方法,然而,在实际的应用场景中使用压缩和量化仍然是个繁琐的迭代过程。在这个过程中,用户可能会进行以下几个方面的尝试。

1)不同的压缩方法。比如,是根据模型的参数值是否趋近于零,还是将其转换成某种贡献值之后趋近于零?压缩时是不是考虑一定的结构化(如果是面向GPU,可能需要压缩成块状稀疏矩阵来提高运行效率)?量化的值点是根据值域平均划分还是基于某种聚类来划分?

2)不同的压缩程度。要考虑在哪些层的神经元参数上做压缩,因为并不是所有层对压缩后模型效果的敏感程度是一样的;选择不同的压缩率或量化的比特数。

3)为了保持在大的压缩率下仍然取得好的模型效果,压缩过程可能需要是渐进的,比如一次压缩10%,然后重新训练,重复此过程直到取得目标的压缩率。那么每次渐进过程的压缩率就是一个需要调整的参数。

显然,这样一个繁琐的过程需要一个好的工具来使之变得方便。这也是我们组正在关注的一个问题。我们正在尝试扩展TensorFlow的API来使用户可以在模型脚本中直接控制量化和压缩的方法、对象、程度和过程。

压缩和量化通常是用来解决模型部署时的性能和内存资源不足的问题,而解决模型训练时内存不够的问题的思路之一是用计算来换内存。比如,如果数据流图中某一个操作节点的计算量很小,但是输出的中间结果数据量很大,一个更好的处理方式是不在内存中保存这个中间结果,而在后面需要用到它的时候再重新执行这个操作节点的计算。当然,重新计算还是引入了一定的额外开销。

事实上,还存在另外一种解决这个问题的思路,就是将大的输入数据就保存在CPU端的主存里,并将操作节点实现成流式的处理,将大的输入数据分段拷贝进GPU的设备内存,并通过异步的拷贝使得对每一分段的计算时间和下一分段的拷贝时间能够重叠起来,从而掩盖住数据拷贝的开销。对于矩阵乘法这样的操作,由于计算复杂度相对于访存复杂度较高,当分段较大的时候,计算时间和拷贝时间是可以达到完美重叠的。然而,如果所要进行的操作不是矩阵乘法,而是一些简单的pointwise操作,计算的复杂度就没有办法和内存拷贝的开销相抵消。所以这种做法还需要跟内核融合相结合。比如将矩阵乘法和后续的pointwise操作相融合,每一个分段的计算都会把该分段的矩阵乘和pointwise操作都做完,然后再处理下一个分段。

以上就是微软亚洲研究院异构计算组在深度学习系统框架优化问题上的思考和研究方向,我们希望从系统层面进一步推动深度学习乃至人工智能的发展。
2019-04-30 11:55:22 xiaoxiaowenqiang 阅读数 943
  • 玩转深度学习实战教程

    玩转深度学习视频培训课程,详细讲解深度学习的原理和利用深度学习框架TensorFlow进行项目实战。课程通过Kaggle竞赛平台的Titanic问题讲解TensorFlow的基本用法以及问题处理的常用技巧,讲解深度学习图像领域的卷积神经网络CNN和多个经典的网络架构、CNN的应用,讲解自然语言处理领域的RNN、LSTM以及它们的多种变种结构,构建语言模型和对话机器人,介绍损失函数和优化深度学习算法在TensorFlow中的实现。

    3209 人正在学习 去看看 王而川

ARM_NEON_CNN编程

SIMD单指令多数据流 intrinsics指令 内联汇编 CNN卷积网络优化 深度学习优化

博文末尾支持二维码赞赏哦 _

本文github

神经网络arm neon加速实现

常用NEON 内置函数记录备用

ARM Cortex系列(A8/A9/A15/A7) NEON多媒体处理SIMD引擎优化

aarch64 armv8 neon intrinsics 和内嵌汇编混用

32位 armv7 neon intrinsics 和内嵌汇编混用

ARM NEON 社区

ARM平台NEON指令的编译和优化 编译选项

程序优化方法经验大全——神文

术语:

System-on-Chip(SOC) 片上系统:核心、内存控制器、片上内存、外围设备、总线互连和其他逻辑(可能包括模拟或射频组件),以便产生系统。 SOC通常指集成度较高的设备,包括单个设备中系统的许多部分,可能包括模拟、混合信号或射频电路。

专用集成电路Application Specific Integrated Circuit(ASIC) :包含ARM内核、内存和其他组件。显然,ASIC和SOC之间有很大的重叠。

嵌入式系统 Embedded systems,
内存消耗 Memory Footprint(memory usage),
SIMD(Single Instruction, Multiple Data) 单指令多数据流,
MMU(Memory Management Unit) 内存管理单元,
MPE(Media Processing Engine) 媒体处理引擎。
VFP(Vector Floating Point) 向量浮点

参考1 ARM NEON 编程系列

arm官方数据手册

Cortex-A Series Programmer’s Guide Version: 4.0

ARM CPU最开始只有普通的寄存器,可以进行基本数据类型的基本运算。
自ARMv5开始引入了VFP(Vector Floating Point)指令,该指令用于向量化加速浮点运算。
自ARMv7开始正式引入NEON指令,NEON性能远超VFP,因此VFP指令被废弃。

SIMD即单指令多数据指令,目前在x86平台下有MMX/SSE/AVX系列指令,arm平台下有NEON指令。
一般SIMD指令通过intrinsics(内部库C函数接口的函数) 或者 汇编 实现。

Intrinsics(内联函数)是使用C语言的方式对NEON寄存器进行操作,因为相比于传统的使用纯汇编语言,具有可读性强,开发速度快等优势。如果需要在代码中调用NEON Intrinsics函数,需要加入头文件"arm_neon.h"。

NEON C内联函数(intrinsics)是由ARM定义的一组全新的数据类型和内联函数,便于使用C语言直接访问NEON单元。在C/C++程序中,内联函数就同普通函数一样,但在编译时,这些内联函数会直接映射为NEON提供的向量指令。当前GCC编译器和ARM编译器都支持相同的NEON内联语法,只需在程序中添加“arm_neon.h”头文件,就可以使用NEON内联函数。

ARM NEON常用 intrinsics 函数总结 !!!

优势:使用内联函数进行优化,开发人员无需关注寄存器分配和互锁等问题,这些都交由编译器处理,而且编写程序比较容易,优化后的性能相对较高。

不足:目前内联函数所提供的功能和灵活性仍远远比不上汇编指令,并且经过编译器编译后,会反复加载/存取寄存器数据,导致系统时钟的浪费。

采用汇编语言进行NEON(NEON 汇编(assembly))的最底层优化,可以使优化性能最大化,但汇编语言比较灵活,手写汇编程序对开发人员来说具有较大挑战,如果使用不恰当,反而会影响优化性能。

在这里,一条SIMD加法指令可以同时得到8个加法结果。就计算步骤本身而言,比单独使用8条加法指令能够获得8倍的加速比。从该示例也可以看出,随着寄存器长度的变长,单指令能够处理的数据量也越来越大,从而获得更高的加速性能。
在Intel最新的AVX2指令集中,寄存器最大长度已经达到512位。

类似于Intel CPU下的MMX/SSE/AVX/FMA指令,ARM CPU的NEON指令同样是通过向量化计算来进行速度优化,通常应用于图像处理、音视频处理等等需要大量计算的场景。

SISD(Single Instruction Single Data)单指令单数据

add r0, r5  # 单条指令执行一个运算
add r1, r6
add r2, r7
add r3, r8

SIMD(Single Instruction Multiple Data (vector mode向量模式))单指令多数据

VADD.F32 S24, S8, S16 
// four operations occur 单条指令并行执行四个运算
// S24 = S8 +S16
// S25 = S9 +S17
// S26 = S10 +S18
// S27 = S11 +S20

SIMD(Single Instruction Multiple Data (packed data mode)包数据模式)

VADD.I16 Q10, Q8, Q9
// One operation adds two 64-bit registers, 128位寄存器
// but each of the four 16-bit lanes in the register is added separately.
// 单个数据为16位,所以有8个数据并行计算加法运算

NEON支持的数据类型:

  • 32bit single precision floatingpoint , 32bit 单精度浮点数;

  • 8, 16, 32 and 64bit unsigned and signed integers , 8, 16, 32 and 64bit 无符号/有符号 整型;

  • 8 and 16bit polynomials 8 and 16bit 多项式。

    B字节Byte: 8 bits.
    H半字Halfword: 16 bits. 半精度浮点16位
    S字Word: 32 bits. 单精度浮点32位
    D双字Doubleword:64 bits. 双精度浮点64位
    Q四字Quadword: 128 bits.

浮点数取整:

向负无穷取整(向左取整) Round towards Minus Infinity (RM) roundTowardsNegative

向正无穷取整(向右取整) Round towards Plus Infinity (RP) roundTowardsPositive

向零取整(向中间取整)Round towards Zero (RZ) roundTowardZero

就近取整 Round to Nearest (RN) roundTiesToEven

随机取整

NEON数据类型说明符:

  • Unsigned integer 无符号整形 U8 U16 U32 U64
  • Signed integer 有符号整形 S8 S16 S32 S64
  • Integer of unspecified type 未指定类型的整数 I8 I16 I32 I64
    Floating point number F16 F32 浮点数 16位浮点数(半精度) 32位浮点数(全精度)
    Polynomial over {0,1} P8 多项式

注:F16不适用于数据处理运算,只用于数据转换,仅用于实现半精度体系结构扩展的系统。

多项式算术在实现某些加密、数据完整性算法中非常有用。

寄存器 ARMV7架构包含:

16个通用寄存器(32bit),R0-R15 register

16个NEON寄存器(128bit),Q0-Q15 quad四字寄存器(同时也可以被视为32个64bit的寄存器,D0-D31 double双字寄存器)

16个VFP寄存器(32bit),S0-S15,single 单字寄存器

NEON和VFP的区别在于VFP是加速浮点计算的硬件不具备数据并行能力,同时VFP更尽兴双精度浮点数(double)的计算,NEON只有单精度浮点计算能力。

16个通用寄存器

寄存器 r0 到 r7 称为低位寄存器。 寄存器 r8 到r15 称为高位寄存器。

下列寄存器名称是预先声明的:

  • r0-r15 和 R0-R15
  • a1-a4(自变量、结果或暂存寄存器,r0 到 r3 的同义词)
  • v1-v8(变量寄存器,r4 到 r11)
  • sb 和 SB(静态基址,r9)
  • ip 和 IP(内部程序调用暂存寄存器,r12)
  • sp 和 SP(堆栈指针,r13)
  • lr 和 LR(链接寄存器,r14)
  • pc 和 PC(程序计数器,r15)。

NEON寄存器有几种形式:

  • 16×128bit寄存器(Q0-Q15); 16个128位的寄存器
  • 或32×64bit寄存器(D0-D31) 32个64位的寄存器
  • 或上述寄存器的组合。

以下扩展寄存器名称是预先声明的:

  • q0-q15 和 Q0-Q15(NEON™ 四字寄存器)
  • d0-d31 和 D0-D31(NEON 双字寄存器,VFP 双精度寄存器)
  • s0-s31 和 S0-S31(VFP 单精度寄存器)。

一个D寄存器64位是双字宽度,一个Q寄存器是128位是四字宽度。

注:每一个Q0-Q15寄存器映射到 一对D寄存器。

寄存器之间的映射关系:

  • D<2n> 偶数 映射到 Q 的最低有效半部;
  • D<2n+1> 奇数 映射到 Q 的最高有效半部;
  • S<2n> 映射到 D 的最低有效半部
  • S<2n+1> 映射到 D 的最高有效半部

例如,通过引用 D12 可以访问 Q6 中向量元素的最低有效半部,通过引用 D13 可以访问这些元素的最高有效半部。

指令集概述

所有 ARM 指令的长度都是 32 位。 这些指令是按字对齐方式存储的,因此在ARM 状态下,指令地址的两个最低有效位始终为零。

跳转指令,此类指令用于:

  • 1.向后跳转以构成循环
  • 2.在条件结构中向前跳转
  • 3.跳转到子例程
  • 4.在 ARM 状态和 Thumb 状态之间转换处理器状态。

寄存器加载和存储指令

此类指令用于从内存加载单个寄存器的值,或者在内存中存储单个寄存器的值。它们可加载或存储 32 位字、16 位半字或 8 位无符号字节。 可以用符号或零扩展字节和半字加载以填充 32 位寄存器。此外,还定义了几个可将 64 位双字值加载或存储到两个 32 位寄存器的指令。

数据处理指令

此类指令用于对通用寄存器执行运算。 它们可对两个寄存器的内容执行加法、减法或按位逻辑等运算,并将结果存放到第三个寄存器中。 此外,它们还可以对单个寄存器中的值执行运算,或者对寄存器中的值与指令中提供的常数(立即值)执行运算。

NEON 数据处理指令可分为:

    1. 正常指令 Normal instructions 结果 同 操作数 同大小同类型。

      生成大小相同且类型通常与操作数向量相同到结果向量。

      正常指令可对上述任意向量类型执行运算,并生成大小相同且类型通常与操作数向量相同的结果向量。

      通过将 Q 附加到指令助记符,可以指定正常指令的操作数和结果必须全部为四字。

      这样指定后,如果操作数或结果不是四字,则汇编程序会生成错误。

    1. 长指令 Long instructions 操作双字vectors,生成四倍长字vectors 结果的宽度一般比操作数加倍,同类型。

      在指令中加L

      长指令对双字向量操作数执行运算,并生成四字向量结果。 所生成的元素通常是操作数元素宽度的两倍,并属于同一类型。通过将 L 追加到指令助记符来指定长指令。

      对双字向量操作数执行运算,生成四字向量到结果。所生成的元素一般是操作数元素宽度到两倍,并属于同一类型。L标记,如VMOVL。

    1. 宽指令 Wide instructions 操作 双字+四倍长字,生成四倍长字,结果和第一个操作数都是第二个操作数的两倍宽度。

      在指令中加W

      一个双字向量操作数和一个四字向量操作数执行运算,生成四字向量结果。W标记,如VADDW。

      宽指令对一个双字向量操作数和一个四字向量操作数执行运算。 此类指令生成四字向量结果。 所生成的元素和第一个操作数的元素是第二个操作数元素宽度的两倍。

      通过将 W 追加到指令助记符来指定宽指令。

    1. 窄指令 Narrow instructions 操作四倍长字,生成双字 结果宽度一般是操作数的一半

      在指令中加N

      四字向量操作数执行运算,并生成双字向量结果,所生成的元素一般是操作数元素宽度的一半。N标记,如VMOVN。

      窄指令对四字向量操作数执行运算,并生成双字向量结果。 所生成的元素通常是操作数元素宽度的一半。

      通过将 N 追加到指令助记符来指定窄指令。

    1. 饱和指令 Saturating variants

      通过在 V 和指令助记符之间使用 Q 前缀来指定饱和指令。

    对于有符号饱和运算,如果结果小于 –2^n,则返回的结果将为 –2^n;

    对于无符号饱和运算,如果整个结果将是负值,那么返回的结果是 0;如果结果大于 2^n–1,则返回的结果将为 2^n–1;

    NEON中的饱和算法:通过在V和指令助记符之间使用Q前缀可以指定饱和指令,原理与上述内容相同。

    饱和指令:当超过数据类型指定到范围则自动限制在该范围内。Q标记,如VQSHRUN

数据类型 x 的饱和范围 (s 就是signed,有符号的意思,u就是unsigned,无符号的意思)

s8   –2^7  <= x < 2^7 
s16  –2^15 <= x < 2^15 
s32  –2^31 <= x < 2^31 
s64  –2^63 <= x < 2^63 
u8   0     <= x < 2^8 
u16  0     <= x < 2^16 
u32  0     <= x < 2^32 
u64  0     <= x < 2^64

NEON指令集(重点)ARMv7/AArch32指令格式

所有的支持NEON指令都有一个助记符V,下面以32位指令为例,说明指令的一般格式:

V{<mod模式>}<op操作>{<shape指令类型>}{<cond条件>}{.<dt数据类型>}{<dest目标地址>}, src1, src2

<mod模式> 可选:

Q: Staturating饱和结果,The instruction uses saturating arithmetic, so that the result is saturated within the range of the specified data type, such as VQABS, VQSHLetc.
    
VQADD.S16 D0, D2, D3

H: Halving,半结果,结果右移动移位,相当于得到结构后在除以2 The instruction will halve the result. It does this by shifting right by one place (effectively a divide by two with truncation), such as VHADD,VHSUB.

VHADD.S16 Q0, Q1, Q4

D: Doubling,双倍结果 The instruction doubles the result, such as VQDMULL, VQDMLAL, VQDMLSL and VQ{R}DMULH.

VQDMULL.S16 Q0, D1, D3   双倍+饱和+长指令


R: Rounding,取整 The instruction will perform rounding on the result, equivalent to adding 0.5 to the result before truncating, such as VRHADD, VRSHR.

VRSUBHN.I16 D0, Q1, Q3

<op操作>: 必须

the operation (for example, ADD加, SUB减, MUL乘).

NEON指令按照作用可以分为:加载数据、存储数据、加减乘除运算、逻辑AND/OR/XOR运算、比较大小运算

shape指令类型 可选:

即前文中的Long (L长指令,结果数据位扩大), Wide (W), Narrow (N结果数据位变窄).

<cond条件> Condition 可选,

used with IT instruction.

<.dt> Datatype 可选 数据类型 .数据类型 前面有点

such as .s8, .u8, .f32 , .I16, .S16 etc.

Destination. 可选 目标操作数地址

Source operand 1. 必须 源操作数地址
Source operand 2. 必须 源操作数地址

注: {} 表示可选的参数。

比如:

VADD.I16 D0, D1, D2 @ 16位整数 加法

VMLAL.S16 Q2, D8, D9 @ 有符号16位整数 乘加

使用NEON主要有四种方法:

    1. NEON优化库(Optimized libraries)
    1. 向量化编译器(Vectorizing compilers)
    1. NEON intrinsics
    1. NEON assembly

根据优化程度需求不同,第4种最为底层,若熟练掌握效果最佳,一般也会配合第3种一起使用。

  1. 优化库 Libraries:直接在程序中调用优化库

OpenMax DL:支持加速视频编解码、信号处理、色彩空间转换等;

Ne10:一个ARM的开源项目,提供数学运算、图像处理、FFT函数等。

  1. 向量化编译 Vectorizing compilers:GCC编译器的向量优化选项

在GCC选项中加入向量化表示能有助于C代码生成NEON代码,如‐ftree‐vectorize。

  1. NEON intrinsics:提供了一个连接NEON操作的C函数接口,编译器会自动生成相关的NEON指令,支持ARMv7或ARMv8平台。

所有的intrinsics函数都在GNU官方说明文档

3. NEON Instrinsic函数

NEON Instrinsic是编译器支持的一种buildin类型和函数的集合,基本涵盖NEON的所有指令,通常这些Instrinsic包含在arm_neon.h头文件中。

ARM-NEON-Intrinsics

使用ARM NEON Intrinsics加速Video Codec 参考

数据类型

NEON 向量数据类型是根据以下模式命名的:<type类型><size大小宽度>x<number of lanes通道数量>_t

例如,int16x4_t 是一个包含四条向量线的向量,每条向量线包含一个有符号 16位整数。

NEON Intrinsics内置的整数数据类型主要包括以下几种:

  • (u)int8x8_t;
  • (u)int8x16_t;
  • (u)int16x4_t;
  • (u)int16x8_t;
  • (u)int32x2_t;
  • (u)int32x4_t;
  • (u)int64x1_t;

其中,第一个数字代表的是数据类型宽度为8/16/32/64位,第二个数字代表的是一个寄存器中该类型数据的数量。如int16x8_t代表16位有符号数,寄存器中共有8个数据。

某些内在函数使用以下格式的向量类型数组:

xx_t

这些类型被视为包含名为 val 的单个元素的普通 C 结构。

以下是一个结构定义示例:

struct int16x4x2_t
{
int16x4_t val[2];
};

标号和具体类型转换:

标记  双字64位D寄存器    四字128位寄存器
s8    int8x8_t           int8x16_t        有符号整数
s16   int16x4_t          int16x8_t
s32   int32x2_t          int32x4_t
s64   int64x1_t          int64x2_t 
u8    uint8x8_t          uint8x16_t       无符号整数
u16   uint16x4_t         uint16x8_t
u32   uint32x2_t         uint32x4_t
u64   uint64x1_t         uint64x2_t
f16   float16x4_t        float16x8_t      浮点数
f32   float32x2_t        float32x4_t
p8    poly8x8_t          poly8x16_t       多项式数
p16   poly16x4_t         poly16x8_t

vcombine_type() 连接组合函数 结果类型长度加倍

vget_high_type() 获取高位 结果类型长度减半

vget_low_type() 获取低位 结果类型长度减半

长指令类型 结果类型长度加倍

窄指令类型 结果类型长度减半

内在函数 inline function

每个内在函数的格式如下:

_

另外提供 q 标记来指定内在函数对 128 位向量进行运算。

例如:

  • vmul_s16,表示两个有符号 16 位值的向量相乘multiply。
    这编译为 VMUL.I16 d2, d0, d1。

  • vaddl_u8,l为long长指令标识,是指两个包含无符号 8 位值的 64 位向量按长型相加,结果为无符号 16 位值的 128 位向量。
    这编译为 VADDL.U8 q1, d0, d1。

  • int8_t vget_lane_s8 (int8x8_t __a, const int __b);

v是向量操作,可以认为就是neon函数,shr是右移位,lane表示操作向量中的某个元素,s8表示结果是s8类型(向量)

  • int8x8_t vget_high_s8 (int8x16_t __a); //ri = a(i+4);

v是向量操作,可以认为就是neon函数,get是取值,high表示取高64位,s8表示结果是s8类型(向量)

  • int8x8_t vget_low_s8 (int8x16_t __a); //ri = ai;

v是向量操作,可以认为就是neon函数,get是取值,low表示取低64为,s8表示结果是s8类型(向量)

 v<noen函数前缀>q<饱和操作>ops<具体操作>tyep<指令类型  q,l,w,n>_flag<标识  n,lane,high or low>_dtype<返回值类型或参数类型>
 
add 加法 
mul 乘法 
sub 减法 
mla 乘加 
mls 乘减 
ceq 比较,类似与 == 
cge 比较,类似与 >= 
cle 比较,类似与 <= 
cgt 比较,类似与 > 
clt 比较,类似与 < 
tst 做与运算后,判断是否等于0 ,ri = (ai & bi != 0) ? 1…1:0…0; 
abd 两个向量相减后的绝对值,vabd -> ri = |ai - bi|; 
max 求最大值,ri = ai >= bi ? ai : bi; 
min 求最小值,ri = ai >= bi ? bi : ai; 
shl 左移位, ri = ai << b; 
shr 右移位, ri = ai >> b; 
abs 求绝对值,ri = |ai|; 
neg 取反,ri = -ai; 
mvn 按位取反,ri = ~ai; 
and 与运算,ri = ai & bi; 
orr 或运算,ri = ai | bi; 
eor 异或运算,ri = ai ^ bi; 
cls 计算连续相同的位数 
get 取值,从向量中取出一个值,所谓的向量可以认为是一个数组,给数组中的某个元素赋值 
set 赋值,给向量中赋值 
dup 构造一个向量,并赋上初始值,ri = a; 
combine 合并操作,把两个向量合并 
mov 改变数据类型,数据范围,比如把u8 变成u16,或者u16变成u8 
zip 压缩操作 
uzp 解压操作 
ld1 加载数据,给定的buffer 指针中拷贝数据,注意是ld后面的是数字1,而不是字母l 
st1 拷贝数据,将neon数据类型拷贝到指定buffer中

示例函数指令分析

int16x8_t vqaddq_s16 (int16x8_t, int16x8_t)
int16x4_t vqadd_s16 (int16x4_t, int16x4_t)
  • 第一个字母’v’指明是vector向量指令,也就是NEON指令;
  • 第二个字母’q’指明是饱和指令,即后续的加法结果会自动饱和;
  • 第三个字段’add’指明是加法指令;
  • 第四个字段’q’指明操作寄存器宽度,为’q’时操作QWORD, 为128位;未指明时操作寄存器为DWORD,为64位;
  • 第五个字段’s16’指明操作的基本单元为有符号16位整数,其最大表示范围为-32768 ~ 32767;
  • 第六个字段为空,普通指令,形参和返回值类型约定与C语言一致。

其它可能用到的助记符包括:

  • l 长指令,数据扩展,双字运算得到四字结果
  • w 宽指令,数据对齐,双字和四字运算得到四字结果
  • n 窄指令, 数据压缩,四字运算得到双字结果

示例2

uint8x8_t vld1_u8 (const uint8_t *)
  • 第一个字母’v’指明是vector向量指令,也就是NEON指令;
  • 第二个字段’ld’表示加载指令 load
  • 第三个字段’1’(注意是1,不是l)表示顺次加载。如果需要处理图像的RGB分量,可能会用到vld3间隔3个单元加载。

NEON指令按照作用可以分为:加载数据、存储数据、加减乘除运算、逻辑AND/OR/XOR运算、比较大小运算

初始化寄存器

// 寄存器的每个lane(通道)都赋值为一个值N
Result_t vcreate_type(Scalar_t N)   // type需要换成具体类型 s8, u8, f32, I16, S16
Result_t vdup_type(Scalar_t N)      // vcreate_s8  vdup_s8   vmov_s8
Result_t vmov_type(Scalar_t N)

加载load 内存数据进寄存器

// 间隔为x,加载数据进NEON寄存器, 间隔:交叉存取,是ARM NEON特有的指令
Result_t vld[x]_type(Scalar_t* N)  // 
Result_t vld[x]q_type(Scalar_t* N) // vld1q_s32 间隔1 即连续内存访问, 

// **通过将 Q 附加到指令助记符,可以指定正常指令的操作数和结果必须全部为四字。** 

float32x4x3_t = vld3q_f32(float32_t* ptr)
// 此处间隔为3,即交叉读取12个float32进3个NEON寄存器中。
// 3个寄存器的值分别为:
// {ptr[0],ptr[3],ptr[6],ptr[9]},   // 128为Q寄存器
// {ptr[1],ptr[4],ptr[7],ptr[10]},
// {ptr[2],ptr[5],ptr[8],ptr[11]}。
    1. VLD1是最简单的形式,从内存加载1~4个寄存器的数据,没有deinterleave,即线性加载;
    1. VLD2加载2或者4个寄存器的数据,解交织奇偶元素到各自的寄存器,这样很容易的把交织的立体声音频数据分解为左右声道的数据;
    1. VLD3加载3个寄存器的数据,很方便的把RGB的数据分为R、G、B通道;
    1. VLD4加载4个寄存器的数据,解交织,用于分解ARGB图像数据;

存储set 寄存器数据到内存 间隔为x,存储NEON寄存器的数据到内存中

void vst[x]_type(Scalar_t* N)
void vst[x]q_type(Scalar_t* N)

算数运算指令

[普通指令] 普通加法运算 res = M+N

Result_t vadd_type(Vector_t M,Vector_t N)
Result_t vaddq_type(Vector_t M,Vector_t N)

[长指令 long] 变长加法运算 res = M+N

为了防止溢出,一种做法是使用如下指令,加法结果存储到长度x2的寄存器中,

如:


Result_t vaddl_type(Vector_t M,Vector_t N)

vuint16x8_t res = vaddl_u8(uint8x8_t M,uint8x8_t N)

[宽指令] 加法运算 res = M+N,第一个参数M宽度大于第二个参数N。

Result_t vaddw_type(Vector_t M,Vector_t N)

[普通指令] 减法运算 res = M-N

Result_t vsub_type(Vector_t M,Vector_t N)

[普通指令] 乘法运算 res = M*N

Result_t vmul_type(Vector_t M,Vector_t N)
Result_t vmulq_type(Vector_t M,Vector_t N)

[普通指令] 乘&加法运算 res = M + N*P

Result_t vmla_type(Vector_t M,Vector_t N,Vector_t P)
Result_t vmlaq_type(Vector_t M,Vector_t N,Vector_t P)

乘&减法运算 res = M-N*P

Result_t vmls_type(Vector_t M,Vector_t N,Vector_t P)
Result_t vmlsq_type(Vector_t M,Vector_t N,Vector_t P)

数据处理指令

[普通指令] 计算绝对值 res=abs(M)

Result_t vabs_type(Vector_t M)

[普通指令] 计算负值 res=-M negative

Result_t vneg_type(Vector_t M)

[普通指令] 计算最大值 res=max(M,N) maxmum

Result_t vmax_type(Vector_t M,Vector_t N)

[普通指令] 计算最小值 res=min(M,N)

Result_t vmin_type(Vector_t M,Vector_t N)

比较指令

[普通指令] 比较是否相等 res=mask(M == N) compare equal

Result_t vceg_type(Vector_t M,Vector_t N)

[普通指令] 比较是否大于或等于 res=mask(M >= N) compare greate and equal

Result_t vcge_type(Vector_t M,Vector_t N)

[普通指令] 比较是否大于 res=mask(M > N)

Result_t vcgt_type(Vector_t M,Vector_t N)

[普通指令] 比较是否小于或等于 res=mask(M <= N) compare little and equal

Result_t vcle_type(Vector_t M,Vector_t N)

[普通指令] 比较是否小于 res=mask(M < N) compare little

Result_t vclt_type(Vector_t M,Vector_t N)

向量加法:

正常向量加法 vadd -> Vr[i]:=Va[i]+Vb[i]
Vr、Va、Vb 具有相等的向量线大小。

//64位==
int8x8_t vadd_s8(int8x8_t a, int8x8_t b); // VADD.I8 d0,d0,d0
int16x4_t vadd_s16(int16x4_t a, int16x4_t b); // VADD.I16 d0,d0,d0
int32x2_t vadd_s32(int32x2_t a, int32x2_t b); // VADD.I32 d0,d0,d0
int64x1_t vadd_s64(int64x1_t a, int64x1_t b); // VADD.I64 d0,d0,d0
float32x2_t vadd_f32(float32x2_t a, float32x2_t b); // VADD.F32 d0,d0,d0
uint8x8_t vadd_u8(uint8x8_t a, uint8x8_t b); // VADD.I8 d0,d0,d0
uint16x4_t vadd_u16(uint16x4_t a, uint16x4_t b); // VADD.I16 d0,d0,d0
uint32x2_t vadd_u32(uint32x2_t a, uint32x2_t b); // VADD.I32 d0,d0,d0
uint64x1_t vadd_u64(uint64x1_t a, uint64x1_t b); // VADD.I64 d0,d0,d0
//128位==
int8x16_t vaddq_s8(int8x16_t a, int8x16_t b); // VADD.I8 q0,q0,q0
int16x8_t vaddq_s16(int16x8_t a, int16x8_t b); // VADD.I16 q0,q0,q0
int32x4_t vaddq_s32(int32x4_t a, int32x4_t b); // VADD.I32 q0,q0,q0
int64x2_t vaddq_s64(int64x2_t a, int64x2_t b); // VADD.I64 q0,q0,q0
float32x4_t vaddq_f32(float32x4_t a, float32x4_t b); // VADD.F32 q0,q0,q0
uint8x16_t vaddq_u8(uint8x16_t a, uint8x16_t b); // VADD.I8 q0,q0,q0
uint16x8_t vaddq_u16(uint16x8_t a, uint16x8_t b); // VADD.I16 q0,q0,q0
uint32x4_t vaddq_u32(uint32x4_t a, uint32x4_t b); // VADD.I32 q0,q0,q0
uint64x2_t vaddq_u64(uint64x2_t a, uint64x2_t b); // VADD.I64 q0,q0,q0

向量长型加法:vaddl -> Vr[i]:=Va[i]+Vb[i]

Va、Vb 具有相等的向量线大小,结果为向量线宽度变成两倍的 128 位向量。

int16x8_t vaddl_s8(int8x8_t a, int8x8_t b); // VADDL.S8 q0,d0,d0
int32x4_t vaddl_s16(int16x4_t a, int16x4_t b); // VADDL.S16 q0,d0,d0
int64x2_t vaddl_s32(int32x2_t a, int32x2_t b); // VADDL.S32 q0,d0,d0
uint16x8_t vaddl_u8(uint8x8_t a, uint8x8_t b); // VADDL.U8 q0,d0,d0
uint32x4_t vaddl_u16(uint16x4_t a, uint16x4_t b); // VADDL.U16 q0,d0,d0
uint64x2_t vaddl_u32(uint32x2_t a, uint32x2_t b); // VADDL.U32 q0,d0,d0

向量宽型加法:vaddw -> Vr[i]:=Va[i]+Vb[i] 64位与128位运算得到128位

int16x8_t vaddw_s8(int16x8_t a, int8x8_t b); // VADDW.S8 q0,q0,d0
int32x4_t vaddw_s16(int32x4_t a, int16x4_t b); // VADDW.S16 q0,q0,d0
int64x2_t vaddw_s32(int64x2_t a, int32x2_t b); // VADDW.S32 q0,q0,d0
uint16x8_t vaddw_u8(uint16x8_t a, uint8x8_t b); // VADDW.U8 q0,q0,d0
uint32x4_t vaddw_u16(uint32x4_t a, uint16x4_t b); // VADDW.U16 q0,q0,d0
uint64x2_t vaddw_u32(uint64x2_t a, uint32x2_t b); // VADDW.U32 q0,q0,d0

向量半加:vhadd -> Vr[i]:=(Va[i]+Vb[i])>>1 求和后除以2

//64位
int8x8_t vhadd_s8(int8x8_t a, int8x8_t b); // VHADD.S8 d0,d0,d0
int16x4_t vhadd_s16(int16x4_t a, int16x4_t b); // VHADD.S16 d0,d0,d0
int32x2_t vhadd_s32(int32x2_t a, int32x2_t b); // VHADD.S32 d0,d0,d0
uint8x8_t vhadd_u8(uint8x8_t a, uint8x8_t b); // VHADD.U8 d0,d0,d0
uint16x4_t vhadd_u16(uint16x4_t a, uint16x4_t b); // VHADD.U16 d0,d0,d0
uint32x2_t vhadd_u32(uint32x2_t a, uint32x2_t b); // VHADD.U32 d0,d0,d0
// 128位
int8x16_t vhaddq_s8(int8x16_t a, int8x16_t b); // VHADD.S8 q0,q0,q0
int16x8_t vhaddq_s16(int16x8_t a, int16x8_t b); // VHADD.S16 q0,q0,q0
int32x4_t vhaddq_s32(int32x4_t a, int32x4_t b); // VHADD.S32 q0,q0,q0
uint8x16_t vhaddq_u8(uint8x16_t a, uint8x16_t b); // VHADD.U8 q0,q0,q0
uint16x8_t vhaddq_u16(uint16x8_t a, uint16x8_t b); // VHADD.U16 q0,q0,q0
uint32x4_t vhaddq_u32(uint32x4_t a, uint32x4_t b); // VHADD.U32 q0,q0,q0

向量舍入半加:vrhadd -> Vr[i]:=(Va[i]+Vb[i]+1)>>1 求和再加1后除以2

//64位
int8x8_t vrhadd_s8(int8x8_t a, int8x8_t b); // VRHADD.S8 d0,d0,d0
int16x4_t vrhadd_s16(int16x4_t a, int16x4_t b); // VRHADD.S16 d0,d0,d0
int32x2_t vrhadd_s32(int32x2_t a, int32x2_t b); // VRHADD.S32 d0,d0,d0
uint8x8_t vrhadd_u8(uint8x8_t a, uint8x8_t b); // VRHADD.U8 d0,d0,d0
uint16x4_t vrhadd_u16(uint16x4_t a, uint16x4_t b); // VRHADD.U16 d0,d0,d0
uint32x2_t vrhadd_u32(uint32x2_t a, uint32x2_t b); // VRHADD.U32 d0,d0,d0
//128位
int8x16_t vrhaddq_s8(int8x16_t a, int8x16_t b); // VRHADD.S8 q0,q0,q0
int16x8_t vrhaddq_s16(int16x8_t a, int16x8_t b); // VRHADD.S16 q0,q0,q0
int32x4_t vrhaddq_s32(int32x4_t a, int32x4_t b); // VRHADD.S32 q0,q0,q0
uint8x16_t vrhaddq_u8(uint8x16_t a, uint8x16_t b); // VRHADD.U8 q0,q0,q0
uint16x8_t vrhaddq_u16(uint16x8_t a, uint16x8_t b); // VRHADD.U16 q0,q0,q0
uint32x4_t vrhaddq_u32(uint32x4_t a, uint32x4_t b); // VRHADD.U32 q0,q0,q0

向量饱和加法:vqadd -> Vr[i]:=sat(Va[i]+Vb[i])

//64位	
int8x8_t vqadd_s8(int8x8_t a, int8x8_t b); // VQADD.S8 d0,d0,d0
int16x4_t vqadd_s16(int16x4_t a, int16x4_t b); // VQADD.S16 d0,d0,d0
int32x2_t vqadd_s32(int32x2_t a, int32x2_t b); // VQADD.S32 d0,d0,d0
int64x1_t vqadd_s64(int64x1_t a, int64x1_t b); // VQADD.S64 d0,d0,d0
uint8x8_t vqadd_u8(uint8x8_t a, uint8x8_t b); // VQADD.U8 d0,d0,d0
uint16x4_t vqadd_u16(uint16x4_t a, uint16x4_t b); // VQADD.U16 d0,d0,d0
uint32x2_t vqadd_u32(uint32x2_t a, uint32x2_t b); // VQADD.U32 d0,d0,d0
uint64x1_t vqadd_u64(uint64x1_t a, uint64x1_t b); // VQADD.U64 d0,d0,d0
//128位  前面的q表示饱和运算,后面的q表示q寄存器,128位寄存器操作数
int8x16_t vqaddq_s8(int8x16_t a, int8x16_t b); // VQADD.S8 q0,q0,q0
int16x8_t vqaddq_s16(int16x8_t a, int16x8_t b); // VQADD.S16 q0,q0,q0
int32x4_t vqaddq_s32(int32x4_t a, int32x4_t b); // VQADD.S32 q0,q0,q0
int64x2_t vqaddq_s64(int64x2_t a, int64x2_t b); // VQADD.S64 q0,q0,q0
uint8x16_t vqaddq_u8(uint8x16_t a, uint8x16_t b); // VQADD.U8 q0,q0,q0
uint16x8_t vqaddq_u16(uint16x8_t a, uint16x8_t b); // VQADD.U16 q0,q0,q0
uint32x4_t vqaddq_u32(uint32x4_t a, uint32x4_t b); // VQADD.U32 q0,q0,q0
uint64x2_t vqaddq_u64(uint64x2_t a, uint64x2_t b); // VQADD.U64 q0,q0,q0

高位半部分向量加法:- > Vr[i]:=Va[i]+Vb[i]

int8x8_t vaddhn_s16(int16x8_t a, int16x8_t b); // VADDHN.I16 d0,q0,q0
int16x4_t vaddhn_s32(int32x4_t a, int32x4_t b); // VADDHN.I32 d0,q0,q0
int32x2_t vaddhn_s64(int64x2_t a, int64x2_t b); // VADDHN.I64 d0,q0,q0
uint8x8_t vaddhn_u16(uint16x8_t a, uint16x8_t b); // VADDHN.I16 d0,q0,q0
uint16x4_t vaddhn_u32(uint32x4_t a, uint32x4_t b); // VADDHN.I32 d0,q0,q0
uint32x2_t vaddhn_u64(uint64x2_t a, uint64x2_t b); // VADDHN.I64 d0,q0,q0

高位半部分向量舍入加法

int8x8_t vraddhn_s16(int16x8_t a, int16x8_t b); // VRADDHN.I16 d0,q0,q0
int16x4_t vraddhn_s32(int32x4_t a, int32x4_t b); // VRADDHN.I32 d0,q0,q0
int32x2_t vraddhn_s64(int64x2_t a, int64x2_t b); // VRADDHN.I64 d0,q0,q0
uint8x8_t vraddhn_u16(uint16x8_t a, uint16x8_t b); // VRADDHN.I16 d0,q0,q0
uint16x4_t vraddhn_u32(uint32x4_t a, uint32x4_t b); // VRADDHN.I32 d0,q0,q0
uint32x2_t vraddhn_u64(uint64x2_t a, uint64x2_t b); // VRADDHN.I64 d0,q0,q0

向量减法

正常向量减法 vsub -> Vr[i]:=Va[i]-Vb[i]

//64bits
int8x8_t vsub_s8(int8x8_t a, int8x8_t b); // VSUB.I8 d0,d0,d0
int16x4_t vsub_s16(int16x4_t a, int16x4_t b); // VSUB.I16 d0,d0,d0
int32x2_t vsub_s32(int32x2_t a, int32x2_t b); // VSUB.I32 d0,d0,d0
int64x1_t vsub_s64(int64x1_t a, int64x1_t b); // VSUB.I64 d0,d0,d0
float32x2_t vsub_f32(float32x2_t a, float32x2_t b); // VSUB.F32 d0,d0,d0
uint8x8_t vsub_u8(uint8x8_t a, uint8x8_t b);        // VSUB.I8 d0,d0,d0
uint16x4_t vsub_u16(uint16x4_t a, uint16x4_t b);    // VSUB.I16 d0,d0,d0
uint32x2_t vsub_u32(uint32x2_t a, uint32x2_t b);    // VSUB.I32 d0,d0,d0
uint64x1_t vsub_u64(uint64x1_t a, uint64x1_t b);    // VSUB.I64 d0,d0,d0
//128bits
int8x16_t vsubq_s8(int8x16_t a, int8x16_t b); // VSUB.I8 q0,q0,q0
int16x8_t vsubq_s16(int16x8_t a, int16x8_t b); // VSUB.I16 q0,q0,q0
int32x4_t vsubq_s32(int32x4_t a, int32x4_t b); // VSUB.I32 q0,q0,q0
int64x2_t vsubq_s64(int64x2_t a, int64x2_t b); // VSUB.I64 q0,q0,q0
float32x4_t vsubq_f32(float32x4_t a, float32x4_t b); // VSUB.F32 q0,q0,q0
uint8x16_t vsubq_u8(uint8x16_t a, uint8x16_t b); // VSUB.I8 q0,q0,q0
uint16x8_t vsubq_u16(uint16x8_t a, uint16x8_t b); // VSUB.I16 q0,q0,q0
uint32x4_t vsubq_u32(uint32x4_t a, uint32x4_t b); // VSUB.I32 q0,q0,q0
uint64x2_t vsubq_u64(uint64x2_t a, uint64x2_t b); // VSUB.I64 q0,q0,q0

向量长型减法:vsubl -> Vr[i]:=Va[i]-Vb[i]

int16x8_t vsubl_s8(int8x8_t a, int8x8_t b); // VSUBL.S8 q0,d0,d0
int32x4_t vsubl_s16(int16x4_t a, int16x4_t b); // VSUBL.S16 q0,d0,d0
int64x2_t vsubl_s32(int32x2_t a, int32x2_t b); // VSUBL.S32 q0,d0,d0
uint16x8_t vsubl_u8(uint8x8_t a, uint8x8_t b); // VSUBL.U8 q0,d0,d0
uint32x4_t vsubl_u16(uint16x4_t a, uint16x4_t b); // VSUBL.U16 q0,d0,d0
uint64x2_t vsubl_u32(uint32x2_t a, uint32x2_t b); // VSUBL.U32 q0,d0,d0

向量宽型减法:vsubw -> Vr[i]:=Va[i]+Vb[i]

int16x8_t vsubw_s8(int16x8_t a, int8x8_t b); // VSUBW.S8 q0,q0,d0
int32x4_t vsubw_s16(int32x4_t a, int16x4_t b); // VSUBW.S16 q0,q0,d0
int64x2_t vsubw_s32(int64x2_t a, int32x2_t b); // VSUBW.S32 q0,q0,d0
uint16x8_t vsubw_u8(uint16x8_t a, uint8x8_t b); // VSUBW.U8 q0,q0,d0
uint32x4_t vsubw_u16(uint32x4_t a, uint16x4_t b); // VSUBW.U16 q0,q0,d0
uint64x2_t vsubw_u32(uint64x2_t a, uint32x2_t b); // VSUBW.U32 q0,q0,d0

向量饱和减法 vqsub-> Vr[i]:=sat(Va[i]-Vb[i])

//64bits
int8x8_t vqsub_s8(int8x8_t a, int8x8_t b); // VQSUB.S8 d0,d0,d0
int16x4_t vqsub_s16(int16x4_t a, int16x4_t b); // VQSUB.S16 d0,d0,d0
int32x2_t vqsub_s32(int32x2_t a, int32x2_t b); // VQSUB.S32 d0,d0,d0
int64x1_t vqsub_s64(int64x1_t a, int64x1_t b); // VQSUB.S64 d0,d0,d0
uint8x8_t vqsub_u8(uint8x8_t a, uint8x8_t b); // VQSUB.U8 d0,d0,d0
uint16x4_t vqsub_u16(uint16x4_t a, uint16x4_t b); // VQSUB.U16 d0,d0,d0
uint32x2_t vqsub_u32(uint32x2_t a, uint32x2_t b); // VQSUB.U32 d0,d0,d0
uint64x1_t vqsub_u64(uint64x1_t a, uint64x1_t b); // VQSUB.U64 d0,d0,d0
//128bits
int8x16_t vqsubq_s8(int8x16_t a, int8x16_t b); // VQSUB.S8 q0,q0,q0
int16x8_t vqsubq_s16(int16x8_t a, int16x8_t b); // VQSUB.S16 q0,q0,q0
int32x4_t vqsubq_s32(int32x4_t a, int32x4_t b); // VQSUB.S32 q0,q0,q0
int64x2_t vqsubq_s64(int64x2_t a, int64x2_t b); // VQSUB.S64 q0,q0,q0
uint8x16_t vqsubq_u8(uint8x16_t a, uint8x16_t b); // VQSUB.U8 q0,q0,q0
uint16x8_t vqsubq_u16(uint16x8_t a, uint16x8_t b); // VQSUB.U16 q0,q0,q0
uint32x4_t vqsubq_u32(uint32x4_t a, uint32x4_t b); // VQSUB.U32 q0,q0,q0
uint64x2_t vqsubq_u64(uint64x2_t a, uint64x2_t b); // VQSUB.U64 q0,q0,q0

向量半减Vr[i]:=(Va[i]-Vb[i])>>1

int8x8_t vhsub_s8(int8x8_t a, int8x8_t b); // VHSUB.S8 d0,d0,d0
int16x4_t vhsub_s16(int16x4_t a, int16x4_t b); // VHSUB.S16 d0,d0,d0
int32x2_t vhsub_s32(int32x2_t a, int32x2_t b); // VHSUB.S32 d0,d0,d0
uint8x8_t vhsub_u8(uint8x8_t a, uint8x8_t b); // VHSUB.U8 d0,d0,d0
uint16x4_t vhsub_u16(uint16x4_t a, uint16x4_t b); // VHSUB.U16 d0,d0,d0
uint32x2_t vhsub_u32(uint32x2_t a, uint32x2_t b); // VHSUB.U32 d0,d0,d0
int8x16_t vhsubq_s8(int8x16_t a, int8x16_t b); // VHSUB.S8 q0,q0,q0
int16x8_t vhsubq_s16(int16x8_t a, int16x8_t b); // VHSUB.S16 q0,q0,q0
int32x4_t vhsubq_s32(int32x4_t a, int32x4_t b); // VHSUB.S32 q0,q0,q0
uint8x16_t vhsubq_u8(uint8x16_t a, uint8x16_t b); // VHSUB.U8 q0,q0,q0
uint16x8_t vhsubq_u16(uint16x8_t a, uint16x8_t b); // VHSUB.U16 q0,q0,q0
uint32x4_t vhsubq_u32(uint32x4_t a, uint32x4_t b); // VHSUB.U32 q0,q0,q0

乘法

向量乘法:vmul -> Vr[i] := Va[i] * Vb[i]

//64bits===
int8x8_t vmul_s8(int8x8_t a, int8x8_t b); // VMUL.I8 d0,d0,d0
int16x4_t vmul_s16(int16x4_t a, int16x4_t b); // VMUL.I16 d0,d0,d0
int32x2_t vmul_s32(int32x2_t a, int32x2_t b); // VMUL.I32 d0,d0,d0
float32x2_t vmul_f32(float32x2_t a, float32x2_t b); // VMUL.F32 d0,d0,d0
uint8x8_t vmul_u8(uint8x8_t a, uint8x8_t b); // VMUL.I8 d0,d0,d0
uint16x4_t vmul_u16(uint16x4_t a, uint16x4_t b); // VMUL.I16 d0,d0,d0
uint32x2_t vmul_u32(uint32x2_t a, uint32x2_t b); // VMUL.I32 d0,d0,d0
poly8x8_t vmul_p8(poly8x8_t a, poly8x8_t b); // VMUL.P8 d0,d0,d0
//128bits==
int8x16_t vmulq_s8(int8x16_t a, int8x16_t b); // VMUL.I8 q0,q0,q0
int16x8_t vmulq_s16(int16x8_t a, int16x8_t b); // VMUL.I16 q0,q0,q0
int32x4_t vmulq_s32(int32x4_t a, int32x4_t b); // VMUL.I32 q0,q0,q0
float32x4_t vmulq_f32(float32x4_t a, float32x4_t b); // VMUL.F32 q0,q0,q0
uint8x16_t vmulq_u8(uint8x16_t a, uint8x16_t b); // VMUL.I8 q0,q0,q0
uint16x8_t vmulq_u16(uint16x8_t a, uint16x8_t b); // VMUL.I16 q0,q0,q0
uint32x4_t vmulq_u32(uint32x4_t a, uint32x4_t b); // VMUL.I32 q0,q0,q0
poly8x16_t vmulq_p8(poly8x16_t a, poly8x16_t b); // VMUL.P8 q0,q0,q0

向量长型乘法:vmull -> Vr[i] := Va[i] * Vb[i]

int16x8_t vmull_s8(int8x8_t a, int8x8_t b); // VMULL.S8 q0,d0,d0
int32x4_t vmull_s16(int16x4_t a, int16x4_t b); // VMULL.S16 q0,d0,d0
int64x2_t vmull_s32(int32x2_t a, int32x2_t b); // VMULL.S32 q0,d0,d0
uint16x8_t vmull_u8(uint8x8_t a, uint8x8_t b); // VMULL.U8 q0,d0,d0

向量乘加:vmla -> Vr[i] := Va[i] + Vb[i] * Vc[i]

//64bits===
int8x8_t vmla_s8(int8x8_t a, int8x8_t b, int8x8_t c); // VMLA.I8 d0,d0,d0
int16x4_t vmla_s16(int16x4_t a, int16x4_t b, int16x4_t c); // VMLA.I16 d0,d0,d0
int32x2_t vmla_s32(int32x2_t a, int32x2_t b, int32x2_t c); // VMLA.I32 d0,d0,d0
float32x2_t vmla_f32(float32x2_t a, float32x2_t b, float32x2_t c); // VMLA.F32 d0,d0,d0
uint8x8_t vmla_u8(uint8x8_t a, uint8x8_t b, uint8x8_t c); // VMLA.I8 d0,d0,d0
uint16x4_t vmla_u16(uint16x4_t a, uint16x4_t b, uint16x4_t c); // VMLA.I16 d0,d0,d0
uint32x2_t vmla_u32(uint32x2_t a, uint32x2_t b, uint32x2_t c); // VMLA.I32 d0,d0,d0
//128bits==
int8x16_t vmlaq_s8(int8x16_t a, int8x16_t b, int8x16_t c); // VMLA.I8 q0,q0,q0
int16x8_t vmlaq_s16(int16x8_t a, int16x8_t b, int16x8_t c); // VMLA.I16 q0,q0,q0
int32x4_t vmlaq_s32(int32x4_t a, int32x4_t b, int32x4_t c); // VMLA.I32 q0,q0,q0
float32x4_t vmlaq_f32(float32x4_t a, float32x4_t b, float32x4_t c); // VMLA.F32 q0,q0,q0
uint8x16_t vmlaq_u8(uint8x16_t a, uint8x16_t b, uint8x16_t c); // VMLA.I8 q0,q0,q0
uint16x8_t vmlaq_u16(uint16x8_t a, uint16x8_t b, uint16x8_t c); // VMLA.I16 q0,q0,q0
uint32x4_t vmlaq_u32(uint32x4_t a, uint32x4_t b, uint32x4_t c); // VMLA.I32 q0,q0,q0

向量长型乘加:vmlal -> Vr[i] := Va[i] + Vb[i] * Vc[i]

int16x8_t vmlal_s8(int16x8_t a, int8x8_t b, int8x8_t c); // VMLAL.S8 q0,d0,d0
int32x4_t vmlal_s16(int32x4_t a, int16x4_t b, int16x4_t c); // VMLAL.S16 q0,d0,d0
int64x2_t vmlal_s32(int64x2_t a, int32x2_t b, int32x2_t c); // VMLAL.S32 q0,d0,d0
uint16x8_t vmlal_u8(uint16x8_t a, uint8x8_t b, uint8x8_t c); // VMLAL.U8 q0,d0,d0
uint32x4_t vmlal_u16(uint32x4_t a, uint16x4_t b, uint16x4_t c); // VMLAL.U16 q0,d0,d0
uint64x2_t vmlal_u32(uint64x2_t a, uint32x2_t b, uint32x2_t c); // VMLAL.U32 q0,d0,d0

向量乘减:vmls -> Vr[i] := Va[i] - Vb[i] * Vc[i]

//64bits==
int8x8_t vmls_s8(int8x8_t a, int8x8_t b, int8x8_t c); // VMLS.I8 d0,d0,d0
int16x4_t vmls_s16(int16x4_t a, int16x4_t b, int16x4_t c); // VMLS.I16 d0,d0,d0
int32x2_t vmls_s32(int32x2_t a, int32x2_t b, int32x2_t c); // VMLS.I32 d0,d0,d0
float32x2_t vmls_f32(float32x2_t a, float32x2_t b, float32x2_t c); // VMLS.F32 d0,d0,d0
uint8x8_t vmls_u8(uint8x8_t a, uint8x8_t b, uint8x8_t c); // VMLS.I8 d0,d0,d0
uint16x4_t vmls_u16(uint16x4_t a, uint16x4_t b, uint16x4_t c); // VMLS.I16 d0,d0,d0
uint32x2_t vmls_u32(uint32x2_t a, uint32x2_t b, uint32x2_t c); // VMLS.I32 d0,d0,d0
//128bits==
int8x16_t vmlsq_s8(int8x16_t a, int8x16_t b, int8x16_t c); // VMLS.I8 q0,q0,q0
int16x8_t vmlsq_s16(int16x8_t a, int16x8_t b, int16x8_t c); // VMLS.I16 q0,q0,q0
int32x4_t vmlsq_s32(int32x4_t a, int32x4_t b, int32x4_t c); // VMLS.I32 q0,q0,q0
float32x4_t vmlsq_f32(float32x4_t a, float32x4_t b, float32x4_t c); // VMLS.F32 q0,q0,q0
uint8x16_t vmlsq_u8(uint8x16_t a, uint8x16_t b, uint8x16_t c); // VMLS.I8 q0,q0,q0
uint16x8_t vmlsq_u16(uint16x8_t a, uint16x8_t b, uint16x8_t c); // VMLS.I16 q0,q0,q0
uint32x4_t vmlsq_u32(uint32x4_t a, uint32x4_t b, uint32x4_t c); // VMLS.I32 q0,q0,q0

向量长型乘减 vmlsl -> Vr[i] := Va[i] - Vb[i] * Vc[i]

int16x8_t vmlsl_s8(int16x8_t a, int8x8_t b, int8x8_t c); // VMLSL.S8 q0,d0,d0
int32x4_t vmlsl_s16(int32x4_t a, int16x4_t b, int16x4_t c); // VMLSL.S16 q0,d0,d0
int64x2_t vmlsl_s32(int64x2_t a, int32x2_t b, int32x2_t c); // VMLSL.S32 q0,d0,d0
uint16x8_t vmlsl_u8(uint16x8_t a, uint8x8_t b, uint8x8_t c); // VMLSL.U8 q0,d0,d0
uint32x4_t vmlsl_u16(uint32x4_t a, uint16x4_t b, uint16x4_t c); // VMLSL.U16 q0,d0,d0
uint64x2_t vmlsl_u32(uint64x2_t a, uint32x2_t b, uint32x2_t c); // VMLSL.U32 q0,d0,d0

比较compare

提供一系列比较内在函数。如果对于一条向量线比较结果为 true,则该向量线的结果为将所有位设置为一。如果对于一条向量线比较结果为 false,则将所有位设置为零。返回类型是无符号整数类型。这意味着可以将比较结果用作 vbsl内在函数的第一个参数。

向量比较 等于否 vceq_type vceqq_type compare equal

// 64位
uint8x8_t vceq_s8(int8x8_t a, int8x8_t b); // VCEQ.I8 d0, d0, d0
uint16x4_t vceq_s16(int16x4_t a, int16x4_t b); // VCEQ.I16 d0, d0, d0
uint32x2_t vceq_s32(int32x2_t a, int32x2_t b); // VCEQ.I32 d0, d0, d0
uint32x2_t vceq_f32(float32x2_t a, float32x2_t b); // VCEQ.F32 d0, d0, d0
uint8x8_t vceq_u8(uint8x8_t a, uint8x8_t b); // VCEQ.I8 d0, d0, d0
uint16x4_t vceq_u16(uint16x4_t a, uint16x4_t b); // VCEQ.I16 d0, d0, d0
uint32x2_t vceq_u32(uint32x2_t a, uint32x2_t b); // VCEQ.I32 d0, d0, d0
uint8x8_t vceq_p8(poly8x8_t a, poly8x8_t b); // VCEQ.I8 d0, d0, d0
// 128位
uint8x16_t vceqq_s8(int8x16_t a, int8x16_t b); // VCEQ.I8 q0, q0, q0
uint16x8_t vceqq_s16(int16x8_t a, int16x8_t b); // VCEQ.I16 q0, q0, q0
uint32x4_t vceqq_s32(int32x4_t a, int32x4_t b); // VCEQ.I32 q0, q0, q0
uint32x4_t vceqq_f32(float32x4_t a, float32x4_t b); // VCEQ.F32 q0, q0, q0
uint8x16_t vceqq_u8(uint8x16_t a, uint8x16_t b); // VCEQ.I8 q0, q0, q0
uint16x8_t vceqq_u16(uint16x8_t a, uint16x8_t b); // VCEQ.I16 q0, q0, q0
uint32x4_t vceqq_u32(uint32x4_t a, uint32x4_t b); // VCEQ.I32 q0, q0, q0
uint8x16_t vceqq_p8(poly8x16_t a, poly8x16_t b); // VCEQ.I8 q0, q0, q0

向量比较大于或等于 vcge vcgeq : compare greate or equal

// 64位
uint8x8_t vcge_s8(int8x8_t a, int8x8_t b); // VCGE.S8 d0, d0, d0
uint16x4_t vcge_s16(int16x4_t a, int16x4_t b); // VCGE.S16 d0, d0, d0
uint32x2_t vcge_s32(int32x2_t a, int32x2_t b); // VCGE.S32 d0, d0, d0
uint32x2_t vcge_f32(float32x2_t a, float32x2_t b); // VCGE.F32 d0, d0, d0
uint8x8_t vcge_u8(uint8x8_t a, uint8x8_t b); // VCGE.U8 d0, d0, d0
uint16x4_t vcge_u16(uint16x4_t a, uint16x4_t b); // VCGE.U16 d0, d0, d0
uint32x2_t vcge_u32(uint32x2_t a, uint32x2_t b); // VCGE.U32 d0, d0, d0

// 128位
uint8x16_t vcgeq_s8(int8x16_t a, int8x16_t b); // VCGE.S8 q0, q0, q0
uint16x8_t vcgeq_s16(int16x8_t a, int16x8_t b); // VCGE.S16 q0, q0, q0
uint32x4_t vcgeq_s32(int32x4_t a, int32x4_t b); // VCGE.S32 q0, q0, q0
uint32x4_t vcgeq_f32(float32x4_t a, float32x4_t b); // VCGE.F32 q0, q0, q0
uint8x16_t vcgeq_u8(uint8x16_t a, uint8x16_t b); // VCGE.U8 q0, q0, q0
uint16x8_t vcgeq_u16(uint16x8_t a, uint16x8_t b); // VCGE.U16 q0, q0, q0
uint32x4_t vcgeq_u32(uint32x4_t a, uint32x4_t b); // VCGE.U32 q0, q0, q0

向量比较小于或等于 vcle vcleq : compare little or equal

//64bits
uint8x8_t vcle_s8(int8x8_t a, int8x8_t b); // VCGE.S8 d0, d0, d0
uint16x4_t vcle_s16(int16x4_t a, int16x4_t b); // VCGE.S16 d0, d0, d0
uint32x2_t vcle_s32(int32x2_t a, int32x2_t b); // VCGE.S32 d0, d0, d0
uint32x2_t vcle_f32(float32x2_t a, float32x2_t b); // VCGE.F32 d0, d0, d0
uint8x8_t vcle_u8(uint8x8_t a, uint8x8_t b); // VCGE.U8 d0, d0, d0
uint16x4_t vcle_u16(uint16x4_t a, uint16x4_t b); // VCGE.U16 d0, d0, d0
uint32x2_t vcle_u32(uint32x2_t a, uint32x2_t b); // VCGE.U32 d0, d0, d0
// 128bits
uint8x16_t vcleq_s8(int8x16_t a, int8x16_t b); // VCGE.S8 q0, q0, q0
uint16x8_t vcleq_s16(int16x8_t a, int16x8_t b); // VCGE.S16 q0, q0, q0
uint32x4_t vcleq_s32(int32x4_t a, int32x4_t b); // VCGE.S32 q0, q0, q0
uint32x4_t vcleq_f32(float32x4_t a, float32x4_t b); // VCGE.F32 q0, q0, q0
uint8x16_t vcleq_u8(uint8x16_t a, uint8x16_t b); // VCGE.U8 q0, q0, q0
uint16x8_t vcleq_u16(uint16x8_t a, uint16x8_t b); // VCGE.U16 q0, q0, q0
uint32x4_t vcleq_u32(uint32x4_t a, uint32x4_t b); // VCGE.U32 q0, q0, q0

**向量比较大于 vcgt vcgtq compare great **

// 64bits
uint8x8_t vcgt_s8(int8x8_t a, int8x8_t b); // VCGT.S8 d0, d0, d0
uint16x4_t vcgt_s16(int16x4_t a, int16x4_t b); // VCGT.S16 d0, d0, d0
uint32x2_t vcgt_s32(int32x2_t a, int32x2_t b); // VCGT.S32 d0, d0, d0
uint32x2_t vcgt_f32(float32x2_t a, float32x2_t b); // VCGT.F32 d0, d0, d0
uint8x8_t vcgt_u8(uint8x8_t a, uint8x8_t b); // VCGT.U8 d0, d0, d0
uint16x4_t vcgt_u16(uint16x4_t a, uint16x4_t b); // VCGT.U16 d0, d0, d0
uint32x2_t vcgt_u32(uint32x2_t a, uint32x2_t b); // VCGT.U32 d0, d0, d0
// 128bits
uint8x16_t vcgtq_s8(int8x16_t a, int8x16_t b); // VCGT.S8 q0, q0, q0
uint16x8_t vcgtq_s16(int16x8_t a, int16x8_t b); // VCGT.S16 q0, q0, q0
uint32x4_t vcgtq_s32(int32x4_t a, int32x4_t b); // VCGT.S32 q0, q0, q0
uint32x4_t vcgtq_f32(float32x4_t a, float32x4_t b); // VCGT.F32 q0, q0, q0
uint8x16_t vcgtq_u8(uint8x16_t a, uint8x16_t b); // VCGT.U8 q0, q0, q0
uint16x8_t vcgtq_u16(uint16x8_t a, uint16x8_t b); // VCGT.U16 q0, q0, q0
uint32x4_t vcgtq_u32(uint32x4_t a, uint32x4_t b); // VCGT.U32 q0, q0, q0

**向量比较小于 vclt vcltq : compare little **

//64bits==
uint8x8_t vclt_s8(int8x8_t a, int8x8_t b); // VCGT.S8 d0, d0, d0
uint16x4_t vclt_s16(int16x4_t a, int16x4_t b); // VCGT.S16 d0, d0, d0
uint32x2_t vclt_s32(int32x2_t a, int32x2_t b); // VCGT.S32 d0, d0, d0
uint32x2_t vclt_f32(float32x2_t a, float32x2_t b); // VCGT.F32 d0, d0, d0
uint8x8_t vclt_u8(uint8x8_t a, uint8x8_t b); // VCGT.U8 d0, d0, d0
uint16x4_t vclt_u16(uint16x4_t a, uint16x4_t b); // VCGT.U16 d0, d0, d0
uint32x2_t vclt_u32(uint32x2_t a, uint32x2_t b); // VCGT.U32 d0, d0, d0
// 128bits===
uint8x16_t vcltq_s8(int8x16_t a, int8x16_t b); // VCGT.S8 q0, q0, q0
uint16x8_t vcltq_s16(int16x8_t a, int16x8_t b); // VCGT.S16 q0, q0, q0
uint32x4_t vcltq_s32(int32x4_t a, int32x4_t b); // VCGT.S32 q0, q0, q0
uint32x4_t vcltq_f32(float32x4_t a, float32x4_t b); // VCGT.F32 q0, q0, q0
uint8x16_t vcltq_u8(uint8x16_t a, uint8x16_t b); // VCGT.U8 q0, q0, q0
uint16x8_t vcltq_u16(uint16x8_t a, uint16x8_t b); // VCGT.U16 q0, q0, q0
uint32x4_t vcltq_u32(uint32x4_t a, uint32x4_t b); // VCGT.U32 q0, q0, q0

向量绝对值比较大于或等于 vcage vcageq: compare abs great equal


uint32x2_t vcage_f32(float32x2_t a, float32x2_t b); // VACGE.F32 d0, d0, d0
uint32x4_t vcageq_f32(float32x4_t a, float32x4_t b); // VACGE.F32 q0, q0, q0

**向量绝对值比较小于或等于 vcale vcaleq: compare abs little equal **

uint32x2_t vcale_f32(float32x2_t a, float32x2_t b); // VACGE.F32 d0, d0, d0
uint32x4_t vcaleq_f32(float32x4_t a, float32x4_t b); // VACGE.F32 q0, q0, q0

向量绝对值比较大于 vcagt vcagtq: compare abs great

uint32x2_t vcagt_f32(float32x2_t a, float32x2_t b); // VACGT.F32 d0, d0, d0
uint32x4_t vcagtq_f32(float32x4_t a, float32x4_t b); // VACGT.F32 q0, q0, q0

向量绝对值比较小于 vcalt vcaltq:compare abs little

uint32x2_t vcalt_f32(float32x2_t a, float32x2_t b); // VACGT.F32 d0, d0, d0
uint32x4_t vcaltq_f32(float32x4_t a, float32x4_t b); // VACGT.F32 q0, q0, q0

向量测试位 test

uint8x8_t vtst_s8(int8x8_t a, int8x8_t b); // VTST.8 d0, d0, d0
uint16x4_t vtst_s16(int16x4_t a, int16x4_t b); // VTST.16 d0, d0, d0
uint32x2_t vtst_s32(int32x2_t a, int32x2_t b); // VTST.32 d0, d0, d0
uint8x8_t vtst_u8(uint8x8_t a, uint8x8_t b); // VTST.8 d0, d0, d0
uint16x4_t vtst_u16(uint16x4_t a, uint16x4_t b); // VTST.16 d0, d0, d0
uint32x2_t vtst_u32(uint32x2_t a, uint32x2_t b); // VTST.32 d0, d0, d0
uint8x8_t vtst_p8(poly8x8_t a, poly8x8_t b); // VTST.8 d0, d0, d0

uint8x16_t vtstq_s8(int8x16_t a, int8x16_t b); // VTST.8 q0, q0, q0
uint16x8_t vtstq_s16(int16x8_t a, int16x8_t b); // VTST.16 q0, q0, q0
uint32x4_t vtstq_s32(int32x4_t a, int32x4_t b); // VTST.32 q0, q0, q0
uint8x16_t vtstq_u8(uint8x16_t a, uint8x16_t b); // VTST.8 q0, q0, q0
uint16x8_t vtstq_u16(uint16x8_t a, uint16x8_t b); // VTST.16 q0, q0, q0
uint32x4_t vtstq_u32(uint32x4_t a, uint32x4_t b); // VTST.32 q0, q0, q0
uint8x16_t vtstq_p8(poly8x16_t a, poly8x16_t b); // VTST.8 q0, q0, q0

差值绝对值

参数间的差值绝对值:Vr[i] = | Va[i] - Vb[i] | vabd: abs difference

int8x8_t vabd_s8(int8x8_t a, int8x8_t b); // VABD.S8 d0,d0,d0
int16x4_t vabd_s16(int16x4_t a, int16x4_t b); // VABD.S16 d0,d0,d0
int32x2_t vabd_s32(int32x2_t a, int32x2_t b); // VABD.S32 d0,d0,d0
uint8x8_t vabd_u8(uint8x8_t a, uint8x8_t b); // VABD.U8 d0,d0,d0
uint16x4_t vabd_u16(uint16x4_t a, uint16x4_t b); // VABD.U16 d0,d0,d0
uint32x2_t vabd_u32(uint32x2_t a, uint32x2_t b); // VABD.U32 d0,d0,d0
float32x2_t vabd_f32(float32x2_t a, float32x2_t b); // VABD.F32 d0,d0,d0
// 128bits
int8x16_t vabdq_s8(int8x16_t a, int8x16_t b); // VABD.S8 q0,q0,q0
int16x8_t vabdq_s16(int16x8_t a, int16x8_t b); // VABD.S16 q0,q0,q0
int32x4_t vabdq_s32(int32x4_t a, int32x4_t b); // VABD.S32 q0,q0,q0
uint8x16_t vabdq_u8(uint8x16_t a, uint8x16_t b); // VABD.U8 q0,q0,q0
uint16x8_t vabdq_u16(uint16x8_t a, uint16x8_t b); // VABD.U16 q0,q0,q0
uint32x4_t vabdq_u32(uint32x4_t a, uint32x4_t b); // VABD.U32 q0,q0,q0
float32x4_t vabdq_f32(float32x4_t a, float32x4_t b); // VABD.F32 q0,q0,q0

**差值绝对值 - 长型 **

int16x8_t vabdl_s8(int8x8_t a, int8x8_t b); // VABDL.S8 q0,d0,d0
int32x4_t vabdl_s16(int16x4_t a, int16x4_t b); // VABDL.S16 q0,d0,d0
int64x2_t vabdl_s32(int32x2_t a, int32x2_t b); // VABDL.S32 q0,d0,d0
uint16x8_t vabdl_u8(uint8x8_t a, uint8x8_t b); // VABDL.U8 q0,d0,d0
uint32x4_t vabdl_u16(uint16x4_t a, uint16x4_t b); // VABDL.U16 q0,d0,d0
uint64x2_t vabdl_u32(uint32x2_t a, uint32x2_t b); // VABDL.U32 q0,d0,d0

加载存储指令

加载并存储单个向量 加载并存储某类型的单个向量。vld1q_type


实例0:数组元素求和

// c版本=======================
#include <iostream>
using namespace std;

float sum_array(float *arr, int len)
{
    if(NULL == arr || len < 1)
    {
        cout<<"input error\n";
        return 0;
    }
    float sum(0.0);
    for(int i=0; i<len; ++i)
    {
        sum += *arr++;
    }
    return sum;
}


// arm intrinsics==============
#include <iostream>
#include <arm_neon.h> //需包含的头文件
using namespace std;

float sum_array(float *arr, int len)
{
    if(NULL == arr || len < 1)
    {
        cout<<"input error\n";
        return 0;
    }

    int dim4 = len >> 2; // 数组长度除4整数
    int left4 = len & 3; // 数组长度除4余数,不够4的剩下的
    
    float32x4_t sum_vec = vdupq_n_f32(0.0);//定义用于暂存累加结果的寄存器且初始化为0
    for (; dim4>0; dim4--, arr+=4) //每次同时访问4个数组元素
    {
        float32x4_t data_vec = vld1q_f32(arr); //依次取4个元素存入寄存器vec
        sum_vec = vaddq_f32(sum_vec, data_vec);//ri = ai + bi 计算两组寄存器对应元素之和并存放到相应结果
    }
    float sum = vgetq_lane_f32(sum_vec, 0)+vgetq_lane_f32(sum_vec, 1)+vgetq_lane_f32(sum_vec, 2)+vgetq_lane_f32(sum_vec, 3);//将累加结果寄存器中的所有元素相加得到最终累加值
    for (; left4>0; left4--, arr++)
        sum += (*arr) ;   //对于剩下的少于4的数字,依次计算累加即可
    return sum;
}

上述算法的时间复杂度时O(N/4)
从上面的例子看出,使用NEON函数很简单,只需要将依次处理,变为批处理(如上面的每次处理4个)。

上面用到的函数有:
float32x4_t vdupq_n_f32 (float32_t value)
将value复制4分存到返回的寄存器中

float32x4_t vld1q_f32 (float32_t const * ptr)
从数组中依次Load4个元素存到寄存器中

相应的 有void vst1q_f32 (float32_t * ptr, float32x4_t val)
将寄存器中的值写入数组中

float32x4_t vaddq_f32 (float32x4_t a, float32x4_t b)
返回两个寄存器对应元素之和 r = a+b

相应的 有float32x4_t vsubq_f32 (float32x4_t a, float32x4_t b)
返回两个寄存器对应元素之差 r = a-b

float32_t vgetq_lane_f32 (float32x4_t v, const int lane)
返回寄存器某一lane的值

其他常用的函数还有:

float32x4_t vmulq_f32 (float32x4_t a, float32x4_t b)
返回两个寄存器对应元素之积 r = a*b

float32x4_t vmlaq_f32 (float32x4_t a, float32x4_t b, float32x4_t c)
乘加 r = a +b*c

float32x4_t vmlsq_f32 (float32x4_t a, float32x4_t b, float32x4_t c)
乘减 r = a - b*c

float32x4_t vextq_f32 (float32x4_t a, float32x4_t b, const int n)
拼接两个寄存器并返回从第n位开始的大小为4的寄存器 0<=n<=3
例如

a: 1 2 3 4 
b: 5 6 7 8 
vextq_f32(a,b,1) -> r: 2 3 4 5 
vextq_f32(a,b,2) -> r: 3 4 5 6 
vextq_f32(a,b,3) -> r: 4 5 6 7
float32x4_t sum = vdupq_n_f32(0); // sum四个通道全部赋值为0,sum={0,0,0,0}
float _a[] = {1,2,3,4}, _b[] = {5,6,7,8} ;
float32x4_t a = vld1q_f32(_a), b = vld1q_f32(_b)  ;// 载入两个数组元素到 两个寄存器

//a的元素乘以b的第几个通道元素,然后后面的累加
float32x4_t sum1 = vfmaq_laneq_f32(sum, a, b, 0);  // sum1={5,10,15,20}
float32x4_t sum2 = vfmaq_laneq_f32(sum1, a, b, 1); 
// sum2={5,10,15,20}+{6,12,18,24} = {11,22,33,44}

float32x4_t sum3 = vfmaq_laneq_f32(sum2, a, b, 2);
// sum3={11,22,33,44}+{7,14,21,28} = {18,36,54,72}

官方文档 其他常用函数

示例1:向量加法**

// 假设 count 是4的倍数
#include<arm_neon.h>

// C version
void add_int_c(int* dst, int* src1, int* src2, int count)
{
	int i;
	for (i = 0; i < count; i++)
		{
		    dst[i] = src1[i] + src2[i];
		}
}

// NEON version
void add_float_neon1(int* dst, 
                     int* src1, 
		     int* src2, // 传入三个数据单元的指针(地址)
		     int count) // 数据量 假设为4的倍数
{
	int i;
	for (i = 0; i < count; i += 4) // 寄存器操作每次 进行4个数据的运输(单指令多数据SIMD)
	{
		int32x4_t in1, in2, out;
		
		// 1. 从内存 载入 数据 到寄存器
		in1 = vld1q_s32(src1);// intrinsics传入的为内存数据指针
		                      // v 表示neon函数
				      // ld表示加载load
				      // q表示使用128位寄存器
				      // s32,有符号32位整数,单个数据32,共有4个数据并行超声
		src1 += 4;// 数据 指针 递增+4 
		
		in2 = vld1q_s32(src2);
		src2 += 4;
		
		// 2. 在寄存器中进行数据运算 加法add
		out = vaddq_s32(in1, in2);
		
		// 3. 将寄存器中的结果 保存到 内存地址中
		vst1q_s32(dst, out);
		dst += 4;// 
	}
	// 实际情况,需要做最后不够4个的数的运输,使用普通c函数部分进行
	// 可参考下面的代码进行改进
}


代码中的 vld1q_s32 会被编译器转换成 vld1.32 {d0, d1}, [r0] 指令,

同理 vaddq_s32 被转换成 vadd.i32 q0, q0, q0,

vst1q_s32 被转换成 vst1.32 {d0,d1}, [r0]。

示例2:向量乘法

//NRON优化的vector相乘
static void neon_vector_mul(
  const std::vector<float>& vec_a, // 向量a 常量引用
  const std::vector<float>& vec_b, // 向量b 常量引用 
  std::vector<float>& vec_result)  // 结果向量 引用
{
	assert(vec_a.size() == vec_b.size());
	assert(vec_a.size() == vec_result.size());
	int i = 0;// 向量索引 从0开始
  
	//neon process
	for (; i < (int)vec_result.size() - 3 ; i+=4)// 每一步会并行执行四个数(单指令多数据simd) 注意每次增加4
	{// 不够 4的部分留在后面用 普通 c代码运算
               // 从内存载入数据到寄存器
		const auto data_a = vld1q_f32(&vec_a[i]);// 函数传入的是 地址(指针)
		const auto data_b = vld1q_f32(&vec_b[i]);
    
		float* dst_ptr = &vec_result[i];// 结果向量的地址(内存中)
    
                // 在寄存器中进行运算,乘法 mulp 运算
		const auto data_res = vmulq_f32(data_a, data_b);
    
                // 将处于寄存器中的结果 保存传输到 内存中国
		vst1q_f32(dst_ptr, data_res);
	}
  
	// normal process 普通C代码 数据相乘= 剩余不够4个数的部分===可能为 1,2,3个数
	for (; i < (int)vec_result.size(); i++)
	{
		vec_result[i] = vec_a[i] * vec_b[i];
	}
}

处理剩余的元素
参考

    1. Larger Arrays 扩展成更大的数组

如果改变你要处理的数组大小,比如增加数组大小到向量大小的整数倍,这样就能在最后一次数据处理时也按照向量大小处理而不会把临近的数据损坏。如上面的例子里,把数组大小增加到24个元素,这样就能用NEON用3次迭代完成所有的数据处理而不会损坏周边数据。

填补数组到向量的整数个大小:

一些情况下,可能没法初始化填充的数据,无论填充什么都会影响计算的结果;

    1. Overlapping重叠计算

如果进行数据处理的操作合适的话,可以考虑把剩余部分的元素通过重叠计算的方式处理,这就会把某些重叠部分的元素计算两次。如下面的例子里,第一次迭代计算元素0到7,第一次计算5到12,第三次计算13到20。从而第一次计算和第二次计算重叠的元素5到7就被计算了两次。

重叠向量,在橙色区域的数据计算两次:
)

重叠处理只适用于需要处理的数组长度不会随着每次迭代而改变的情况,但不适用于每次迭代结果改变的情况,如累加计算,这样重叠部分的数据会被计算两次;

    1. 单个元素的计算过程Single Elements

NEON提供了能处理向量里的单一元素的加载和存储指令,用这些指令,你能加载包含一个元素的部分向量,处理它然后把结果保存到内存。如下面的例子,前两次的迭代处理跟前面类似,处理元素0到7以及8到15,剩下的5个元素可以在第三次迭代处理,加载处理并存储单一的元素。

处理单一的元素实例:

)

这种方法比前面的两种方法速度要慢,每个元素的处理都需要单独进行;

这种的剩余元素处理方法需要两个迭代循环,第一个处理向量的循环,还有处理剩余元素的循环,这会增加代码大小;

NEON的单一元素加载只改变目标元素的值,而保留其他的元素不变,如果你向量计算的指令会在一个向量间反复计算,如VPADD,这些寄存器需要在第一个元素加载时初始化。

    1. 或者剩余的单个元素直接使用C语言进行计算

示例3:从内存变量 加载数据 到 寄存器向量

#include <stdio.h>
#include <arm_neon.h>
unsigned short int A[] = {1,2,3,4}; 
    // 含有四个无符号短整型整数的数组 array with 4 elements
int main(void)
{
	uint16x4_t v;     // 4通道16位的向量declare a vector of four 16-bit lanes
	v = vld1_u16(A);  // 从数组加载到向量load the array from memory into a vector
	v = vadd_u16(v,v);// 每个元素加上自身,扩大一倍double each element in the vector
	vst1_u16(A, v);   // 存储结果回数组A store the vector back to memory
	return 0;
}

示例4:直接从数据创建vcreate_u8()寄存器变量

#include <arm_neon.h>
int main (void)
{
	uint8x8_t v;        // 定义一个8通道个8位数据的向量
	unsigned char A[8]; // 分配内存存储一个含有8个无符号字符数据的数组
	v = vcreate_u8(0x0102030405060708); // 创建一个8X8位向量,存储 1,2,3,4,5,6,7,8
	vst1_u8(A, v);      // 将向量数据 存储到内存
	return 0;
}

示例5:加载多个向量数据

#include <arm_neon.h>
int main (void)
{
	uint8x8x3_t v; // 定义一个包含3个向量的向量数组,每个向量为8通道8位无符号整形
	unsigned char A[24]; // 定义一个包含24个无符号字节数据的数组,表示24个像素
	v = vld3_u8(A);      // 从A处加载数据(多向量间隔加载)
	// v.val[0] 是第一个向量={A[0],A[3],A[6],A[9],A[12],A[15],A[18],A[21]},RGB红色通道
	// v.val[1] 是第二个向量={A[1],A[4],A[7],A[10],A[13],A[16],A[19],A[22]},RGB绿色通道
	// v.val[2] 是第三个向量={A[2],A[5],A[8],A[11],A[14],A[17],A[20],A[23]},RGB蓝色通道
	v.val[0] = vadd_u8(v.val[0],v.val[0]);// 红色通道数值加倍
	vst3_u8(A, v); // 在把使用向量处理后的数据,存回内存数组A中
	return 0;
}

vld3_u8:

vswp_u8: 交换R和B通道

vld1_u8:

加载和保存:

示例6:数组矩阵相乘

列主导4*4矩阵相乘:

细节-结果矩阵的产生:

结果矩阵的第一列:

A矩阵第一列和B矩阵第一列的第一个元素相乘 +
A矩阵第二列和B矩阵第一列的第二个元素相乘 +
A矩阵第三列和B矩阵第一列的第三个元素相乘 +
A矩阵第四列和B矩阵第一列的第四个元素相乘

void altneonmult(const float *matrixA, const float *matrixB, float *matrixR)
// matrixA \ matrixB \ matrixR均为 4*4 浮点数矩阵,列优先存储??
// 计算过程为 matrixR = matrixA * matrixB
{
	float32x4_t a, b0, b1, b2, b3, r;// 4通道32位浮点数  行row 列column
	a0 = vld1q_f32(matrixA);     /* A矩阵第一列 从内存地址加载数据,连续加载,4个32位共128位数据*/
	a1 = vld1q_f32(matrixA + 4); /* A矩阵第二列*/
	a2 = vld1q_f32(matrixA + 8); /* A矩阵第三列*/
	a3 = vld1q_f32(matrixA + 12); /* A矩阵第四列 */
	
// 结果矩阵的第一列
	b = vld1q_f32(matrixB); /* B矩阵第一列 */
	r = vmulq_lane_f32(a0, vget_low_f32(b), 0);     // A矩阵第一列 乘 B矩阵第一列的第一个元素
	r = vmlaq_lane_f32(r, a1, vget_low_f32(b), 1);  // 乘加
	r = vmlaq_lane_f32(r, a2, vget_high_f32(b), 0);
	r = vmlaq_lane_f32(r, a3, vget_high_f32(b), 1);
	vst1q_f32(matrixR, r); /* store col 0 of result */
// 结果矩阵的第二列
	b = vld1q_f32(matrixB + 4); /* B矩阵第二列 */
	r = vmulq_lane_f32(a0, vget_low_f32(b), 0);
	r = vmlaq_lane_f32(r, a1, vget_low_f32(b), 1);
	r = vmlaq_lane_f32(r, a2, vget_high_f32(b), 0);
	r = vmlaq_lane_f32(r, a3, vget_high_f32(b), 1);
	vst1q_f32(matrixR + 4, r); /* store col 1 of result */
// 结果矩阵的第三列
	b = vld1q_f32(matrixB + 8); /* B矩阵第三列 */
	r = vmulq_lane_f32(a0, vget_low_f32(b), 0);
	r = vmlaq_lane_f32(r, a1, vget_low_f32(b), 1);
	r = vmlaq_lane_f32(r, a2, vget_high_f32(b), 0);
	r = vmlaq_lane_f32(r, a3, vget_high_f32(b), 1);
	vst1q_f32(matrixR + 8, r); /* store col 2 of result */
// 结果矩阵的第四列
	b = vld1q_f32(matrixB + 12); /* B矩阵第四列 */
	r = vmulq_lane_f32(a0, vget_low_f32(b), 0);
	r = vmlaq_lane_f32(r, a1, vget_low_f32(b), 1);
	r = vmlaq_lane_f32(r, a2, vget_high_f32(b), 0);
	r = vmlaq_lane_f32(r, a3, vget_high_f32(b), 1);
	vst1q_f32(matrixR + 12, r); /* store col 3 of result */
}

// 先提取 再计算 最后存取
void neonmult(const float *matrixA, const float *matrixB, float *matrixR)
{
// 0. 定义变量
	float32x4_t a0, a1, a2, a3, b0, b1, b2, b3, r0, r1, r2, r3;
	
// 1. 先提取每个矩阵的每一列
	a0 = vld1q_f32(matrixA); /* col 0 of matrixA */
	a1 = vld1q_f32(matrixA + 4); /* col 1 of matrixA */
	a2 = vld1q_f32(matrixA + 8); /* col 2 of matrixA */
	a3 = vld1q_f32(matrixA + 12); /* col 3 of matrixA */
	
	b0 = vld1q_f32(matrixB); /* col 0 of matrixB */
	b1 = vld1q_f32(matrixB + 4); /* col 1 of matrixB */
	b2 = vld1q_f32(matrixB + 8); /* col 2 of matrixB */
	b3 = vld1q_f32(matrixB + 12); /* col 3 of matrixB */
	
// 2. 计算结果矩阵的每一列
	/* compute all the cols in the order specified by compiler */
        // 第一列
	r0 = vmulq_lane_f32(a0, vget_low_f32(b0), 0);     // 乘 
	r0 = vmlaq_lane_f32(r0, a1, vget_low_f32(b0), 1); // 乘加
	r0 = vmlaq_lane_f32(r0, a2, vget_high_f32(b0), 0);// 乘加
	r0 = vmlaq_lane_f32(r0, a3, vget_high_f32(b0), 1);// 乘加
	//第二列
	r1 = vmulq_lane_f32(a0, vget_low_f32(b1), 0);
	r1 = vmlaq_lane_f32(r1, a1, vget_low_f32(b1), 1);
	r1 = vmlaq_lane_f32(r1, a2, vget_high_f32(b1), 0);
	r1 = vmlaq_lane_f32(r1, a3, vget_high_f32(b1), 1);
	//第三列
	r2 = vmulq_lane_f32(a0, vget_low_f32(b2), 0);
	r2 = vmlaq_lane_f32(r2, a1, vget_low_f32(b2), 1);
	r2 = vmlaq_lane_f32(r2, a2, vget_high_f32(b2), 0);
	r2 = vmlaq_lane_f32(r2, a3, vget_high_f32(b2), 1);
	//第四列
	r3 = vmulq_lane_f32(a0, vget_low_f32(b3), 0);
	r3 = vmlaq_lane_f32(r3, a1, vget_low_f32(b3), 1);
	r3 = vmlaq_lane_f32(r3, a2, vget_high_f32(b3), 0);
	r3 = vmlaq_lane_f32(r3, a3, vget_high_f32(b3), 1);
	
// 3. 存储设置结果矩阵
	vst1q_f32(matrixR, r0);    // 第一列
	vst1q_f32(matrixR + 4, r1);//第二列
	vst1q_f32(matrixR + 8, r2);//第三列
	vst1q_f32(matrixR + 12, r3);//第四列
}

示例7: 向量叉乘 Cross product

a = [ai, aj, ak]

b = [bi, bj, bk]

** r = a 叉乘 b = [ajbk-akbj, akbi-aibk, aibj-ajbi]**

// Single cross product===== 单叉积?
void cross_product_s(float32_t *r, float32_t* a, float32_t* b)
{
	// 向量存储 ai bi在低地址,ak bk在高地址
	// 寄存器内存 register for example:
	// [element3, element2, element1, element0]  element0低地址  element3高地址
	float32x2_t vec_a_1 = vld1_f32(a + 1); //D register = [ak, aj]  aj低地址
	float32x2_t vec_a_2 = vld1_f32(a);     //D register = [aj, ai]  ai低地址
	
	float32x2_t vec_b_1 = vld1_f32(b + 1); //D register = [bk, bj]  bj低地址
	float32x2_t vec_b_2 = vld1_f32(b);     //D register = [bj, bi]  bi低地址
	
	// 寄存器合并 combine
	float32x4_t vec_a = vcombine_f32(vec_a_1, vec_a_2); //Q register = [aj, ai, ak, aj]
	float32x4_t vec_b = vcombine_f32(vec_b_1, vec_b_2); //Q register = [bj, bi, bk, bj]
        // 寄存器移通道 低位通道数据到最高位通道,其他数据依次往低位通道移动
	float32x4_t vec_a_rot = vextq_f32(vec_a, vec_a, 1); //Q register = [ aj, aj, ai, ak ] 
	float32x4_t vec_b_rot = vextq_f32(vec_b, vec_b, 1); //Q register = [ bj, bj, bi, bk ]
	
	// vec_a = [ aj, ai, ak, aj ]
	// vec_b_rot = [ bj, bj, bi, bk ]
	// vec_a_rot = [ aj, aj, ai, ak ]
	// vec_b = [ bj, bi, bk, bj ]
	
	float32x4_t prod = vmulq_f32(vec_a, vec_b_rot); // 乘
	// prod = [ ajbj, aibj, akbi, ajbk ]
	
        // vec_a_rot*vec_b = [aj*bj, aj*bi, ai*bk, ak*bj]
	prod = vmlsq_f32(prod, vec_a_rot, vec_b);// 乘  再 减  prod - vec_a_rot * vec_b
	// prod = [ ajbj-ajbj, aibj-ajbi, akbi-aibk, ajbk-akbj ]
	
	vst1_f32(r, vget_low_f32(prod)); // 先存储低位两个通道  [XXX, akbi-aibk, ajbk-akbj]
	vst1_lane_f32(r + 2, vget_high_f32(prod), 0); // 再存储第三个通道 [aibj-ajbi, akbi-aibk, ajbk-akbj]
}


// Four cross products
void cross_product_q(float32_t* r, float32_t* a, float32_t* b)
{
	float32x4x3_t vec_a = vld3q_f32(a); // [,,,ai]  0
	                                    // [,,,aj]  1
					    // [,,,ak]  2
					    
	float32x4x3_t vec_b = vld3q_f32(b); // [,,,bi]  0
	                                    // [,,,bj]  1
					    // [,,,bk]  2
	float32x4x3_t result;
	
	result.val[0] = vmulq_f32(vec_a.val[1], vec_b.val[2]); // 乘 aj*bk
	result.val[0] = vmlsq_f32(result.val[0], vec_a.val[2], vec_b.val[1]); // 乘减 aj*bk - ak*bj
	
	result.val[1] = vmulq_f32(vec_a.val[2], vec_b.val[0]); // 乘 ak*bi
	result.val[1] = vmlsq_f32(result.val[1], vec_a.val[0], vec_b.val[2]); // 乘减 ak*bi - ai*bk
	
	result.val[2] = vmulq_f32(vec_a.val[0], vec_b.val[1]); // 乘 ai*bj
	result.val[2] = vmlsq_f32(result.val[2], vec_a.val[1], vec_b.val[0]); // 乘减 ai*bj - aj*bi
	
	vst3q_f32(r, result);
}

示例7: 向量的点积 Dot product

A = (a1,a2,a3,…,an)

B = (b1,b2,b3,…,bn)

A * B = a1b1 + a2b2 + a3b3 + … + anbn

向量的每一维相乘然后相加,相乘之间具有良好的并行性,所以可以通过ARM NEON intrinsic指令进行加速。下面是代码实现:

// 浮点数 
float dot(float* A,float* B,int K)
{
    float sum=0;
    float32x4_t sum_vec=vdupq_n_f32(0); // 和向量,从立即数创建数据
    float32x4_t left_vec,right_vec;     // 向量A 和 向量 B
    for(int k=0; k<K; k+=4) // 这里默认K为4倍数,未考虑剩余数据
    {
        left_vec  = vld1q_f32(A + k); // 先将两个数组每次4个存入ARM NEON intrinsic下的128位变量中
        right_vec = vld1q_f32(B + k);
        sum_vec   = vmlaq_f32(sum_vec,left_vec,right_vec);// 乘加,利用一个乘加指令计算4个乘积的累加和。
    }
    
    // 最后将4个sum再相加就得到最终的结果。
    float32x2_t r = vadd_f32(vget_high_f32(sum_vec),vget_low_f32(sum_vec));// 两两相加
    sum += vget_lane_f32(vpadd_f32(r,r),0);

    return sum;
}

// 相比于串行代码,上面的代码有接近4倍的加速比。当数据类型是short或者char时,可以取得更高的加速比,下面以char举例:

int dot(char* A,char* B,int K)
{
    int sum=0;
    int16x8_t sum_vec=vdupq_n_s16(0);// 128位 和向量,从立即数创建数据
    int8x8_t left_vec, right_vec;    // 64位  向量A 和 向量B
    int32x4_t part_sum4; // 4个32位 128位寄存器 和
    int32x2_t part_sum2; // 2个32位 64位寄存器  和

    //有溢出的风险
    for(k=0; k<K; k+=8)
    {
        left_vec  = vld1_s8(A + A_pos + k);
        right_vec = vld1_s8(B + B_pos + k);
        sum_vec   = vmlal_s8(sum_vec,left_vec,right_vec);
    }

    part_sum4=vaddl_s16(vget_high_s16(sum_vec),vget_low_s16(sum_vec));   
    part_sum2=vadd_s32(vget_high_s32(part_sum4),vget_low_s32(part_sum4));
    sum+=vget_lane_s32(vpadd_s32(part_sum2,part_sum2),0);

    return sum;
}

示例8:3x3 pool 池化代码 最大值/均值池化

// 先分别读取三列
constexpr const int pool_size = 3;
const float32x4_t top_data    = vld1q_f32(reinterpret_cast<const float *>(input_top_ptr + input.offset()));
const float32x4_t middle_data = vld1q_f32(reinterpret_cast<const float *>(input_middle_ptr + input.offset()));
const float32x4_t bottom_data = vld1q_f32(reinterpret_cast<const float *>(input_bottom_ptr + input.offset()));

float32x2_t       res         = {};
if(pooling_type == PoolingType::AVG)
{// 均值池化=============
   // Calculate scale
   float scale = calculate_avg_scale(id, pool_size, upper_bound_w, upper_bound_h, pool_pad_x, pool_pad_y, pool_stride_x, pool_stride_y);
   const float32x2_t scale_v = vdup_n_f32(scale);// 寄存器 初始化为 scale 2个32位

   // Perform pooling
   const float32x4_t sum_data = vaddq_f32(vaddq_f32(top_data, bottom_data), middle_data);
   res = vpadd_f32(vget_high_f32(vsetq_lane_f32(0.f, sum_data, 3)), vget_low_f32(sum_data));
   res  = vmul_f32(vpadd_f32(res, res), scale_v);// 得到4个最大的float 
}
else
{// 最大值池化
   const float32x4_t max_data = vmaxq_f32(vmaxq_f32(top_data, bottom_data), middle_data);
   res = vpmax_f32(vget_high_f32(vsetq_lane_f32(-std::numeric_limits<float>::max(), max_data, 3)), vget_low_f32(max_data));
   res = vpmax_f32(res, res);
}

*(reinterpret_cast<float *>(output.ptr())) = vget_lane_f32(res, 0);

4. NEON assembly

采用汇编语言进行NEON(NEON 汇编(assembly))的最底层优化,可以使优化性能最大化,但汇编语言比较灵活,手写汇编程序对开发人员来说具有较大挑战,如果使用不恰当,反而会影响优化性能。

NEON可以有两种写法:

    1. Assembly文件: 纯汇编文件,后缀为”.S”或”.s”。注意对寄存器数据的保存。
    1. inline assembly内联汇编

在C/C++程序中编写汇编代码主要有两种形式:汇编函数或内联汇编。汇编函数中,需要声明代码段、操作堆栈等,过于复杂。而编写内联汇编,在C代码中需要以“asm”关键字标识,并在asm()编写汇编语句。这种方法只需要在待优化部分局部采用汇编语言实现,相对简单。

数据加载保存移动

扩展 寄存器 加载和存储 指令

语法:

VLDR{cond}{.size} Fd, [Rn{, #offset}]   # load加载,从内存中加载一个扩展寄存器。
VSTR{cond}{.size} Fd, [Rn{, #offset}]   # set保存,将一个扩展寄存器的内容保存到内存中。
VLDR{cond}{.size} Fd, label
VSTR{cond}{.size} Fd, label

cond: 是一个可选的条件代码,EQ等于\NE不等于\HI无符号大于\LS无符号小于等于\GE有符号大于等于\LT有符号小于\GT有符号大于\LE有符号小于等于

size:是一个可选的数据大小说明符。 如果 Fd 是单精度 VFP 寄存器,则必须为 32,传送一个字;否则必须为 64,传送两个字。

Fd:是要加载或保存的扩展寄存器。 对于 NEON 指令,它必须为 Dd。 对于 VFP 指令,它可以为 Dd 或 Sd。

Rn:是存放要传送的基址的 ARM 寄存器。

offset:是一个可选的数值表达式。 在汇编时,该表达式的值必须为一个数字常数。 该值必须是 4 的倍数,并在 -1020 到 +1020 的范围内。 该值被加到基址上以构成用于传送的地址。

label:是一个程序相对的表达式。必须位于当前指令的 ±1KB 范围之内。

扩展寄存器加载多个、存储多个、从堆栈弹出、推入堆栈

语法:

VLDMmode{cond} Rn,{!} Registers # 加载多个
VSTMmode{cond} Rn,{!} Registers # 存储多个
VPOP{cond} Registers            # 从堆栈弹出 VPOP Registers 等效于 VLDM sp!,Registers
VPUSH{cond} Registers           # 推入堆栈   VPUSH Registers 等效于 VSTMDB sp!,Registers

mode 必须是下列值之一:

IA 表示在每次传送后递增地址。IA 是缺省值,可以省略。 increase
DB 表示在每次传送前递减地址。 decrease
EA 表示空的升序堆栈操作。 对于加载操作,该值与 DB 相同;对于保存操作,该值与 IA 相同。
FD 表示满的降序堆栈操作。 对于加载操作,该值与 IA 相同;对于保存操作,该值与 DB 相同。

! 是可选的。! 指定必须将更新后的基址写回到 Rn 中。 如果未指定!,则 mode 必须为 IA。

Registers 是一个用大括号 { 和 } 括起的连续扩展寄存器的列表。 该列表可用逗号分隔,也可以采用范围格式。 列表中必须至少有一个寄存器。可指定 S、D 或 Q 寄存器,但一定不能混用这些寄存器。 D 寄存器的数目不得超过 16 个,Q 寄存器的数目不得超过 8 个。 如果指定 Q 寄存器,则在反汇编时它们将显示为 D 寄存器。

VMOV(在两个 ARM 寄存器和一个扩展寄存器之间传送内容)

在两个 ARM 寄存器与一个 64 位扩展寄存器或两个连续的 32 位 VFP 寄存器之间传送内容。

语法:

VMOV{cond} Dm, Rd, Rn # 将 Rd 的内容传送到 Dm 的低半部分,并将 Rn 的内容传送到 Dm 的高半部分
VMOV{cond} Rd, Rn, Dm # 将 Dm 的低半部分的内容传送到 Rd,并将 Dm 的高半部分的内容传送到 Rn
VMOV{cond} {Sm, Sm1}, Rd, Rn # 将 Sm 的内容传送到 Rd,并将 Sm1 的内容传送到
VMOV{cond} Rd, Rn, {Sm, Sm1} # 将 Rd 的内容传送到 Sm,并将 Rn 的内容传送到 Sm1
Dm 是一个 64 位扩展寄存器。
Sm 是一个 VFP 32 位寄存器。
Sm1 是 Sm 之后的下一个 VFP 32 位寄存器。
Rd、Rn 是 ARM 寄存器。 不要使用 r15。

VMOV(在一个 ARM 寄存器R 和一个 NEON 标量之间)

在一个 ARM 寄存器和一个 NEON 标量之间传送内容。

语法
VMOV{cond}{.size} Dn[x], Rd # 将 Rd 的最低有效字节、半字或字的内容传送到 Sn。
VMOV{cond}{.datatype} Rd, Dn[x] # 将 Dn[x] 的内容传送到 Rd 的最低有效字节、半字或字。

size 是数据大小。 可以为 8、16 或 32。 如果省略,则 size 为 32。

datatype 是数据类型。 可以为 U8、S8、U16、S16 或 32。 如果省略,则 datatype为 32。

Dn[x] 是 NEON 标量,16 位标量限定为寄存器 D0-D7,其中 x 位于范围 0-3 内,32 位标量限定为寄存器 D0-D15,其中 x 为 0 或 1。

Rd 是 ARM 寄存器。Rd 不得为 R15。

NEON 逻辑运算和比较运算

VAND、VBIC、VEOR、VORN 和 VORR(寄存器)

VAND(按位与)、VBIC(位清除)、VEOR(按位异或)、VORN(按位或非)和 VORR(按位或)指令在两个寄存器之间执行按位逻辑运算,并将结果存放到目标寄存器中。

语法:

Vop{cond}.{datatype} {Qd}, Qn, Qm
Vop{cond}.{datatype} {Dd}, Dn, Dm

op 必须是下列值之一:
AND 逻辑“与”\ORR 逻辑“或”\EOR 逻辑异或\BIC 逻辑“与”求补\ORN 逻辑“或”求补。

Qd、Qn、Qm 为四字运算指定目标寄存器、第一个操作数寄存器和第二个操作数寄存器。

Dd、Dn、Dm 为双字运算指定目标寄存器、第一个操作数寄存器和第二个操作数寄存器。

VBIC 和 VORR(立即数)

VBIC(位清除(立即数))获取目标向量的每个元素,对其与一个立即数执行按位与求补运算,并将结果返回到目标向量。

VORR(按位或(立即数))获取目标向量的每个元素,对其与一个立即数执行按位或运算,并将结果返回到目标向量。

语法:

Vop{cond}.datatype Qd, #imm
Vop{cond}.datatype Dd, #imm

op 必须为 BIC 或 ORR。

datatype 必须为 I16 或 I32。

Qd 或 Dd 是用于存放源和结果的 NEON 寄存器。

imm 是立即数。

立即数

如果 datatype 为 I16,则立即数必须采用下列格式之一:
• 0x00XY
• 0xXY00。

如果 datatype 为 I32,则立即数必须采用下列格式之一:
• 0x000000XY
• 0x0000XY00
• 0x00XY0000
• 0xXY000000。

VBIF、VBIT 和 VBSL

VBIT(为 True 时按位插入):如果第二个操作数的对应位为 1,则该指令将第一个操作数中的每一位插入目标中;否则将目标位保持不变。

VBIF(为 False 时按位插入):如果第二个操作数的对应位为 0,则该指令将第一个操作数中的每一位插入目标中;否则将目标位保持不变。

VBSL(按位选择):如果目标的对应位为 1,则该指令从第一个操作数中选择目标的每一位;如果目标的对应位为 0,则从第二个操作数中选择目标的每一位。

语法:

Vop{cond}{.datatype} {Qd}, Qn, Qm
Vop{cond}{.datatype} {Dd}, Dn, Dm

VMOV、VMVN(寄存器)

VMOV向量移动(寄存器)将源寄存器中的值复制到目标寄存器中。

VMVN向量求反移动(寄存器)对源寄存器中每一位的值执行求反运算,并将结果存放到目标寄存器中。

语法:

VMOV{cond}{.datatype} Qd, Qm
VMOV{cond}{.datatype} Dd, Qm
VMVN{cond}{.datatype} Qd, Qm
VMVN{cond}{.datatype} Dd, Qm

NEON 乘法指令

VMUL(向量乘法))将两个向量中的相应元素相乘,并将结果存放到目标向量中。
VMLA(向量乘加)将两个向量中的相应元素相乘,并将结果累加到目标向量的元素中。
VMLS(向量乘减)将两个向量中的相应元素相乘,从目标向量的相应元素中减去相乘的结果,并将最终结果放入目标向量中。
语法:

Vop{cond}.datatype {Qd}, Qn, Qm
Vop{cond}.datatype {Dd}, Dn, Dm
VopL{cond}.datatype Qd, Dn, Dm

内联汇编 inline assembly

ARM GCC Inline Assembler Cookbook

博客参考

优点:在C代码中嵌入汇编,调用简单,无需手动存储寄存器;
缺点:有较为复杂的格式需要事先学习,不好移植到其他语言环境。

汇编语言笔记

内联汇编参考

比如上述intrinsics代码产生的汇编代码为:

// ARMv7‐A/AArch32
void add_float_neon2(int* dst, int* src1, int* src2, int count)
{
	asm volatile (
		"1: \n"                        // 用于构成循环的标记号
		"vld1.32 {q0}, [%[src1]]! \n"  // 从src地址处载入4个32位的浮点数 地址递增
		"vld1.32 {q1}, [%[src2]]! \n"
		"vadd.f32 q0, q0, q1 \n"       // q0 = q0 +q1
		"subs %[count], %[count], #4 \n"// 循环计数count = count-4
		"vst1.32 {q0}, [%[dst]]! \n"   // 将运算结果存储到目标地址,目标地址递增
		"bgt 1b \n"                    // 如果count>0,跳转到标记号1处继续执行
		: [dst] "+r" (dst)             // 可写
		: [src1] "r" (src1), [src2] "r" (src2), [count] "r" (count)
		: "memory", "q0", "q1"
	);
}

建议的NEON调优步骤

    1. 理清所需的寄存器、指令。 建议根据要实现的任务,画出数据变换流程,和每步所需的具体指令,尽可能找到最优的实现流程。这一步非常关键,如果思路出错或是不够优化,则会影响使用NEON的效果,并且对程序修改带来麻烦,一定要找到最优的实现算法哦~
    1. 先实现intrinsics(可选)。 初学者先实现intrinsics是有好处的,字面理解性更强,且有助于理解NEON指令。建议随时打印关键步骤的数据,以检查程序的正误。
    1. 写成汇编进一步优化。 将intrinsics生成的汇编代码进行优化调整。一般来说,有以下几点值得注意【干货】:
  • 只要intrinsics运算指令足够精简,运算类的汇编指令就不用大修;

  • 大部分的问题会出在存取、移动指令的滥用、混乱使用上;

  • 优化时要尽量减少指令间的相关性,包括结构相关、数据相关控制相关,保证流水线执行效率更高;

  • 大概估算所有程序指令取指、执行、写回的总理论时间,以此估算本程序可以优化的空间;

  • 熟练对每条指令准备发射、写回时间有一定的认识,有助于对指令的优化排序;

  • 一定要多测试不同指令的处理时间!!原因是你所想跟实际有出入,且不同的编译器优化的效果可能也有些不同;

  • 一定要有一定的计算机体系结构基础,对存储结构、流水线有一定的体会!!

总结一下NEON优化就是:

  • 第一优化算法实现流程;
  • 第二优化程序存取;
  • 第三优化程序执行;
  • 第四哪儿能优化,就优化哪儿

需要注意的地方

  1. load数据的时候,第一次load会把数据放在cache里面,只要不超过cache的大小,下一次load同样数据的时候,则会比第一次load要快很多,会直接从cache中load数据,这样在汇编程序设计的时候是非常需要考虑的问题。

如:求取一个图像的均值,8*8的窗口,先行求和,然后列求和出来均值,这时候会有两个函数,数据会加载两遍,如果按照这样去优化的话则优化不了多少。如果换成上面这种思路,先做行16行,然后再做列,这样数据都在cache里面,做列的时候load数据会很快。

在做neon乘法指令的时候会有大约2个clock的阻塞时间,如果你要立即使用乘法的结果,则就会阻塞在这里,在写neon指令的时候需要特别注意。乘法的结果不能立即使用,可以将一些其他的操作插入到乘法后面而不会有时间的消耗。

如:vmul.u16 q1, d3, d4

     vadd.u32 q1, q2, q3

此时直接使用乘法的结果q1则会阻塞,执行vadd需要再等待2个clock的时间

使用饱和指令的时候,如乘法饱和的时候,在做乘法后会再去做一次饱和,所以时间要比直接做乘法要慢。

如: vmul.u16 q1, d3, d4

      vqmul.u32 q1, q2, q3

后一个的时间要比第一个的时间要久。

在对16位数据进行load或者store操作的时候,需要注意的是字节移位。比如是16位数据,则load 8个16位数据,如果指定寄存器进行偏移,此时需要特别注意。

例如:vld1.64 {d0}, [r0], r1

内联汇编使用心得

ARM GCC Inline Assembler Cookbook

inline assembly下面的三个冒号一定要注意
output/input registers的写法一定要写对,clobber list也一定要写完全,否则会造成令你头疼的问题 (TT)

这个问题在给出的cookbook中也有介绍,但是并不全面,有些问题只有自己碰到了再去解决。 笔者就曾经被虐了很久,从生成的汇编发现编译器将寄存器乱用,导致指针操作完全混乱,毫无头绪…

一般情况下建议的写法举例:

asm volatile (
	... /* assembly code 汇编代码 */
	// 所有的汇编代码必须用双引号括起来。
        // 如果有多行汇编代码的话,每一条语句都要用双引号括起来,并且在代码后面要加上换行符(“\n”或者“\n\t”)。
	
	// "[modifier修改符 可选]constraint限定符" (C expression C语言表达式)
	// 修改符和限定符要用双引号括起来,而C表达式要用括号括起来。
	: "+r"(arg0) // %0
	  "+r"(arg1) // %1 // 输入寄存器 Output Registers
	: "r"(arg2)  // %2 // 输入寄存器 Input Registers
	: "cc", "memory", r0, r1  // 寄存器变化
);

限定符

限定符   在ARM指令集下              在Thumb指令集下
f         浮点寄存器f0...f7              N/A
h         N/A                           寄存器r8...r15
G         浮点常量立即数                 N/A
H         和G作用相同                    N/A
I         数据处理指令中用到的立即数      范围为0...255的常量
J         范围为-4095...4095的索引常量    范围为-255...-1的常量
K         和I作用相同                    和I作用相同
L         和I作用相同                    范围为-7...7的常量
l         和r作用相同                    寄存器r0...r7
M         范围为0.32或者是2的幂次方的常量  范围为0...1020的4的倍数的常量
m         内存地址memory                 内存地址
N         N/A                           范围为0...31的常量
O         N/A                           范围为 -508...508 的4的倍数的常量
r         通用寄存器r0...r15             N/A
w         向量浮点寄存器s0...s31         N/A
X         任何类型的操作数               任何类型的操作数
    
数字 0,1,2,3,... 指代前面定义的操作数

是常用的也就是r,f和m等几个。

修改符

修改符是加在限定符之前的,并且是可选的,如果没有修改符的话,则表明这个操作数是只读的。

这个对输入操作数没有问题,但是对输出操作数来说,肯定是需要被修改的,那怎么办呢?

答案就是使用修改符,修改这个操作数的属性。目前,GCC中定义了三个修改符,分别是:

修改符    含义
=        只写 操作数,通常用于输出操作数中
+        可读 且 可写 操作数,必须要列在输出操作数中
&        寄存器只能用于输出(不能作为输入寄存器)

所以,作为输出操作数,只需要在限定符前加上“=”就可以了。

如果想让一个C变量既作为输入操作数,也作为输出操作数的话,可以使用“+”限定符,并且这个操作数只需要在输出操作数列表中列出就行了。例如:

__asm__(
        "mov %0, %0, ror #1"   
        : "+r" (y)  
        );  

是将变量y中的值右移1位。因为输入和输出操作数是一个,所以该操作数要既可读也可写,因此添加了“+”修改符。

其实,在限定符中,也可以使用数字,其作用是指代前面定义的操作数,0代表第一个,1代表第二个,以此类推。

__asm__(
        "mov %0, %0, ror #1"   
        : "=r" (y)  
        : "0" (y)  
        );  

// 这个例子的效果和前面的例子是相同的。本例不同的是,先定义了一个可写的输出变量,同时在输入变量列表中,明确用数字0指出了前面定义的第一个操作数同时也要用来作为输入操作数。

使用“&”修改符,明确告诉编译器,代表输出操作数的寄存器一定不能使用输入操作数已经使用过的寄存器。下面举个例子:

如果汇编代码中有输入寄存器还没有使用完毕,就对输出操作数进行修改的情况,则特别需要用“&”修改符,保证不复用。

__asm__ __volatile__(
                 "ldr %0, [%1]\n\t"  
                 "str %2, [%1, #4]"  
                 : "=&r" (rdv)  
                 : "r" (&table), "r" (wdv)  
                 : "memory");  

本例中,将操作一个table数组,读出它的第一个数存放到rdv中,然后修改第二个数为wdv中存放的值。乍看一下没什么问题,但是如果编译器用同一个寄存器来表示输入操作数&table(%1)和输出操作数rdv(%0)怎么办呢?执行完第一条语句之后,table数组的地址就被修改掉了。所以,可以在输出操作数中加上一个“&”修改符,强制保证输出操作数不能和输入操作数复用同一个寄存器,这个问题就解决了

修改寄存器列表

在汇编指令中,有可能会用到一些指定的寄存器,但是在执行你定义的汇编程序时,那个指定的寄存器有可能另有别的用途,存放了非常重要的数据。等你的程序执行完成后,那个寄存器的值已经被你修改过了,肯定会造成执行错误。因此,在执行你的程序之前必须要做必要的备份和恢复的动作。但是,编译器并不会分析你的汇编代码,找出这种被你修改过,需要恢复的寄存器,因此你必须显式的告诉编译器,被你修改过的寄存器有哪些。这就是修改寄存器列表所起到的作用。

对于嵌入内联ARM汇编来说,此列表中的值有下面三种类型:

类型           作用
r0...r15     告诉编译器汇编代码中 修改了通用寄存器r0...r15
cc           告诉编译器汇编代码 会 导致 CPU状态位 的 改变
memory       告诉编译器汇编代码 会 读取或修 改内存中某个地址 存放的值

对于“memory”来说,它并不是表示寄存器被读取或修改了,而是表示内存中的值被修改了。出于优化的目的,在执行你的汇编代码之前,编译器将某些变量的值还保存在寄存器中,并没有被写到实际的内存中。但是,如果你的汇编代码会读取内存中的值,则很有可能新的值还在寄存器中,而内存中存放的还是老的值,这样就会造成错误。添加了“memory”之后,编译器会在执行你的代码之前,保证将保存在寄存器中,没有更新到内存中的值全部都写入到内存中。

此列表中的每一项都要用双引号("")括起来,每项之间要用逗号(“,”)分割。

ARM NEON CNN卷积网络优化 深度学习优化 实例

参考NCNN

1.绝对值 AbsVal arm_neon_v7 neon_v8 优化

//  arm 内联汇编
// asm(
// 代码列表
// : 输出运算符列表        "r" 表示同用寄存器  "m" 表示内存地址 "I" 立即数 
// : 输入运算符列表        "=r" 修饰符 = 表示只写,无修饰符表示只读,+修饰符表示可读可写,&修饰符表示只作为输出
// : 被更改资源列表
// );
// __asm__ __volatile__(); 

// 关键字“__asm__”,其实也可以写成“asm”。但是“asm”并不是所有版本的GCC编译器都支持的,
// 而且有可能和程序中别的地方定义的变量或函数名冲突,所以用“__asm__”的话,兼容性会好一点。

// __volatile__或volatile 是可选的,假如用了它,则是向GCC 声明不答应对该内联汇编优化,
// 否则当 使用了优化选项(-O)进行编译时,GCC 将会根据自己的判定决定是否将这个内联汇编表达式中的指令优化掉。

// 作用是禁止编译器对后面编写的汇编指令再进行优化。一般情况下,自己写的汇编代码肯定是自己进行设计优化过了的,
// 如果编译器再进行优化的话,很有可能效果还不如不优化,而且也有可能会出现奇怪的错误,所以通常都会带上这个关键字。
// 同样,“__volatile__”也可以写成“volatile”,但可能兼容性会没那么好。

#if __ARM_NEON
#include <arm_neon.h>
#endif // __ARM_NEON

// 换行符和制表符的使用可以使得指令列表看起来变得美观。
int AbsVal_arm::forward_inplace(Mat& bottom_top_blob) const
{
    int w = bottom_top_blob.w;// 输入特征图宽度
    int h = bottom_top_blob.h;// 输入特征图高度
    int channels = bottom_top_blob.c;// 输入特征图通道数
    int size = w * h;// 一个通道的元素数量

    #pragma omp parallel for // omp并行
    // #pragma omp parallel for num_threads(opt.num_threads)
    for (int q=0; q<channels; q++)//遍历每一个特征通道
    {
        float* ptr = bottom_top_blob.channel(q);// 当前特征通道数据的起始地址指针

// 如果支持ARM_NEON 则使用NEOB进行优化
#if __ARM_NEON
        int nn = size >> 2;// 128位的寄存器,一次可以操作 4个float32位,剩余不够4个的,最后面直接c语言执行
                           // 右移两位相当于除以4
        int remain = size - (nn << 2);// 4*32 =128字节对其后 剩余的 float32个数, 剩余不够4个的数量
        
#else
        int remain = size; // 若不支持优化,则全部使用不同C语言版本进行计算
#endif // __ARM_NEON
        
/*
从内存中载入:
v7:
   带了前缀v的就是v7 32bit指令的标志;
   ld1表示是顺序读取,还可以取ld2就是跳一个读取,ld3、ld4就是跳3、4个位置读取,这在RGB分解的时候贼方便;
   后缀是f32表示单精度浮点,还可以是s32、s16表示有符号的32、16位整型值。
   这里Q寄存器是用q表示,q5对应d10、d11可以分开单独访问(注:v8就没这么方便了。)
   大括号里面最多只有两个Q寄存器。

     "vld1.f32   {q10}, [%3]!        \n"
     "vld1.s16 {q0, q1}, [%2]!       \n" 


v8:
  ARMV8(64位cpu) NEON寄存器 用 v来表示 v1.8b v2.8h  v3.4s v4.2d
  后缀为8b/16b/4h/8h/2s/4s/2d)
  大括号内最多支持4个V寄存器;

  "ld1    {v0.4s, v1.4s, v2.4s, v3.4s}, [%2], #64 \n"   // 4s表示float32
  "ld1    {v0.8h, v1.8h}, [%2], #32     \n"
  "ld1    {v0.4h, v1.4h}, [%2], #32     \n"             // 4h 表示int16


所有的汇编代码必须用双引号括起来。如果有多行汇编代码的话,每一条语句都要用双引号括起来,并且在代码后面要加上换行符(“\n”或者“\n\t”)。

这样做是因为GCC会将汇编代码部分作为字符串形式直接传给汇编器,加上换行符后,汇编器就能准确知道哪些字符串表示的是一条汇编语句。同时,为了增加可读性,每条汇编语句都可以换行。

*/
        
// 优化过程
#if __ARM_NEON
// arm_v8================================
#if __aarch64__ // ARMv8-A 是首款64 位架构的ARM 处理器,是移动手机端使用的CPU
        if (nn > 0)// 这里的循环次数已经是 除以4之后的了
        {
        asm volatile(
            "0:                               \n" // 0: 作为标志,局部标签
            "prfm       pldl1keep, [%1, #128] \n" // %1处为ptr标识为1标识,即数据地址,预取 128个字节 4*32 = 128
            "ld1        {v0.4s}, [%1]         \n" // 载入 ptr 指针对应的值,连续4个
            "fabs       v0.4s, v0.4s          \n" // ptr 指针对应的值 连续4个,使用fabs函数 进行绝对值操作 4s表示浮点数
            "subs       %w0, %w0, #1          \n" // %0 引用 参数 nn 操作次数每次 -1  #1表示1
	                                          // 
            "st1        {v0.4s}, [%1], #16    \n" // %1 引用 参数 ptr 指针 向前移动 4*4=16字节
	                                          // store 1, {v0.4s} 计算绝对值后 再存入 [%1]?
            "bne        0b                    \n" // 如果非0,则向后跳转到 0标志处执行
	    
            // BNE指令会去查看状态寄存器,当Z!=0的时候就跳转到指定位置.
            // BEQ功能与BNE刚好相反,Z==0的时候才跳转到指定位置.

            // 每个操作数的寄存器行为 “=”,表示此操作数类型是只写,即输出寄存器。
	    // "[modifier修改符可选]constraint限定符" (C expression C语言表达式) 
            : "=r"(nn),     // %0 操作次数 nn  循环变量
              "=r"(ptr)     // %1 引用参数 ptr 数据内存地址指针
            
             // 数据 标签标识 nn 标识为0  ptr标识为1
	     // 使用百分号(“%”)后面接一个数字,0表示定义的第一个操作数,1表示定义的第二个操作数,依次类推。
            : "0"(nn),  
              "1"(ptr)
            // 寄存器变化表 list of clobbered registers  
            : "cc", "memory", "v0" // v0寄存器,内存memory, cc CPU状态位 可能会变化
        );
        }
#else
        
// arm_v7===========================
        if (nn > 0)
        {
        asm volatile(
            "0:                             \n" // 0: 作为标志,局部标签
            "vld1.f32   {d0-d1}, [%1]       \n" // %1处为ptr标识为1标识,即数据地址
	                                        // IA 表示在每次传送后递增地址。IA 是缺省值,可以省略。??
            "vabs.f32   q0, q0              \n" // q0寄存器 = [d1 d0],128位寄存器,取出四个 float 单精度浮点数 进行绝对值计算 后 写入
            "subs       %0, #1              \n" // %0为 循环变量nn标识,标识循环次数-1  #1表示1
            "vst1.f32   {d0-d1}, [%1]!      \n" // 存储 store1 经过绝对值运算后的寄存器的值 存入原内存中
	                                        // !感叹号作用? 指针 [%1] 前移16字节??
						// ! 指定必须将更新后的基址([%1]递增16)写回到 [%1] 中
						
            "bne        0b                  \n" // 如果非0,则向后跳转到 0标志处执行
            // 每个操作数的寄存器行为 “=”,表示此操作数类型是只写,即输出寄存器。
            : "=r"(nn),     // %0
              "=r"(ptr)     // %1
            // 数据 标签标识 nn 标识为0  ptr标识为1
            : "0"(nn),
              "1"(ptr)
            // 寄存器变化表 list of clobbered registers  
            : "cc", "memory", "q0"// q0寄存器,内存memory, cc CPU状态位 可能会变化 
        );
        }
#endif // __aarch64__
#endif // __ARM_NEON
        
        // 剩余不够4个的直接c语言执行=====
        for (; remain>0; remain--)// 循环次数-1
        {
            *ptr = *ptr > 0 ? *ptr : - *ptr;
            ptr++;// 指针+1
        }
    }

    return 0;
}

2. BN层 通道数据归一化 BatchNorm

// load_model() 函数预处理===============

    // 去均值 归一化 合在一起=============
    // 各个通道均值 mean_data = sum(xi)/m
    // 各个通道方差 var_data     = sum((xi - mean_data)^2)/m
    // xi‘ = ( xi - mean_data )/(sqrt(var_data + eps))  // 去均值,除以方差,归一化
    
    // yi = slope_data * xi'  + bias_data  //  缩放 + 平移=====
    
    // 写成一起=====================
    // yi = slope_data / (sqrt(var_data + eps)) * xi  + bias_data - slope_data*mean_data/(sqrt(var_data + eps)) 
    // b = slope_data / (sqrt(var_data + eps)) = slope_data /sqrt_var;
    // a = bias_data - slope_data*mean_data/(sqrt(var_data + eps)) = bias_data - slope_data*mean_data/sqrt_var;
    
    // yi = b * xi + a
    
    
int BatchNorm_arm::forward_inplace(Mat& bottom_top_blob, const Option& opt) const
{
    int dims = bottom_top_blob.dims;
    
    if (dims != 3) // 只有三通道的特征图才使用 neon加速
        return BatchNorm::forward_inplace(bottom_top_blob, opt);

    // a = bias - slope * mean / sqrt(var)
    // b = slope / sqrt(var)
    // value = b * value + a

    int w = bottom_top_blob.w;// 特征图宽度
    int h = bottom_top_blob.h;// 特征图高度
    int size = w * h;// 一张特征图尺寸

// 整合后的变化系数  yi = b * xi + a
    const float* a_data_ptr = a_data; // batchnorm.h 中公开的 Mat矩阵数据,数组首地址
    const float* b_data_ptr = b_data;
    
    #pragma omp parallel for num_threads(opt.num_threads)
    for (int q=0; q<channels; q++)// 遍历每个通道
    {
        float* ptr = bottom_top_blob.channel(q);// 每一个通道的 特征图 数据 首地址
 // 每通道 的 变化系数==都一样=
        float a = a_data_ptr[q];
        float b = b_data_ptr[q];

#if __ARM_NEON
        int nn = size >> 2; // 128位寄存器一个可以操作 4个 32位浮点数,所以总数除以4得到 寄存器操作次数
	                    // 右移动2位,相当于除以4,例如 10,右移两位相当于乘除4,得到2
        int remain = size - (nn << 2);// 10-2*4=2 剩余2个 不够4,使用普通c语言版本
#else
        int remain = size; // 如果不支持neon,则全部使用 普通c语言计算呢
#endif // __ARM_NEON

#if __ARM_NEON
#if __aarch64__
        if (nn > 0)
        {
        asm volatile(
            "dup        v1.4s, %w4             \n" // 每通道的 变化系数a,b都一样只需载入一次,传入的为立即数使用dup
            "dup        v2.4s, %w5             \n" // v1存储a,v2存储b,v0存储特征数据,v3存储变化的数据地址以及a
            "0:                                \n" // 构成循环的标记号
            "prfm       pldl1keep, [%1, #128]  \n" // 从%1 ptr 处预读取 128字节 4*32 4个浮点数
            "ld1        {v0.4s}, [%1]          \n" // 载入 ptr 指针对应的值到 v0,连续4个float
            "orr        v3.16b, v1.16b, v1.16b \n" // v1 --> v3,  v3 =a
            "fmla       v3.4s, v0.4s, v2.4s    \n" // 特征数据v0*缩放v2 + 偏置v3 最后赋值给 v3 += v0×b
            "subs       %w0, %w0, #1           \n" // %0 为nn 执行次数 -1   #1   为1
            "st1        {v3.4s}, [%1], #16     \n" // 结果v3 store存储到 原数据地址处,原数据地址递增16字节
            "bne        0b                     \n" // 不为零跳回去,继续循环
            : "=r"(nn),     // %0
              "=r"(ptr)     // %1
            : "0"(nn),      // 2 ???=====
              "1"(ptr),     // 3 ???=====
              "r"(a),       // %4 存入寄存器 只读, 不变, 参数 偏置a
              "r"(b)        // %5 存入寄存器 只读, 不变,参数 缩放归一化系数
            : "cc", "memory", "v0", "v1", "v2", "v3"
	    //  cc CPU状态位,内存memory,v,v1,v2,v3寄存器 可能会变化
        );
        }
#else
        if (nn > 0)
        {
        asm volatile(
            "vdup.f32   q1, %4              \n"// 每通道的 变化系数a,b都一样只需载入一次,传入的为立即数使用dup
            "vdup.f32   q2, %5              \n"// q1存储变量 a,q2存储变量b,q0存储特征值
	                                       // q3存储中间变量,先存储a和b以及q0执行乘加后,存储最终的结果
					       // 最后把 在q3中的结果 存储回原 特征数据地址处
					       
            "0:                             \n"// 构成循环的标记号
            "pld        [%1, #128]          \n"// 从%1 ptr 处预读取 128字节 4*32 4个浮点数
            "vld1.f32   {d0-d1}, [%1 :128]  \n"// 从%1 ptr 处载入 4个浮点数到q0,传入的为指针,使用ld
            "vorr.32    q3, q1, q1          \n"// q3 = q1 或 q1 = 变量a
            "vmla.f32   q3, q0, q2          \n"// q3 += q0(特征值)*q2(变量b), 乘加运算
            "subs       %0, #1              \n"// 循环次数 nn -1
            "vst1.f32   {d6-d7}, [%1 :128]! \n"// q3->{d6-d7} 结果值 顺序store到 原特征值地址处[%1]
	                                       // !感叹号,强制[%1]向后跳转128位 
            "bne        0b                  \n"// 不为零跳回去,继续循环 
	    
            : "=r"(nn),     // %0 循环次数(按寄存器一次并行运算4个浮点数数) nn 
              "=r"(ptr)     // %1 特征值数据地址
            : "0"(nn),      // 2 ???===
              "1"(ptr),     // 3 ???===
              "r"(a),       // %4
              "r"(b)        // %5
            : "cc", "memory", "q0", "q1", "q2", "q3"
	    //  cc CPU状态位,内存memory,q0,q1,q2,q3寄存器 可能会变化
        );
        }
#endif // __aarch64__
#endif // __ARM_NEON
        for (; remain>0; remain--)
        {
            *ptr = b * *ptr + a;// 剩余不够 4个的 直接c语言执行

            ptr++;// 数据地址增加 1
        }
    }
    return 0;
}    
    

3.添加偏置类 bias

// 进行运算: y = x + bias ---> x

int Bias_arm::forward_inplace(Mat& bottom_top_blob, const Option& opt) const
{
    int w = bottom_top_blob.w;// 特征图宽度
    int h = bottom_top_blob.h;// 特征图高度
    int channels = bottom_top_blob.c;// 通道数量(特征 图 厚度,汉堡包层数)
    int size = w * h;// 单通道特征尺寸

    const float* bias_ptr = bias_data; // 偏置数据 指针 在 bias.h 中定义的 public公开数据
    
    #pragma omp parallel for num_threads(opt.num_threads)// omp并行执行
    
    for (int q=0; q<channels; q++)// 遍历每个通道
    {
        float* ptr = bottom_top_blob.channel(q);// 每个通道数据起始指针 (原有特征数据)

        float bias = bias_ptr[q];// 每通道偏置参数一样

#if __ARM_NEON
        int nn = size >> 2; // 128位寄存器一个可以操作 4个 32位浮点数,所以总数除以4得到 寄存器操作次数
	                    // 右移动2位,相当于除以4,例如 10,右移两位相当于乘除4,得到2
        int remain = size - (nn << 2);// 剩余不够4个的数量 1~3
#else
        int remain = size;
#endif // __ARM_NEON

#if __ARM_NEON
// 这里 直接使用了 neon Instrinsic 内在函数,不过优化程度不如 汇编代码

        float32x4_t _bias = vdupq_n_f32(bias);// 偏置数据 dup载入到 寄存器 4个32位的浮点数
	                                      // 传入的为 立即数
        for (; nn>0; nn--)
        {
            float32x4_t _p = vld1q_f32(ptr);// 载入 特征值 传入的为 数据的地址 
            float32x4_t _outp = vaddq_f32(_p, _bias);// 加上偏置_bias
            vst1q_f32(ptr, _outp);                   // 从寄存器数据 设置内存数据 store1存储结果数据到ptr

            ptr += 4;// 特征指针 移动四个单位
        }
	
// 可以试写 neon内联汇编代码,区分v8 、v7===============
	
#endif // __ARM_NEON

        for (; remain>0; remain--)
        {
            *ptr = *ptr + bias; // 普通c 版本 加上偏置

            ptr++;
        }
    }

    return 0;
}

在这里插入图片描述

2019-02-08 14:04:24 qq_43678612 阅读数 50
  • 玩转深度学习实战教程

    玩转深度学习视频培训课程,详细讲解深度学习的原理和利用深度学习框架TensorFlow进行项目实战。课程通过Kaggle竞赛平台的Titanic问题讲解TensorFlow的基本用法以及问题处理的常用技巧,讲解深度学习图像领域的卷积神经网络CNN和多个经典的网络架构、CNN的应用,讲解自然语言处理领域的RNN、LSTM以及它们的多种变种结构,构建语言模型和对话机器人,介绍损失函数和优化深度学习算法在TensorFlow中的实现。

    3209 人正在学习 去看看 王而川
               

每周荐书:SQL优化、深度学习、数据科学家(评论送书)


老规矩,感谢大家对每周荐书栏目的支持,先公布下上周中奖名单


冲向改变

帝炎魔

《Swift进阶》

 

刘望舒

贾文静

《Java多线程编程实战指南(核心篇)》

 

当年的春天

llld5553

《React前端技术与工程实践》

 

请以上六位用户私信给小编快递地址,我尽快给各位发送奖品。


每周荐书活动规则:

文末评论里回复你对本周推荐图书的看法,或想要获得某本书的书名及理由

下期荐书更新时,会在本期评论中选出6名优秀评论可以免费获得此书


收获,不止SQL优化——抓住SQL的本质


跟着乐于分享的数据库大师梁敬彬抓住表象背后的SQL本质

梁敬彬 梁敬弘 著

ISBN 978-7-121-31436-0

2017年6月出版

定价:88.00元


编辑推荐

SQL优化并不简单,做好SQL优化需要掌握数据库体系结构、表和索引设计、高效SQL写法、高级SQL语法、多种优化工具等知识,甚至还得分析业务特点,以及了解优化器的缺点。只有建立SQL优化方法论体系,才能够迅速找到最适合的方法来优化SQL,从而解决由SQL引发的性能问题。在这本书里,全方位详解了SQL性能优化之道,相信读者定会受益良多!



深入浅出深度学习:原理剖析与Python实践


通俗易懂讲解深度学习核心要素,快速炼成AI工程师!

杨强教授、邓侃博士力荐!

入门+工具+思维+实践=深度学习速成宝典!

 

黄安埠 著

ISBN 978-7-121-31270-0

2017年6月出版

定价:79.00元


编辑推荐

本书最大的特色在于取舍明确,一切无助于迅速理解深度学习精髓的内容全被摒弃了,并着重阐述了技术上的重点和难点;表达上深入浅出:即便是从未接触过AI知识的人,也能从作者简明清晰的表述中,一窥深度学习的殿堂。

对任何一位想成为AI/深度学习领域工程师的读者来说,《深入浅出深度学习:原理剖析与Python实践》能帮你迅速打开AI的大门,并成长为一名合格的AI工程师。

 


数据科学家养成手册


用科学的眼光看待数据、收集数据、分析数据

用科学的眼光审视数据、解读数据、运用数据

高扬 编著

ISBN 978-7-121-31304-2

2017年5月出版

定价:79.00元


编辑推荐

从哲学、数学、物理、统计,到测量、实验、辩证、分析,用谈天说地的方式激发思考,归纳总结数据科学的实质及成就一位数据科学家所需要的基本素养。

《数据科学家养成手册》适合大数据从业人员和对大数据相关知识感兴趣的人,初级和中级程序员、架构师及希望通过对数据的感知改进工作的人,产品经理、运营经理、数据分析师、数据库开发工程师等对数据分析工作敏感的人,以及所有对数据科学感兴趣并希望逐步深入了解数据科学知识体系的人阅读。


           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

深度学习最优化

阅读数 49

没有更多推荐了,返回首页