为什么必须将代码从 x86 迁移到 ARM?

2020-06-18

“我们非常希望 ARM 和鲲鹏的生态能成为下一个计算产业迭代的方向。”在 6 月 8 日于成都举办的华为 DevRun 开发者沙龙——四川鲲鹏开发者嘉年华上,华为鲲鹏计算产业开源与生态营销总监梁冰对计算产业未来展望道。

新基建风口下,算力一跃成为了新的生产力,云、AI 与 5G 则是新的生产工具,技术聚变将引发商业裂变,带动各行各业的数字化快速发展。基于 5G 等新兴技术的创新应用催生多样化算力的需求,市场既需要通用计算算力也需要异构计算算力。此外,摩尔定律的逐渐放缓,让算力和性能陷入一系列发展瓶颈,市场对创新架构的需求日益加深,计算平台的创新之战一触即发。

在此背景下,x86 架构的不足越发明显,功耗大、通用寄存器数量少、计算机硬件利用率低、寻址范围小等问题凸显,难以跟上算力发展的速度。与此同时,ARM 架构在移动互联网盛行的当下却开始焕发出别样的生命力。

“基于 ARM 架构来设计的鲲鹏,其成功的前提是早早地便看到了未来计算机算力架构的迭代之路。对华为来说,不光要在中国携手合作伙伴共建鲲鹏生态,为更多企业带来价值,更为重要的是要融入全球 ARM 的生态,共同推进 ARM 进阶为下一代计算机平台的实施标准。”梁冰对鲲鹏生态的意义阐述道。

从 x86 迁移到 ARM 架构的过程并不简单,本次活动中,华为鲲鹏计算专家对于鲲鹏软件迁移路径也进行了非常全面细致的讲解,同时针对软件迁移过程中可能遇到的问题及解决方案也进行了相关讲解,还为现场开发者设置了实操演练环节,一对一指导下进行鲲鹏软件迁移线上实验。

从 x86 向鲲鹏迁移的必要性

众所周知,计算机是由软件和硬件组成的,上层的软件是通过指令集驱动下层的硬件,软件想要跑在 CPU 上的话,那它必须要有对应的指令集。

如上图所示,整个计算机的计算基础架构,从底层的晶体管、物理原材料,二极管也就是逻辑门电路,到往上走的微架构、自评级架构,再往上面走就到了操作系统所完成的二进制机器译码汇编高级语言、JavaC 等等,整个技术站从复杂到抽象,技术语言也从经历一个简单的交换指令发展为汇编进行访存和锁存,再到形成机器码,这个过程中最核心的变化在指令集上。
想要成功运行,底层计算平台就必须能够支持该 CPU 的指令才可以,这也是在 x86 和鲲鹏编译的不同之处。

如上图所示,x86 和鲲鹏使用的指令是不一样的,简单来讲,在鲲鹏上使用的是精简指令集,而在 x86 上使用的是一个复杂的指令集。综合来讲,它有三点主要的差异:

  • 架构差异:x86 和鲲鹏处理器 (aarch64) 属于不同的架构。

  • 指令集差异:x86-- 复杂指令集,鲲鹏处理器 – 精简指令集。

  • 向量寄存器差异:x86 和鲲鹏处理器使用向量寄存器不同,向量指令集也存在差异。

从这三点出发,一个在 x86 上运行的程序根本不可能毫无阻碍地就可以在鲲鹏上使用,必须经过编译,同时也证明了迁移的必要性。

迁移的五个步骤

  1. 迁移准备:信息收集 & 环境申请
    信息的收集主要分为两部分,首先是硬件信息的收集,主要就是一些型号,目的是根据此类信息匹配一个 x86 的服务器;其次是收集软件栈信息,包括操作系统、虚拟机、中间件、编译器、上层依赖的开源软件、商业软件、业务软件等信息。

  2. 迁移分析:软件栈分析 & 编程语言分析
    这一过程要做的就是对收集到的信息和软件栈做初步分析,目的是判断是否真正需要迁移,评估迁移的工作量。

