鲲鹏架构入门与实战
上QQ阅读APP看书,第一时间看更新

5.2.3 移植常见问题

在进行实际的代码移植过程中,因为环境的多样性,会遇到多种问题,下面按照源码修改和嵌入式汇编两个类别,分别对可能出现的问题进行分析并给出建议的解决方法,需要特别注意的是,给出的解决方法仅供参考,本书不对实际的使用作任何担保。

1.源码修改类问题

1)代码中汇编指令需要重写

■ 现象描述:

ARM的汇编语言与x86完全不同,需要重写,涉及使用嵌入汇编的代码,都需要针对ARM进行配套修改。

■ 处理步骤:

需要重新实现汇编代码段。

■ 示例:

在x86架构下,示例代码如下:

在鲲鹏平台下,使用gcc内置函数实现,示例代码如下:

以__sync_add_and_fetch为例,编译后其反汇编对应代码如下:

     <__sync_add_and_fetch>:
     ldxr x2, [x0]
     add x2, x2, x1
     stlxr w3, x2, [x0]

2)快速移植内联SSE/SSE2应用

■ 现象描述:

部分应用采用了gcc封装的用SSE/SSE2实现的函数,但是gcc目前没有提供对应的鲲鹏平台版本,需要实现对应函数。

■ 处理步骤:

目前已有开源代码实现了部分鲲鹏平台的函数,代码下载网址:https://GitHub.com/open-estuary/sse2neon.git,使用方法如下:

步骤1:将已下载项目中的SSE2NEON.h文件复制到待移植项目中。

步骤2:在源文件中删除如下代码:

     #include <xmmintrin.h>
     #include <emmintrin.h>

步骤3:在源代码中包含头文件SSE2NEON.h。

3)对结构体中的变量进行原子操作时程序异常coredump

■ 现象描述:

程序调用原子操作函数对结构体中的变量进行原子操作,程序coredump,堆栈如下:

■ 问题原因:

鲲鹏平台对变量的原子操作、锁操作等用到了ldaxr、stlxr等指令,这些指令要求变量地址必须按变量长度对齐,否则执行指令会触发异常,导致程序coredump。一般是因为代码中对结构体进行强制字节对齐,导致变量地址不在对齐位置上,对这些变量进行原子操作、锁操作等会触发问题。

■ 处理步骤:

代码中搜索#pragmapack关键字(该宏改变了编译器默认的对齐方式),找到使用了字节对齐的结构体,如果结构体中变量会被作为原子操作、自旋锁、互斥锁、信号量、读写锁的输入参数,则需要修改代码保证这些变量按变量长度对齐。

4)核数目硬编码

■ 问题原因:

鲲鹏服务器相对于x86服务器,CPU核数会有变化,如果模块代码针对处理器核数目硬编码,则会造成无法充分利用系统能力的情况,例如CPU核的利用率差异大或者绑核出现跨numa的情况。

■ 处理步骤:

可以通过搜索代码中的绑核接口(sched_setaffinity)来排查绑核的实现是否存在CPU核数硬编码的情况。如果存在,则根据鲲鹏服务器实际核数进行修改,消除硬编码,可通过接口sysconf(_SC_NPROCESSORS_CONF)获取实际核数再进行绑核。

5)双精度浮点型转整型时数据溢出,与x86平台表现不一致

■ 现象描述:

C/C++双精度浮点型数转整型数据时,如果超出了整型的取值范围,鲲鹏平台的表现与x86平台的表现不同。

■ 问题原因:

在两个平台下,是两套CPU架构,其中的算数逻辑单元的实现可能会有差异,操作系统、编译器的实现都会有所不同。x86(指令集)中的浮点到整型的转换指令,定义了一个indefinite integer value——“不确定数值”(64bit:0x8000000000000000),大多数情况下x86平台确实都在遵循这个原则,但是在从double向无符号整型转换时,又出现了不同的结果。鲲鹏的处理则非常清晰和简单,在上溢出或下溢出时,保留整型能表示的最大值或最小值,开发者并不会面对不确定或无法预期的结果。

■ 处理步骤:

参考如下数据转换的表格,调整代码中的实现。

double型数据向long转换,如表5-3所示。

表5-3 double型数据向long转换

double型数据向unsigned long转换,如表5-4所示。

表5-4 double型数据向unsigned long转换

