从x86到ARM,C和C++实现90%代码自动迁移的方法论

x86与ARM之争,已经贯穿了很长时间,过去一直是x86架构比较受到市场和开发者的欢迎。但是自从移动互联网、物联网和边缘计算兴起之后,ARM似乎已经找到最适合自己生存的土壤。架构之争的平台技术拐点,已然来临。

x86与ARM之争,已经贯穿了很长时间,过去一直是x86架构比较受到市场和开发者的欢迎。但是自从移动互联网、物联网和边缘计算兴起之后,ARM似乎已经找到最适合自己生存的土壤。

架构之争的平台技术拐点,已然来临。

现在,每个人手上都有一台智能计算终端,移动应用逐渐云化,5G 催生了云游戏的诞生;Web 应用的加密性越来越重要,HTTPS 流量越来越大;大数据分布式并行计算成为主流等,这些都让 x86 架构的不足逐渐显露出来。

所以才会有现在所遇到的情况,即不得不从x86上的应用迁移至ARM上。但也正因为这是两个完全不同的平台,所以在迁移过程中会遇到各种各样的问题。这也是DevRun 开发者沙龙–首期【鹏城实验室 & 华为鲲鹏专场】中所重点讲到的问题之一。

以下内容经由InfoQ编辑整理自DevRun 开发者沙龙【鹏城实验室 & 华为鲲鹏专场】中张永正和杨少洪老师的分享。

鯤鹏软件迁移概述和方法论

首先要搞懂:为什么要迁移?

从x86到ARM,C和C++实现90%代码自动迁移的方法论

上图所示为程序执行的过程和对应的计算栈。任何一台计算机都是由硬件和软件组成的,类似于最底层的基础物理原材料、晶体管、寄存器、微架构等都属于硬件层面。而软件层面则特指由高级语言、汇编语言开发的应用程序。要执行这些应用程序,需要底层CPU支持由汇编器形成二进制的机器码(由指令和数据组成)去运行。

因此就需要底层计算平台能够支持该CPU的指令才可以,这也是在x86和鲲鹏编译的区别之处。

在x86和鯤鹏上编译之后的指令差异是哪些?可以参照下图左侧,显而易见这是一套非常简单的代码,分别在x86和鯤鹏上编译之后形成三点指令差异:

首先是汇编不同,x86上是两条mov指令,通过把A和B的变量从内存当中取到寄存器,并将两值相加,再由一条mov指令写回内存;鯤鹏上则是通过两条ldr指令、一条add指令以及一条str指令完成整个过程。

其次是指令长度不同,x86上mov指令是24位的,ldr指令是16位的,在鯤鹏上则都是32位;第三则是寄存器不同,x86和鲲鹏处理器使用的向量寄存器不同,其向量指令级也存在差异。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

鲲鹏处理器与x86处理器的指令差异

这也正是做迁移的原因,因为在x86上所编译出来的应用程序因为有以上三点的不同,因此无法在鲲鹏上直接运行。

从x86到鲲鹏,迁移五步走战略

从大量的实践中得以总结出一些规律和方法,主要分为以下5个步骤:

1、迁移准备,主要以收集硬件信息和软件栈信息为主;

在这期间,主要收集硬件和软件信息。硬件方面的信息主要是收集芯片和服务器的型号,从而方便提供配置性能差不多的鲲鹏服务器;其次是收集软件栈信息,主要分为操作系统、虚拟机、中间件、编译器、上层依赖的开源软件、商业软件、业务软件等信息。

2、迁移分析,对收集到的信息和软件栈做初步分析,判断是否真正需要迁移,评估迁移的工作量;

下图左侧就是一个非常完善的技术栈,底层有芯片,中间层为OS、虚拟机、编译器等相对应的运行环境。上层是业务软件,分为开源、自研和商用软件。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

开源软件的迁移相对较为简单,其中一部分开源软件在ARM上已经被编译好的包,直接下载即可。即便没有现成的编译成果,自行下载原码进行编译也并不复杂方便;自研软件的迁移需要注意语言类型的差异,编译型语言是需要重新编译之后才能运行在新环境上,但是对于解释型的语言来说就没有重新编译的需要,只要更换所依赖的虚拟机就可以;商用软件则较为麻烦一些,首先可以通过联系厂商获取它对应ARM架构下的软件版本,如果没有的话就需要寻找有类似功能的软件做替换。

此外像运行环境、虚拟机、编译器和操作系统这些也是要进行替换,但是这些并非需要重新编译,因为在华为云鯤鹏论坛内有软件仓库,可以直接去软件仓库下载由鲲鹏官方所做的经过验证的版本。

3、编译迁移,分析完成之后就可以着手迁移工作,主要分为代码迁移和软件包迁移;