对开源软件来说,因为社区发展已经比较成熟,迁移相对较为简单,通过基于鲲鹏的架构分支或者鲲鹏支持的软件包,自行安装即可。对于自研软件来讲,情况则较为复杂,如 C、C++ 这类编译型软件,从指令集差异化出发,需要重新编译后才能完成迁移,而 Java、Python 这种解释型的语言,因为其虚拟机如 JDK、PVM 已经把上层的一些跟指令集相关的东西屏蔽掉了,平滑地迁移过去即可。对于商用软件而言,不会公开源码,如果需要迁移,就必须要联系厂家去编译一个鲲鹏的版本,并且完成一系列的适配。

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

  1. 编译迁移:代码迁移 & 软件包迁移
    编译型的语言,之所以迁移起来比较困难,原因就在于涉及到比较多的迁移点,下面将从代码和软件包两方面进行阐述。

对于对指令集有依赖的代码来说,与 x86 架构相关的,都需要替换成 ARM 架构下的语言;像以 Java、Python 为代表的解释型语言,换一下 JDK 和 PVM 就行了,如果语言中调用了编译型语言,必须把这部分重新编译才可行。对于软件包来说,其迁移与 Java、Python 语言类似,即扫描出来里边的对编译型语言的依赖,把这一部分依赖进行替换即可。

  1. 性能调优:性能指标测试 & 性能优化
    经过前面几个步骤之后,软件迁移其实就基本完成了,之后要对性能进行调优,主要分为建立基准 、压力测试 、确定瓶颈、 实施优化、 确认效果五个步骤。

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

  1. 测试与认证:压力测试 & 长稳测试 & 规模商用
    调优结束之后,接下来就要把功能运力、长稳运力以及性能运力都跑一遍,达到一个商用的标准,就可以筹备上线了。此外也可以将软件和系统进行鲲鹏展翅认证,其可以扩展应用的软件使用空间并能够加入鲲鹏生态。

不同语言的迁移注意事项

实际上,像 Java、Python、Perl 这种跨平台的开发语言,它的迁移的技术难度相对来说是比较低的,主要是因为 Java 有一个 JBM,Python 中存在一个解释器,它们屏蔽了大部分的平台架构上的差异。与之相反,C 和 C++ 的难度相对来说要高一些,原因是如内敛函数还有 x86 加速的一些指令,无法支持像微软这种闭源的开发架构。

典型的如 C/C++/Go 语言,都属于编译型语言。编译型语言开发的程序在从 x86 处理器迁移到鲲鹏处理器时,必须经过重新编译才能运行。如下图所示,源码需要由编译器、汇编器翻译成机器指令,再通过链接器链接库函数才能生成机器语言程序。

接下来就是 C/C++ 代码的编译构建了,这个过程一共分为六步:

  • 获取源码:通过 github 或第三方开源社区获取

  • 准备编译环境:安装编译器 gcc 等

  • 使用源码中的 CMakeLists.txt 或 configure 脚本生成 makefile

  • 执行 makefile 编译可执行程序

  • 替换依赖库:重编译或替换依赖 x86 平台的 so 库

  • 将可执行程序安装部署到生产或测试系统

既然是最为复杂的,C/C++ 语言的迁移问题也是涉猎最广泛的,主要涉及到七类问题:

编译脚本和编译选项的移植:以 x86 下 -m64 代码为例,其主要功能是将应用程序编译为 64 位,对应到鲲鹏上是用 -mabi=lp64 的编译选项,此类编译选项需要在脚本中修改;此外, x86 平台上默认的 char 类型是一种有符号的类型,对应到鲲鹏上则是无符号类型,在移植过程中需要显示定义并将 char 类型定义为有符号,方法一是在源代码里加上 signed char,方法二是直接用 fsigned-char 来修改。

编译宏的移植:编译宏的作用就是让编译器知道编译哪些分支代码能够在不同架构下达到最优性能。如何对编译宏下面的代码实现移植?86 代码上有些编译器自带自定义宏,比如 smd 属性相关的宏在 x86 上是 SSE 开头的宏,对应到鲲鹏平台上就需要自定义它的编译宏和所相对应的分支。

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