double型数据向int转换,如表5-5所示。

表5-5 double型数据向int转换

double型数据向unsigned int转换,如表5-6所示。

表5-6 double型数据向unsigned int转换

2.嵌入式汇编类问题

1)替换x86 pause汇编指令

■ 现象描述:

编译报错:Error:unknown mnemonic 'pause'--'pause'。

■ 问题原因:

pause指令给处理器提供提示,以提高spin-wait循环的性能,需替换为鲲鹏平台的yield指令。

■ 处理步骤:

x86平台实现样例:

     static inline void PauseCPU()
     {
         __asm__ __volatile__("pause"::: "memory");
     }

鲲鹏平台实现样例:

     static inline void PauseCPU()
     {
          __asm__ __volatile__("yield"::: "memory");
     }

2)替换x86 pcmpestri汇编指令

■ 现象描述:

编译报错Error:unknown mnemonic 'pcmpestri-'-'pcmpestri'。

■ 问题原因:

与pcmpestrm指令类似,pcmpestri也是x86 SSE4指令集中的指令。根据指令介绍,其用途是根据指定的比较模式,判断字符串str2的字节是否在str1中出现,返回匹配到的位置索引(首个匹配结果为0的位置)。同样,对于该指令,需要彻底了解其功能,通过C代码重新实现其功能。

指令介绍:

https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=SSE4_2&expand=834

https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visualstudio-2010/bb531465(v=vs.100)。

■ 处理步骤:

如下代码段是Impala中对pcmpestri指令的调用,该调用参考Intel的_mm_cmpestri接口实现将pcmpestri指令封装成SSE4_cmpestri,代码如下:

从指令介绍中看,不同的模式所执行的操作差异较大,完全实现指令功能所需代码行太多。结合代码中对接口的调用,实际使用到的模式为PCMPSTR_EQUAL_EACH|PCMPSTR_UBYTE_OPS|PCMPSTR_NEG_POLARITY。即按照字节长度进行匹配,对str1与str2做对应位置字符是否相等判断,若相等,则将对应bit位置置1,最后输出首次出现1的位置。根据该思路进行代码实现,代码如下:

3)替换x86 movqu汇编指令

■ 现象描述:

编译报错:unknown mnemonic 'movqu'--'movqu'。

■ 问题原因:

movqu为x86指令集中的指令,在鲲鹏上无法使用。该指令可以实现寄存器到寄存器,寄存器到地址的数据复制。x86上movqu指令用法有两种:

第一种是将xmm2寄存器或者128位内存地址的内容复制到xmm1寄存器,代码如下:

     MOVDQU xmm1, xmm2/m128

第二种是将xmm1寄存器的内容复制到128位内存地址或者xmm2寄存器,代码如下:

     MOVDQU xmm2/m128, xmm1

参考资料:https://x86.puri.sm/html/file_module_x86_id_184.html。

■ 处理步骤:

对于第一种调用,可以用NEON指令ld1替代:

ld1指令Load multiple 1-element structures to one,two,three or four registers。

     LD1 {Vt.T}, [Xn|SP]

可参考指令集手册的9.98节,下载网址:

http://infocenter.arm.com/help/topic/com.arm.doc.dui0802a/DUI0802A_armasm_reference_guide.pdf。

对于第二种调用,可以用st1指令来替代:

st1指令Store multiple 1-element structures from one,two three or four registers.

     ST1 {Vt.T}, [Xn|SP]

可参考指令集手册的9.202节,下载网址:

http://infocenter.arm.com/help/topic/com.arm.doc.dui0802a/DUI0802A_armasm_reference_guide.pdf。

以下是一个简单的示例,代码如下:

4)替换x86 pand汇编指令

■ 现象描述:

编译报错:unknown mnemonic 'pand'--'pand'。

■ 问题原因:

pand是x86指令集中的指令,无法在鲲鹏设备上使用。其功能是按位进行and运算,使用方法有两种:

第一种用法是对寄存器xmm2或内存地址中128位内容与xmm1进行按位与运算,结果存放于xmm2中,指令用法如下:

     PAND xmm1, xmm2/m128

第二种用法是对寄存器mm2或内存地址中64位内容与mm1进行按位与运算,结果存放于mm2中,指令用法如下:

     PAND mm1, mm2/m64

指令使用方法参考:https://c9x.me/x86/html/file_module_x86_id_230.html。