迁移分析之后可以进行代码的编译和打包,编译主要涉及两个到代码迁移和软件包迁移。

其中代码迁移需要区分语言,像C/C++和指令级的差异是比较大的,因此在x86上编译出来的应用程序无法在在鯤鹏上直接使用,因此要在鯤鹏上重新编译才可以。此外编译型语言所涉及的修改点相对更多,因为代码当中有可能蕴含一些对指令级相关的宏定义或者功能性函数;但是对于Java/Python这种解释型语言来说就会简单很多。如果是纯Java/Python的程序,就无须做编译,因为本身的虚拟机已经对指令级进行了屏蔽,只要更换虚拟机就可以。

对于软件包迁移来说,首先需要扫描该软件包是否存在依赖库或者依赖的可执行程序,这些库和可执行程序如果是用C语言写的是需要重新编译的,编译之后重新把软件包打包即可。

4、性能调优,验证完成之后对性能指标进行测试,进行性能调优;

由于大部分软件对性能都有要求,因此在迁移完成之后需要对性能进行调优,这里总结了【建立基准-压力测试-确定瓶颈-实施优化-确认效果】这五步法。

首先需要建立调优基准,该基准根据当前的硬件配置、组网、测试模型来做综合评估,以建立合理的条有目标;其次在调优目标建立后,通过压测工具对软件或系统进行加压,在加压过程中暴露性能瓶颈,确定瓶颈之后对瓶颈进行优化;第四,注意在优化过程中要及时记录,因为优化并不一定是正向的,出现负向优化时需要及时回退;最后在优化措施实施完成后,需要重新启动压力测试工具以确认优化效果。

这个过程并非一蹴而就,有可能其中某个步骤需要经过多次循环之后才能达到既定目标。

5、测试与认证,保障商用上线。

和日常软件开发一样,需要对其进行功能、性能、长稳等层层测试,以确保能达到商用标准。此外也可以拿软件和系统到鲲鹏上做鯤鹏展翅认证,其可以扩展应用的软件使用空间并能够加入鲲鹏生态。

C/C++代码编译原理及构建流程

编译型语言,从源码到可执行程序的历程

C、C++、GO都是非常典型的编译型语言,编译型语言所开发的程序从x86平台移植到鯤鹏平台时一般都需要重新编译才能运行,这点上文也已经提到了。那么为什么需要重新编译才能运行呢?接下来举个简单的例子。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

上图为test.c的源码文件,首先经过预处理,把代码里面以#号开头的代码片断编译为预处理文件,预处理文件再经由编译生成汇编代码。汇编代码经过汇编器生成目标文件,这也就是常说的机器码。然而机器码是无法直接运行的,所以需要联接动态库或者静态库来最终生成可执行文件。

编译构建的过程

从代码工程的角度看,其可以分为两类:一类是编译构建的脚本,二是源码。在编译构建的脚本中一般会存在Makefile、cmakelist等一系列脚本文件,C和C++的源码一般是有src、tests等文件。

那么编译构建脚本类文件在迁移过程中会涉及哪些因素?一般会涉及到编译选项的移植,源码类文件会涉及到编译宏,另外可能还会有编译器自带的Builtin函数的移植、SSE intrinsic函数移植等。需要强调的是这里提到的是迁移过程中可能会涉及到的移植项,但在实际迁移过程中可能只需要编译选项或修改编译宏就可以。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

C/C++代码编译构建过程

首先要从获取源码开始,可以通过GitHub等开源社区来获取;其次需要选择所需的编译环境,就是安装编译器gcc等;之后根据源码的编译脚本生成Makefile文件,再用Makefile编译生成可持续文件。如果这部分代码之中有依赖x86平台的SO库,那么这部分的依赖库是需要重新编译替换的。在编译完成之后进行安装部署,之后进入到实际的系统之中进行测试。

典型的移植类问题

在对编译构建的流程有基本理解后,就需要深入了解实际迁移过程中所涉及到的各种移植项。

1.编译脚本和编译选项的移植

从x86到ARM,C和C++实现90%代码自动迁移的方法论

以上图为例,其中x86下-m64代码的主要功能是将应用程序编译为64位,对应到鯤鹏上是用-mabi=lp64的编译选项。上文有提到这编译选项需要在脚本中修改,对应的Cmakelists里有可能存在add_defin等多种定义方式。

再看常用的数据类型移植,众所周知x86平台上默认的char类型是一种有符号的类型,对应到鯤鹏上则是无符号类型。因此在移植过程中需要显示定义并将char类型定义为有符号。一种方法是在源代码里加上signed char,但是缺点是可能改不全从而引发一些不可预知的问题。另一种方法是直接用fsigned-char来修改,在不同架构下差异化的编译选项也可以通过gcc文档进行查询。