内联汇编函数的移植:第一种是指令替换,x86 上对应的是 bswap 指令,鲲鹏对应的是 rev 指令,其它有些操作和寄存器都是基于内联汇编的语法规则进行替换的。第二种是 Builtin 函数的替换,以 x86 的指令 popcnt 为例,比如 popcount 是对二进制数里面的 1 进行计数,对应到鯤鹏平台上所替换的是 popcountll。

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

SSE intrinsic 函数移植(MMX/SSE):针对 MMX 指令,x86 上用的是 -m64 的向量做加法运算,对应到鲲鹏上是 int32×2 然后再做加法运算,类似于常用的 C 函数规则;针对 SSE 指令,从内存中加载 4 个单精度浮点数据到寄存器,x86 是 load,对应到鲲鹏用的是 vld1q。
SSE intrinsic 函数移植(AVX):以 AVX 指令使用了 256 位寄存器运算为例,向量 A 和向量 B 中分别存储了 8 个单 精度浮点型 (32 位),对应到鲲鹏处理器上,使用 128 位寄存器实现 SIMD(Single Instruction Multi Data) 进行计算。
尽管解释性语言难度降低,但 Java、Python 代码迁移过程中同样有一些问题需要注意。

如上图所示,Java 想要进行迁移的话,首先需要安装运行环境 JDK,Java 源码通过 Java 编译器之后就会生成一个 Java 的字节码,这时候可能 jar 包会有一些 SO 库的依赖,那就需要链接进行打包。

这整个的过程,存在迁移修改点的地方有二,一是 JDK 的安装,迁移过来推荐使用华为的 JDK 或者 OpenJDK;二是对 SO 库这个二进制库的依赖,如果你是在 x86 下编译的,毫无疑问,这在鲲鹏下面是没办法进行运行的,所以你需要拿到相应的源码或者找到 aarch64 的 SO 库来进行替换,最后进行重新打包。

再来看 Python 源码的迁移,其实与 Java 很类似,也是需要从编译环境和 SO 库两大方面入手修改。环境上推荐使用 A32,Python3 你也可以通过样本安装,也可以通过源码安装;SO 库有多种类型,但对于各种方式的 SO 库,最后都是对应为一个 SO,定义为 SO 库,需要的步骤也都是一致的,即装配环境、重新编译、重新替换。

不同的地方只是前面安装的是 Python 的运行环境。Python 源码同样通过解释器生成一个字节码,这时候可能我们会依赖外部的一些 Python import 模块进来,这些模块里面可能有一些 SO 库,Python 解释器的解释执行后就是部署运行了。

相对于 C++ 来说,Java/Python 的迁移点并不是太多,总结下来,主要需要注意两点:
编译器包括解释器的安装、迁移,这里有两种方式,方式一是通过 YAM 源的方式进行安装,方式二是找到源码进行编译重新安装。
x86 依赖 SO 库的情况下,需要找到 SO 库相应的源码,进行重新编译后实现替换。

写在最后

“拿华为的话说,就是自己的降落伞自己先跳。长虹大数据平台的规模比较大,有一百多个服务器,原来是一个 PB 级的数据存储,涉及到的整个生态组件有 20 多个,我们将原来存在的 1700 多万的终端业务迁移到基于鲲鹏的大数据计算平台,耗时近 5 个月。鲲鹏的架构在 HBS 场景下优势凸出,另外在存储、IO 等方面基本上是持平,总体来说,鲲鹏的芯片表现性能都还不错。”长虹计算产业事业部技术总监蒲文龙这样评价到同行者华为鲲鹏。

无独有偶,麒麟软件有限公司技术总监李洋同样赞扬道,“华为鲲鹏的代码迁移工具,包括分析扫描工具、代码迁移工具及性能优化工具,这是作为开发者一定要去了解的非常方便的工具,它可以让开发者快速地分析系统软件源代码,以及对所需要的依赖包进行编译和重构。”
对于开发者来说,迁移是一套必须掌握的技能;对于互联网企业来说,迁移是助力企业抓住未来趋势的一个必要动作。从 x86 架构到 ARM 架构,计算产业的未来已经渐渐明晰,而鲲鹏作为先行者,生态的力量也愈发强大,始终秉持着“硬件开放、软件开源、使能伙伴”的初心,逐渐发展成为滋养新计算产业的一方沃土。


写评论...