■ 处理步骤:

对于以上两种情况,在鲲鹏上均可以用NEON指令AND替换,采用64或者128位长度的向量寄存器存放数据,代码如下:

     AND Vd.<T>, Vn.<T>, Vm.<T>

Bitwise AND(vector).Where<T>is 8B or 16B(though an assembler shouldaccept any valid format)。

其中Vn、Vm为待操作的寄存器,Vd是目的寄存器,<T>即是选择寄存器位数。

参考指令集手册的9.7节,下载网址:http://infocenter.arm.com/help/topic/com.arm.doc.dui0802a/DUI0802A_armasm_reference_guide.pdf。

下面是一个简单的使用NEON指令AND对数据进行按位与操作的过程,供参考,代码如下:

5)替换x86 pxor汇编指令

■ 现象描述:

编译报错:unknown mnemonic 'pxor'--'pxor'。

■ 问题原因:

pxor是x86指令集中的指令,无法在鲲鹏设备上使用。其功能是按位进行xor运算,使用方法有两种:

第一种用法是对寄存器xmm2或内存地址中128位内容与xmm1进行按位异或运算,结果存放于xmm1中,指令用法如下:

     PXOR xmm1, xmm2/m128

第二种用法是对寄存器mm2或内存地址中64位内容与mm1进行按位异或运算,结果存放于mm1中,指令用法如下:

     PXOR mm1, mm2/m64

指令用法参考网址:https://c9x.me/x86/html/file_module_x86_id_272.html。

■ 处理步骤:

对于以上两种情况,在鲲鹏上均可以用NEON指令EOR替换,采用64或者128位长度的向量寄存器存放数据,代码如下:

     EOR Vd.<T>, Vn.<T>, Vm.<T>

Bitwise exclusive OR(vector).Where<T>is 8B or 16B(an assembler shouldaccept any valid arrangement)。其中Vn、Vm为待操作的寄存器,Vd是目的寄存器,<T>是选择寄存器位数。参考指令集手册的9.29节,下载网址为http://infocenter.arm.com/help/topic/com.arm.doc.dui0802a/DUI0802A_armasm_reference_guide.pdf。

下面是一个简单的使用NEON指令EOR对数据进行按位异或操作的过程,参考代码如下:

6)替换x86 pshufb指令

■ 现象描述:

编译报错:unknown mnemonic 'pshufb'--'pshufb'。

■ 问题原因:

pshufb(Packed Shuffle Bytes)指令的功能是根据第二个操作数指定的控制掩码对第一个操作数执行散列操作,产生一个组合数。它是x86平台的汇编指令,在鲲鹏平台上需要进行替换。x86上的指令用法如下:

     pshufb xmm1, xmm2/m128

■ 处理步骤:

pshufb指令对应的SSE intrinsic函数是_mm_shuffle_epi8,因此pshufb在鲲鹏上的替换可以分为两步:

步骤1:将pshufb汇编指令替换成SSE intrinsic。

x86上实现样例,代码如下:

     __asm__("pshufb %1, %0": "+x" (mmdesc): "xm" (shuf_mask));

在鲲鹏上先替换成SSE intrinsic函数,代码如下:

     _mm_shuffle_epi8(mmdesc, shuf_mask);

步骤2:移植内联SSE函数_mm_shuffle_epi8。gcc目前没有提供对应的鲲鹏平台版本,因此需要实现对应函数,代码如下:

7)替换x86 cpuid汇编指令

■ 现象描述:

编译报错:/tmp/ccfaVZfw.s:Assembler messages:/tmp/ccfa VZfw.s:34:Error:unknown mnemonic 'cpuid'--'cpuid'。

■ 问题原因:

cpuid是x86平台上专有的获取cpuid信息的汇编指令,在鲲鹏平台上需要重写。在鲲鹏平台上,midr_el1寄存器里存放的是cpuid信息,可以通过读寄存器获取cpuid。

■ 处理步骤:

x86实现样例,代码如下:

midr_el1是64位寄存器,其中高32位为预留位,其值为0。读出来是一个32位的值。鲲鹏平台上可替换成的代码如下:

8)替换x86 xchgl汇编指令

■ 现象描述:

编译报错:{standard input}:Assembler messages:{standard input}:1222:Error:unknown mnemonic 'xchgl'--'xchgl x1,[x19,112]'。