2.编译宏的移植

如果有相同的代码片断在不同平台下可能会存在不同分支,需要将不同架构下的性能优势发挥到较高水平。但是对于编译器来说,如何才能得知要编译哪些分支代码呢?这就是编译宏的作用。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

如果大家对大型开源软件有接触过,相信对上图这些编译选项都不会陌生。gcc编译器所自带的x86编译选项就是x86_64,对应到鯤鹏平台上是ark-64。当然除了gcc编译器自带的自定义宏,在实际场景中也需要对自研代码进行自定义,在鲲鹏上也是需要进行自定义替换。

不过针对编译宏的替换只是其中一个方面,其最重要的是对编译宏下面的代码实现移植。x86代码上有些编译器自带自定义宏,比如smd属性相关的宏在x86上是SSE开头的宏,对应到鯤鹏平台上就需要自定义它的编译宏和所相对应的分支。

3.Builtin函数移植

Builtin函数是编译器自带的函数,其在实际迁移项目中相当常见,主要是crc32校验值的计算。需要移植的普通builtin函数实际并不多,大部分需移植的builtin函数集中在SSE intrinsic函数内。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

通过上图可以看到在x86平台上其和在鯤鹏平台上是类似的,从命名来看有差异的地方就只存在于架构。

4.内联汇编函数的移植

内联汇编对于部分开发者来说平时接触的会比较少,所以又可能会感觉到陌生。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

上图列举了将字节序进行反序的例子,比如0X56781314反序输出的是0X14137856,x86上对应的是bswap指令,鯤鹏对应的是rev指令,其它有些操作和寄存器都是基于内联汇编的语法规则进行替换的。上图的另一个例子是Builtin函数,列举了内联汇编转换用鯤鹏上面的Builtin函数做替换的例子。比如popcount是对二进制数里面的1进行计数,对应到鯤鹏平台上所替换的是popcountll。

5.SSE intrinsic函数移植(SIMD技术)

关于SSE intrinsic函数的移植,在这之前需要先了解SIMD的技术。SIMD(Single Instruction Multi Data)是一种单指令处理多数据流的并行处理技术,能够在批量数据操作时进行向量 化运算加速,具有较高的执行效率,在多媒体处理、矩阵运算等场景都有广泛的应用。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

Intel的SIMD扩展指令统称SSE,主要分为三类,MMX是64位寄存器,SSE到SSE4是28位的,三是AVX256和AVX512。鯤鹏基于SIMD的技术发展比较成熟,现在有些基于开源量的NEON库主要是在图象处理和视频处理层面。

6.SSE intrinsic函数移植(MMX/SSE)

经过调用编译器就能够基于C函数调用完成对SIMD技术的应用,极大方便了开发者。

从x86到ARM,C和C++实现90%代码自动迁移的方法论

以上图为例,在x86上用的是-m64的向量,add是关键字,而且这是加法运算,后面32是数据类型。对应到鯤鹏上是int32×2然后再做加法运算,这常用的C函数规则是类似的。针对SSE指令,从内存中加载4个单精度浮点数据到寄存器,x86是load,对应到鯤鹏用的是vld1q。

7.SSE intrinsic函数移植(AVX)

AVX指令和MMX类似,只不过其位数不同。以AVX指令使用了256位寄存器运算为例,向量A和向量B中分别存储了8个单 精度浮点型(32位)。该指令将向量A和向量B中的8个数值分别相加,并将结果以返回值的形式返回 (结果中依然是8个单精度浮点型数据) ,最后再从向量寄存器中分别取出8个单精度浮点数累加得到结果。

对应到鲲鹏方面,鲲鹏处理器采用精简指令集,使用128位寄存器实现SIMD(Single Instruction Multi Data)计算。在实现本例16个浮点数的相加时,通过两条vaddq_f32指令来分别完成,每条指令完成两组共8个浮点数算,最后再从向量寄存器中分别取出8个浮点数累加。

写在最后

对于开发者而言,代码和软件迁移是一套必须要掌握的技能。尤其是各种智能终端数量暴涨,物联网飞速发展的当下,x86平台已经难以适应全生态的发展,从x86迁移至ARM平台,正是现在的大势所趋。而鲲鹏生态,则为每一位有迁移需求的开发者提供了最便利的工具和环境。没有哪一款平台是最好的,只有最适合业务的那款平台,从x86到ARM,答案正在逐渐清晰。

     (本内容属于网络转载,文中涉及图片等内容如有侵权,请联系编辑删除。市场有风险,选择需谨慎!此文仅供参考,不作买卖及投资依据。)

原创文章,作者:陈晨,如若转载,请注明出处:https://www.kejixun.com/article/496791.html

(0)
陈晨陈晨管理团队

相关推荐