■ 问题原因:

xchgl是x86上的汇编指令,作用是交换寄存器/内存变量和寄存器的值,如果交换的两个变量中有内存变量,则会对内存变量增加原子锁操作。鲲鹏上可用GCC的原子操作接口__atomic_exchange_n替换。__atomic_exchange_n的第3个入参是内存屏障类型,使用者可以根据自身代码逻辑选择不同的屏障。当对多线程访问临界区的逻辑不清晰时,建议使用__ATOMIC_SEQ_CST屏障,避免由屏障使用不当带来一致性问题。

■ 处理步骤:

x86实现样例,代码如下:

鲲鹏上可替换成的代码如下:

9)替换x86 cmpxchgl汇编指令

■ 现象描述:

编译报错:{standard input}:Assembler messages:{standard input}:1222:Error:unknown mnemonic 'cmpxchgl'

■ 问题原因:

与xchgl类似,cmpxchgl是x86上的汇编指令,其作用是比较并交换操作数。鲲鹏上无对应指令,可用GCC的原子操作接口__atomic_compare_exchange_n进行替换。

■ 处理步骤:

x86实现样例,代码如下:

鲲鹏上可替换成的代码如下:

10)替换x86 rep汇编指令

■ 现象描述:

编译报错:Error:unknown mnemonic 'rep'--r'ep'。

■ 问题原因:

rep为x86平台的重复执行指令,需替换为鲲鹏平台的rept指令。

■ 处理步骤:

修改方法参考如下:

x86实现样例,代码如下:

     #define nop __asm__ __volatile__("rep;nop": : :"memory")

鲲鹏平台实现样例,本样例实现空指令,参数n为循环次数,代码如下:

     #define __nops(n) ".rept " #n "\nnop\n.endr\n"
     #define nops(n) asm volatile(__nops(n))

11)替换x86 bswap汇编指令

■ 现象描述:

编译报错:Error:unknown mnemonic 'bswap'--'bswap x3'。

■ 问题原因:

bswap是x86平台的字节序反序指令,需替换为鲲鹏平台的rev指令。

■ 处理步骤:

在x86平台下的实现,代码如下:

在鲲鹏平台下的实现,代码如下:

12)替换x86 crc32汇编指令

■ 现象描述:

编译错误:Error:unknown mnemonic c'rc32q'--c'rc32q(x3),x2'或operand 1should be an integer register--c'rc32b [sp,11],x0'或unrecognized command line option-'msse4.2'。

■ 问题原因:

x86平台使用的是crc32b、crc32w、crc32l、crc32q汇编指令完成CRC32C校验值计算

功能,而鲲鹏平台使用crc32cb、crc32ch、crc32cw、crc32cx 4个汇编指令完成CRC32C校验值计算功能。

■ 处理步骤:

使用crc32cb、crc32ch、crc32cw、crc32cx取代x86的CRC32系列汇编指令,替换方法如表5-7所示,并在编译时添加编译参数-march=armv8+crc。

表5-7 替换方法

■ 示例:

在x86平台下的实现,代码如下:

在鲲鹏平台下的实现,代码如下:

13)替换x86 rdtsc汇编指令

■ 现象描述:

编译报错:error:impossible constraint in a'sm'__asm__ __volatile__("rdtsc":"=a"(lo),"=d"(hi));

■ 问题原因:

TSC是时间戳计数器的缩写,它是Pentium兼容处理器中的一个计数器,它记录自启动以来处理器消耗的时钟周期数。在每个时钟到来时,该计数器自动加1。因为TSC随着处理器周期速率的变化而变化,所以它提供了非常高的精确度。它经常被用来分析和检测代码。x86平台TSC的值可以通过rdtsc指令来读取,而鲲鹏平台需要使用类似算法实现。

■ 处理步骤:

x86平台实现样例,代码如下:

鲲鹏平台实现样例:

方法一:使用Linux提供的获取时间函数clock_gettime进行近似替换,代码如下:

方法二:鲲鹏有Performance Monitors Control Register系列寄存器,其中PMCCNTR_EL0类似于x86的TSC寄存器。但默认情况下用户态是不可读的,需要内核态使能后才能读取。具体可参考网址http://iLinux Kernel.com/?p=1755。

a.下载read aarch64 TSC(http://www.iLinux Kernel.com/files/aarch64_tsc.tar.bz2),解压压缩包,在aarch64_tsc目录下执行make命令,安装相应内核驱动,生成文件,生成文件中包括一个文件名为pmu.ko的文件。

b.执行insmod pmu.ko命令安装内核模块,使能内核态(初次执行即可)。

c.代码替换。

示例代码如下:

其中Cent Speed和External Clock的值可由以下命令获取:

     dmidecode |grep MHz

14)替换x86 popcntq汇编指令

■ 现象描述:

编译报错:Error:unknown mnemonic 'popcnt'--'popcnt [sp,8],x0'。

■ 问题原因:

popcnt为x86平台的位1计数指令,鲲鹏平台无对应指令,需使用替换算法实现。

■ 处理步骤:

x86平台实现样例,代码如下:

鲲鹏平台实现样例,代码如下:

15)替换x86 atomic原子操作函数

■ 现象描述:

部分应用会通过封装汇编指令实现原子操作,如原子加及原子减。由于指令集差异,x86上所使用的原子操作指令在ARM平台并不能保证原子性,因此需要进行相应替换。

①atomic_add指令

函数功能:对整数变量进行原子加。

处理步骤:

x86平台实现样例,代码如下:

在鲲鹏上进行替换:

第1种方法:使用GCC自带原子操作替换,代码如下:

第2种方法:使用内联汇编替换,代码如下:

②atomic_sub指令

函数功能:对整数变量进行原子减。

处理步骤:

x86平台实现样例,代码如下:

在鲲鹏上进行替换:

第1种方法:使用GCC自带原子操作替换,代码如下:

第2种方法:使用内联汇编替换,代码如下:

③atomic_dec_and_test指令

函数说明:对整数进行减操作,并判断执行原子减后结果是否为0。

处理步骤:

x86平台实现样例,代码如下:

在鲲鹏上进行替换:

第1种方法:使用GCC自带原子操作函数替换,代码如下:

第2种方法:使用内联汇编替换,代码如下:

④atomic_inc_and_test指令

函数说明:对整数进行加操作,并判断返回结果是否为0。

处理步骤:

x86平台实现样例,代码如下:

在鲲鹏上进行替换:

第1种方法:使用GCC自带原子操作函数替换,代码如下:

第2种方法:使用内联汇编替换,代码如下:

⑤atomic64_add_and_return指令

函数说明:对两个长整数进行加操作,并将结果作为返回值返回。

处理步骤:

需要重新实现汇编代码段。

在x86平台实现样例,代码如下:

在鲲鹏平台下,使用GCC内置函数实现,代码如下:

16)替换x86 pcmpestrm汇编指令

■ 现象描述:

编译报错Error:unknown mnemonic 'pcmpestrm'--'pcmpestrm'。

■ 问题原因:

pcmpestrm指令是x86指令集中SSE4中的指令。根据指令介绍,其用途是根据指定的比较模式,判断字符串str2的字节是否在字符串str1中出现,将每个字节的对比结果返回(最大长度为16字节)。该指令是典型的x86复杂指令,通过一条指令即可完成复杂的字符串匹配功能,鲲鹏架构中无类似实现。对于这种指令,需要彻底了解其功能,通过C代码重新实现其功能。

指令介绍:

https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=SSE4_2&expand=835。

https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visualstudio-2010/bb514080(v=vs.100)。

■ 处理步骤:

以下代码段是Impala中对pcmpestrm指令的调用,该调用参考Intel的_mm_cmpestrm接口实现将pcmpestrm指令封装成SSE4_cmpestrm,代码如下:

从指令介绍中看,不同的模式所执行的操作差异较大,完全实现指令功能所需代码行太多。结合代码中对接口的调用,实际使用到的模式为PCMPSTR_EQUAL_ANY|PCMPSTR_UBYTE_OPS。即按照字节长度进行匹配,对比字符串str2中的每个字符是否在字符串str1中出现,若出现,则将对应bit位置置1。

根据识别到的功能进行代码实现,代码如下:

注意:无直接替代指令的场景,需要结合指令功能、所需功能共同分析,切忌生搬硬套直接代码复制及替换。

注意:5.2.3节移植常见问题内容引用自华为《鲲鹏代码迁移参考手册》4.2节和4.3节,网址为http://ic-openlabs.huawei.com/chat/download/鲲鹏代码迁移参考手册.pdf。