• [技术干货] 计算机工作过程详解笔记
    计算机的工作过程分为以下三个步骤:1)把程序和数据装入主存储器。2)将源程序转换成可执行文件。3)从可执行文件的首地址开始逐条执行指令。1、从源程序到可执行文件      在计算机中编写的C语言程序,都必须被转换为一系列的低级机器指令,这些指令按照一种称为可执行目标文件的格式打好包,并以二进制磁盘文件的形式存放起来。      以UNIX系统中的GCC编译器程序为例,读取源程序文件 hello.c,并把它翻译成一个可执行目标文件hello,整个翻译过程可分为4个阶段完成,如图      1)预处理阶段:预处理器(cpp)对源程序中以字符#开头的命令进行处理,例如将#include命令后面的.h文件内容插入程序文件。输出结果是一个以.i为扩展名的源文件 hello.i。      2)编译阶段:编译器(ccl)对预处理后的源程序进行编译,生成一个汇编语言源程序hello.s。汇编语言源程序中的每条语句都以一种文本格式描述了一条低级机器语言指令。      3)汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一个称为可重定位目标文件的 hello.o,它是一种二进制文件,因此在文本编辑器中打开它时会显示乱码。      4)链接阶段:链接器(ld)将多个可重定位目标文件和标准库函数合并为一个可执行目标文件,或简称可执行文件。本例中,链接器将hello.o和标准库函数 printf 所在的可重定位目标模块printf.o合并,生成可执行文件hello。最终生成的可执行文件被保存在磁盘上。2、指令执行过程的描述      程序中第一条指令的地址置于PC 中,根据PC取出第一条指令,经过译码、执行步骤等,控制计算机各功能部件协同运行,完成这条指令的功能,并计算下一条指令的地址。用新得到的指令地址继续读出第二条指令并执行,直到程序结束为止。      比如取数指令(即将指令地址码指示的存储单元中的操作数取出后送至运算器的ACC中)信息流程如下:1)取指令:PC→MAR→M→MDR→IR      根据PC取指令到IR。将PC的内容送MAR,MAR中的内容直接送地址线,同时控制器将读信号送读/写信号线,主存根据地址线上的地址和读信号,从指定存储单元读出指令,送到数据线上,MDR从数据线接收指令信息,并传送到IR中。2)分析指令:OP(IR)→CU      指令译码并送出控制信号。控制器根据R中指令的操作码,生成相应的控制信号,送到不同的执行部件。在本例中,IR中是取数指令,因此读控制信号被送到总线的控制线上。3)执行指令:Ad(IR)→MAR→M→MDR→ACC      取数操作。将IR中指令的地址码送 MAR,MAR中的内容送地址线,同时控制器将读信号送读/写信号线,从主存指定存储单元读出操作数,并通过数据线送至MDR,再传送到ACC中。      此外,每取完一条指令,还须为取下一条指令做准备,形成下一条指令的地址,即(PC)+1→PC。注意:(PC)指程序计数器PC中存放的内容。PC→MAR应理解为(PC)→MAR,即程序计数器中的值经过数据通路送到MAR,也即表示数据通路时括号可省略(因为只是表示数据流经的途径,而不强调数据本身的流动)。但运算时括号不能省略,即(PC)+1-PC 不能写为PC+1→PC。
  • [技术干货] 【技术长文】计算机程序和计算机语言
    什么是计算机程序 ?计算机程序是为了告诉计算机"做某件事或解决某个问题"而用"***计算机语言***编写的命令集合(语句)只要让计算机执行这个程序,计算机就会自动地、有条不紊地进行工作,计算机的一切操作都是由程序控制的,离开程序,计算机将一事无成现实生活中你如何告诉别人如何做某件事或者解决某个问题?通过人能听懂的语言: 张三你去楼下帮我买一包烟, 然后顺便到快递箱把我的快递也带上来其实我们通过人能听懂的语言告诉别人做某件事就是在发送一条条的指令计算机中也一样, 我们可以通过计算机语言告诉计算机我们想做什么, 每做一件事情就是一条指令, 一条或多条指令的集合我们就称之为一个计算机程序什么是计算机语言 ?在日常生活、工作中, 语言是人们交流的工具中国人和中国人交流,使用中文语言美国人和美国人交流,使用英文语言人想要和计算机交流,使用计算机语言可以看出在日常生活、工作中,人们使用的语言种类很多如果一个很牛人可能同时掌握了中文语言和英文语言, 那么想要和这个人交流既可以使用中文语言,也可以使用英文语言计算机其实就是一个很牛的人, 计算机同时掌握了几十门甚至上百门语言, 所以我们只要使用任何一种计算机已经掌握的语言就可以和计算机交流常见的计算机语言类型有哪些 ?机器语言所有的代码里面只有0和1, 0表示不加电,1表示加电(纸带存储时 1有孔,0没孔)优点:直接对硬件产生作用,程序的执行效率非常非常高缺点:指令又多又难记、可读性差、无可移植性汇编语言符号化的机器语言,用一个符号(英文单词、数字)来代表一条机器指令优点:直接对硬件产生作用,程序的执行效率非常高、可读性稍好缺点:符号非常多和难记、无可移植性高级语言非常接近自然语言的高级语言,语法和结构类似于普通英文优点:简单、易用、易于理解、远离对硬件的直接操作、有可移植性缺点:有些高级语言写出的程序执行效率并不高对比(利用3种类型语言编写1+1)机器语言10111000 00000001 00000000 00000101 00000001 00000000汇编语言MOV AX, 1 ADD AX, 1高级语言1 + 1————————————————版权声明:本文为CSDN博主「极客江南」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_44617968/article/details/117656810
  • [问题求助] 急:鲲鹏920 ARM64 GCC 9.3.1 如何编译ARMV7 GNU风格 C和ARM32混合汇编
    【功能模块】背景: 现有大量ARM V7 32位C和gnu风格混合汇编代码需要移植到kunpeng920平台上,原计划以独立32位进程方式运行在ARM64平台上,使用X86交叉编译工具和kunpeng920 ARM64平台原生gcc编译工具都遇到不识别-Wa,-mimplicit-it=thumb -march=armv7-a,不识别以下gnu风格C混合汇编的问题。 __asm__ __volatile__ ("MRS %2, CPSR\n"    "MOV %0, %3, LSL %4\n"        "MOV %1, %0, ASR %4\n"    "TEQ %3, %1\n"    "EORNE %0,%5,%3,ASR#31\n"    "ORRNE %2, %2, 0x08000000\n"    "MSRNE CPSR_f, %2\n"      :"+r" (L_var_out),"+r"(tmp0),"+r"(Overflow):"r"(L_var1),"r"(var2),"r"(tmp));       }这些混合汇编(包含thumb,neon指令)可在gcc version 4.6.3  编译通过,并能在多核arm-v7 cortex-A7处理器上稳定运行。升级到kunpeng920是为了在国芯上获得更好的性能,但是-Wa,-mimplicit-it=thumb -march=armv7-a 传给汇编器的时候提示不支持,如何解决?有什么变通方案?由于这些媒体处理混合汇编太多了,重写是不现实的。先谢!【操作步骤&问题现象】1、2、【截图信息】--------编译不过的ARM64 kunpeng920 gcc信息[root@sbc-arm ~]# gcc -vUsing built-in specs.COLLECT_GCC=gccCOLLECT_LTO_WRAPPER=/opt/aarch64/compiler/gcc-9.3.1-2021.03-aarch64-linux/bin/../libexec/gcc/aarch64-linux-gnu/9.3.1/lto-wrapperTarget: aarch64-linux-gnuConfigured with: /usr1/cloud_compiler_hcc/build/hcc_arm64le_native/../../open_source/hcc_arm64le_native_build_src/gcc-9.3.0/configure --prefix=/usr1/cloud_compiler_hcc/build/hcc_arm64le_native/arm64le_build_dir/gcc-9.3.1-2021.03-aarch64-linux --enable-shared --enable-threads=posix --enable-checking=release --enable-__cxa_atexit --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,fortran,lto --enable-initfini-array --enable-gnu-indirect-function --with-multilib-list=lp64 --enable-multiarch --with-gnu-as --with-gnu-ld --enable-libquadmath --with-pkgversion='Kunpeng gcc 1.3.2.b023' --with-sysroot=/ --with-gmp=/usr1/cloud_compiler_hcc/build/hcc_arm64le_native/arm64le_build_dir/gcc-9.3.1-2021.03-aarch64-linux --with-mpfr=/usr1/cloud_compiler_hcc/build/hcc_arm64le_native/arm64le_build_dir/gcc-9.3.1-2021.03-aarch64-linux --with-mpc=/usr1/cloud_compiler_hcc/build/hcc_arm64le_native/arm64le_build_dir/gcc-9.3.1-2021.03-aarch64-linux --with-isl=/usr1/cloud_compiler_hcc/build/hcc_arm64le_native/arm64le_build_dir/gcc-9.3.1-2021.03-aarch64-linux --libdir=/usr1/cloud_compiler_hcc/build/hcc_arm64le_native/arm64le_build_dir/gcc-9.3.1-2021.03-aarch64-linux/lib64 --disable-bootstrap --build=aarch64-linux-gnu --host=aarch64-linux-gnu --target=aarch64-linux-gnuThread model: posixgcc version 9.3.1 (Kunpeng gcc 1.3.2.b023) --------编译通过的ARM32  ARM-V7(处理器cortex-A7) gcc信息Using built-in specs.COLLECT_GCC=./gccTarget: arm-linux-gnueabiConfigured with: /scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/src/gcc-linaro-4.6-2012.02/configure --build=i686-build_pc-linux-gnu --host=i686-build_pc-linux-gnu --target=arm-linux-gnueabi --prefix=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/install --with-sysroot=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/install/arm-linux-gnueabi/libc --enable-languages=c,c++,fortran --disable-multilib --with-arch=armv7-a --with-tune=cortex-a9 --with-fpu=vfpv3-d16 --with-float=softfp --with-pkgversion='crosstool-NG linaro-1.13.1-2012.02-20120222 - Linaro GCC 2012.02' --with-bugurl=https://bugs.launchpad.net/gcc-linaro --enable-__cxa_atexit --disable-libmudflap --disable-libgomp --disable-libssp --with-gmp=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static --with-mpfr=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static --with-mpc=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static --with-ppl=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static --with-cloog=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static --with-libelf=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static --with-host-libstdcxx='-L/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/.build/arm-linux-gnueabi/build/static/lib -lpwl' --enable-threads=posix --disable-libstdcxx-pch --enable-linker-build-id --enable-gold --with-local-prefix=/scratch/cbuild/slave/slaves/oort14/crosstool-ng-linaro-1.13.1-2012.02-20120222/crosstool-ng/default/c/builds/arm-linux-gnueabi-linux/install/arm-linux-gnueabi/libc --enable-c99 --enable-long-long --with-mode=thumbThread model: posixgcc version 4.6.3 20120201 (prerelease) (crosstool-NG linaro-1.13.1-2012.02-20120222 - Linaro GCC 2012.02) 【日志信息】(可选,上传日志内容或者附件)gcc -O3  -fno-builtin-round -mglibc   -c -o basic_op.o basic_op.c/tmp/ccaReFAu.s: Assembler messages:/tmp/ccaReFAu.s:21: Error: unknown or missing system register name at operand 2 -- `mrs x4,CPSR'/tmp/ccaReFAu.s:22: Error: unexpected characters following instruction at operand 2 -- `mov x0,x2,LSL x1'/tmp/ccaReFAu.s:23: Error: unexpected characters following instruction at operand 2 -- `mov x3,x0,ASR x1'/tmp/ccaReFAu.s:24: Error: unknown mnemonic `teq' -- `teq x2,x3'/tmp/ccaReFAu.s:25: Error: unknown mnemonic `eorne' -- `eorne x0,x5,x2,ASR#31'/tmp/ccaReFAu.s:26: Error: unknown mnemonic `orrne' -- `orrne x4,x4,0x08000000'/tmp/ccaReFAu.s:27: Error: unknown mnemonic `msrne' -- `msrne CPSR_f,x4'
  • [问题求助] ARM汇编代码编译不通过
    自己开发的C应用程序包含ARM汇编代码,想运行在鲲鹏920上。用gcc-linaro-5.5.0-2017.10-x86_64_aarch64-linux-gnu.tar.xz 在64位Ubuntu下面交叉编译不通过。遇到的错误信息是不能识别“-mimplicit-it=thumb”。请问怎么解决? 
  • [技术干货] X86 xchgl和cmpxchgl指令替换案例分享
    【问题背景】客户迁移过程中,编译自研代码时,有如果两个编译报错:不识别xchgl汇编指令{standard input}: Assembler messages:{standard input}:1222: Error: unknown mnemonic `xchgl' -- `xchgl x1,[x19,112]'{standard input}:1225: Error: unknown mnemonic `xchgl' -- `xchgl x0,[x19,88]' 不识别cmpxchgl汇编指令{standard input}: Assembler messages:{standard input}:1222: Error: unknown mnemonic `cmpxchgl'【原因分析】xchgl和 cmpxchgl都是X86上的指令集,ARM64上不识别,需要进行替换。xchgl指令的作用是交换 (寄存器/内存变量)和 (寄存器) 的值。如果交换的两个变量中有内存变量,会对内存变量增加原子锁操作。详细见以下释义: cmpxchgl指令的作用是比较并交换两数。详细见官网释义:【解决方案】1.1 内存屏障选择xchgl和 cmpxchgl这两个指令在ARM64上可以用GCC的原子操作接口进行替换。在使用GCC内置的原子操作函数__atomic_xxxx_n时,输出参数包含memory order(即通常我们所说的内存屏障)。GCC4.7前,__sync 同步原语中默认的内存模型为full barrier模型,__sync原语前后的读写操作均不可做指令重排。为提高流水线执行效率,GCC 4.7合入C++11的内存模型,通过__atomic 同步原语,由使用者控制需要的屏障级别。对多线程访问临界区的逻辑不清晰时,建议仍使用__ATOMIC_SEQ_CST屏障,避免由屏障使用不当带来一致性问题。下面是几种可选的屏障类型及其简要介绍: Memory orderInstroduction__ATOMIC_RELAXED__ATOMIC_RELAXED__ATOMIC_CONSUMEload操作,当前线程依赖该原子变量的访存操作不能reorder到该指令之前,对其他线程store操作(release)可见__ATOMIC_ACQUIREload操作,当前线程所有访存操作不能reorder到该指令之前,对其他线程store操作(release)可见__ATOMIC_RELEASEstore操作,当前线程所有访存操作不能reorder到该指令之后,对其他线程load操作(consume)可见__ATOMIC_ACQ_RELload/store操作,memory_order_acquire + memory_order_release__ATOMIC_SEQ_CSTmemory_order_acq_rel + 顺序一致性(sequential consisten) 1.2 xchgl替换方法xchgl在交换两值时,其中有一个是内存变量,替换时要对内存变量加原子锁。arm上没有可以完全替换的汇编指令,可以使用GCC的原子操作接口__atomic_exchange_n进行替换。X86实现样例:inline int nBasicAtomicInt::fetchAndStoreOrdered(int newValue){    /* 原子操作, 把_value的值和newValue     * 交换, 且返回_value原来的值     */    asm volatile("xchgl %0,%1"                 : "=r" (newValue), "+m" (m_value)                 : "0" (newValue)                 : "memory");    return newValue;} TaiShan上可替换成:inline int nBasicAtomicInt::fetchAndStoreOrdered(int newValue){    /* 原子操作, 把_value的值和newValue     * 交换, 且返回_value原来的值     */    return __atomic_exchange_n(&_q_value, newValue, __ATOMIC_SEQ_CST);}   1.3 cmpxchgl替换方法cmpxchgl可用GCC的原子操作接口__atomic_compare_exchange_n进行替换。与xchgl类似,GCC原子操作接口的参数, 使用者可以根据自身代码逻辑选择合适的参数。X86实现样例:inline bool nBasicAtomicInt::testAndSetOrdered(int expectedValue, int newValue){    unsigned char ret;    /* 原子操作, 原来m_value的值如果等于expectedValue,则把newValue     * 载入m_value, 且返回ret=true; 如果不等于,则m_value的值不变,且返回ret=false     */    asm volatile("lock\n"                 "cmpxchgl %3,%2\n"                 "sete %1\n"                 : "=a" (newValue), "=qm" (ret), "+m" (m_value)                 : "r" (newValue), "0" (expectedValue)                 : "memory");    return ret != 0;}TaiShan上可替换成:inline bool nBasicAtomicInt::testAndSetOrdered(int expectedValue, int newValue){    unsigned char ret;    /* 原子操作, 原来m_value的值如果等于expectedValue,则把newValue载入_value, 且返回ret=true; 如果不等于,则m_value的值不变,且返回ret=false */    return __atomic_compare_exchange_n(&m_value, &expectedValue, newValue, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST);} 【总结】客户自研代码迁移时,如果遇到汇编指令报错(编译日志中有“Assembler messages”关键字),而ARM64上没有功能完全匹配的汇编指令时,除了可以用汇编指令的组合替换之外,还可以尝试使用GCC的内置函数替换
  • [技术干货] C++入门
    一、C++概念C++是一种面向对象的计算机程序设计语言,由美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现,最初它被称作“C with Classes”(包含类的C语言)。C++它是一种静态数据类型检查的、支持多重编程范式的通用程序设计语言,支持过程化程序设计、数据抽象、面向对象程序设计、泛型程序设计等多种程序设计风格。C++是C语言的继承,进一步扩充和完善了C语言,成为一种面向对象的程序设计语言二、C++关键字C++中总共63个关键字,包括了C语言中32个关键字三、C++命名空间在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。1.命名空间的定义定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。2.命名空间的使用C++为了防止命名冲突,把自己库里面的东西都定义在一个std的命名空间中要使用标准库里面的东西,有三种方式指定命名空间–麻烦,每个地方都要指定,但也是最规范的方式代码如下:int c = 100;namespace N{    int a = 10;    int b = 20;    int Add(int left, int right)    {        return left + right;    }    int Sub(int left, int right)    {        return left - right;    }}把std整个展开,相当于库里面的东西全部到全局域里面去了,使用起来方便但是可能会有与自己命名空间定义的冲突,规范工程中不推荐这种,日常练习可以用这种。代码如下:using namespace std;对部分常用的库里面的东西展开->针对1和2的折中方案,项目中也经常使用代码如下:using std::cout;using std::endl;int main(){    printf("%d\n", N::a);    printf("%d\n", N::b);    printf("%d\n", N::Add(1, 2));    printf("%d\n", N::Sub(1, 2));    int c = 10;    printf("%d\n", c);   //局部变量优先,所以c为10    printf("%d\n", ::c); //指定访问左边域,空白表示全局域}四、C++输入&&输出使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。使用C++输入输出更方便,不需增加数据格式控制,比如:整形–%d,字符–%costream 类型全局对象,istream 类型全局对象 ,endl全局的换行符号代码如下:struct Person{    char name[10];    int age;};int main(){    std::cout << "bit education ";    std::cout << "bit education" << std::endl;    //cout与cin对比C语言printf\scanf 来说可以自动识别类型(函数重载+运算符重载)    int a = 10;    int* p = &a;    printf("%d,%p\n", a, p);    std::cout << a << "," << p << std::endl;    std::cin >> a;    printf("%d\n", a);    char str[100];    std::cin >> str;  //cin不用&,因为引用    std::cout << str << std::endl;        struct Person P = { "uzi", 23 };  //格式化输出printf比cout好    printf("name:%s age:%d\n", P.name, P.age);    std::cout << "name:" << P.name<<" age:"<< P.age << "\n";}五、C++缺省参数1.缺省参数的概念1.缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参代码如下:void TestFunc(int a = 0){    cout << a << endl;}int main(){    TestFunc(); // 没有传参时,使用参数的默认值    TestFunc(10); // 传参时,使用指定的实参}2.缺省参数的分类半缺省参数代码如下:void testFunc3(int a, int b = 10, int c = 20){    cout << "a = " << a << endl;    cout << "b = " << b << endl;    cout << "c = " << c << endl;}全缺省参数代码如下:void testFunc2(int a = 10, int b = 20, int c = 30){    cout << "a = " << a << endl;    cout << "b = " << b << endl;    cout << "c = " << c << endl;}正常参数代码如下:void testFunc1(int a = 0){    std::cout << a << std::endl;}int main(){    testFunc1(10);    testFunc2();    testFunc3(1);    return 0;}注意:半缺省参数必须从右往左依次来给出,不能间隔着给缺省参数不能在函数声明和定义中同时出现缺省值必须是常量或者全局变量C语言不支持(编译器不支持)六、C++函数重载1.函数重载概念函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。2.函数重载实现代码如下:int Add(int left, int right){    return left + right;}double Add(double left, double right){    return left + right;}int main(){    cout << Add(10, 20) << endl;    cout << Add(10.5, 20.0) << endl;    //fun();    return 0;}注意(特别重要): 缺省参数缺省参数符合重载的定义,但如果调用的时候编译器不识别函数重载调用哪个函数,所以分情况讨论。代码如下:void fun(int a, int b, int c = 10){}void fun(int a, int b){}3.函数命名规则–>C++支持重载,C不支持为什么C++支持函数重载,而C语言不支持函数重载呢?在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程 序员个人的程序库,将其需要的函数也链接到程序中。其中编译和链接也分为几个步骤:其中分为更细的话:在C++调用Add函数在C下调用Add函数通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。4.extern "C"的作用有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译,所以这个函数不能进行重载。代码如下:extern "C" int Add(int left, int right);int main(){    Add(1, 2);    return 0;}七、C++引用1.引用的概念引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间代码如下:int main(){    int x = 10;    int &y = x;    y = 20;    std::cout << "y=" << y << std::endl;    int &z = y;    z = 30;    std::cout << "z=" << z << std::endl;}2.引用的特性引用在定义时必须初始化一个变量可以有多个引用引用一旦引用一个实体,再不能引用其他实体3.常引用代码如下:void TestConstRef(){    //常引用是创建一个临时变量,引用名是临时变量的引用    const int a = 10;    //int& ra = a; // 该语句编译时会出错,a为常量,而且a为不可以修改    const int& ra = a;    // int& b = 10; // 该语句编译时会出错,b为常量    const int& b = 10;    double d = 12.34;    //int& rd = d; // 该语句编译时会出错,类型不同    const int& rd = d;}rc是临时空间的别名代码如下:int c=10;double d=1.11;const double& rc=c;4.引用的使用场景1.做参数代码如下:void Swap2(int& a, int& b) //通过引用来交换{    int tmp = a;    a = b;    b = tmp;}void Swap1(int* a, int *b) //通过指针来交换{    int tmp = *a;    *a = *b;    *b = tmp;}2.做返回值代码如下:int& Add(int a, int b){    int c = a + b;    return c;}int main(){    int& ret = Add(1, 2);    Add(3, 4);    cout << "Add(1, 2) is :" << ret << endl;    return 0;}如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回5.传值、传引用效率比较以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。6.引用和指针的区别在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间,在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。八、C++内联函数1.内联函数概念以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。2.内联函数特性代码如下:#define _CRT_SECURE_NO_WARNINGS   1#include<iostream>int Add2(int left, int right){    return left + right;}inline int Add1(int left, int right){    return left + right;}int main(){    int ret1, ret2;    ret1 = Add1(1, 2);    ret2 = Add1(1, 2);    std::cout << ret1 << std::endl;    std::cout << ret2 << std::endl;    return 0;}inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。九、C++auto关键字1.auto关键字概念在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。2.auto关键字的使用代码如下:int main(){    int x = 10;    auto a = &x;  // int*     auto* b = &x; // int*    int& y = x;   // y的类型是什么?int    auto c = y;  // int     auto& d = x; // d的类型是int, 但是这里指定了d是x的引用    // 打印变量的类型    cout << typeid(x).name() << endl;    cout << typeid(y).name() << endl;    cout << typeid(a).name() << endl;    cout << typeid(b).name() << endl;    cout << typeid(c).name() << endl;    cout << typeid(d).name() << endl;    return 0;}auto与指针和引用结合起来使用,用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&在同一行定义多个变量当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。3.auto关键字不能使用场景auto不能作为函数的参数代码如下:// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导void TestAuto(auto a){}auto不能直接用来声明数组代码如下:void TestAuto(){  int a[] = {1,2,3};  auto b[] = {4,5,6};}为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。十、基于范围的for循环(C++11)1.范围for的语法在C++98中如果要遍历一个数组,可以按照以下方式进行:代码如下:int main(){    int array[] = { 1, 2, 3, 4, 5 };    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)    {        cout << array[i] << " ";    }    cout << endl;}    return 0;}对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。代码如下:int main(){    // 范围for C++11新语法遍历,更简单,数组都可以    // 自动遍历,依次取出array中的元素,赋值给e,直到结束    for (auto& e : array)    {        e *= 2;    }    for (auto ee : array)    {        cout << ee << " ";    }    cout << endl;}2.范围for的使用条件for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在大家了解一下就可以了)十一、指针空值nullptr(C++11)1.程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。2.在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下3.将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。总结以上就是今天要讲的内容,本文仅仅简单介绍了C++入门的简单知识,虽然这么知识范围很大,但也为我们以后学习C++有更好的了解,我们务必掌握。另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。
  • [技术干货] 【转载技术长文】精简指令集计算机
    转自维基百科(https://zh.wikipedia.org/wiki/%E7%B2%BE%E7%AE%80%E6%8C%87%E4%BB%A4%E9%9B%86%E8%AE%A1%E7%AE%97%E6%9C%BA) 精简指令集计算机(英语:reduced instruction set computer,缩写:RISC)或简译为精简指令集,是计算机中央处理器的一种设计模式。这种设计思路可以想像成是一家模块化的组装工厂,对指令数目和寻址方式都做了精简,使其实现更容易,指令并行执行程度更好,编译器的效率更高。目前常见的精简指令集微处理器包括DEC Alpha、ARC、ARM、AVR、MIPS、PA-RISC、Power ISA(包括PowerPC、PowerXCell)、RISC-V和SPARC等。 目录 1 历史 2 精简指令集之前的设计原理 3 RISC设计原理 4 提升中央处理器性能的方法 5 参考 历史 精简指令集的名称最早来自1980年大卫·帕特森在加州大学柏克莱分校主持的Berkeley RISC计划。但在他之前,已经有人提出类似的设计理念。由约翰·科克主持,在1975年开始,1980年完成的IBM 801计划,可能是第一个使用精简指令集理念来设计的系统。 这种设计思路最早的产生缘自于有人发现,尽管传统处理器设计了许多特性让代码编写更加便捷,但这些复杂特性需要几个指令周期才能实现,并且常常不被运行程序所采用。此外,处理器和主内存之间运行速度的差别也变得越来越大。在这些因素促使下,出现了一系列新技术,使处理器的指令得以流水执行,同时降低处理器访问内存的次数。 早期,这种指令集的特点是指令数目少,每条指令都采用标准字长、执行时间短、中央处理器的实现细节对于机器级程序是可见的等等。 实际上在后来的发展中,RISC与CISC(复杂指令集)在竞争的过程中相互学习,现在的RISC指令集也达到数百条,运行周期也不再固定。虽然如此,RISC设计的根本原则——针对流水线化的处理器优化——没有改变,而且还在遵循这种原则的基础上发展出RISC的一个并发化变种VLIW(包括Intel EPIC),就是将简短而长度统一的精简指令组合出超长指令,每次运行一条超长指令,等于并发运行多条短指令。 另一方面,目前最常见的复杂指令集x86 CPU,虽然指令集是CISC的,但因对常用的简单指令会以硬件线路控制尽全力加速,不常用的复杂指令则交由微码循序器“慢慢解码、慢慢跑”,因而有“RISCy x86”之称。 精简指令集之前的设计原理 在早期的计算机业界,编译器技术并不发达,程序多半以机器语言或汇编语言完成的。为了便于编写程序,计算机体系结构师设计出越来越复杂的指令,可以直接对应高级编程语言的高级功能。当时的看法是硬件比编译器更容易设计,所以结构的复杂性在硬件这端。 加速这种复杂化的另一因素是缺乏大容量的内存。在内存容量受限的应用中,具有极高消息密度的程序更加实用。当时内存中的每一字节都很宝贵,例如只有几千个字节来存储某个完整系统。它使产业界倾向于高度编码的指令、长度不等的指令、多操作数的指令,以及把数据的搬移与计算合并在一起的指令。在当时看来,相对于使指令更容易解码,指令的编码打包问题尤为重要。 还有一个因素是当时的内存不仅容量少,而且速度很慢,使用的都是磁性技术。凭借高密度打包的指令,访问慢速资源的频率可以降低。 微处理器只有少量寄存器的两个原因是: 寄存器每一个比特位都比外部内存贵。以当时的集成电路技术水准,大量寄存器对芯片或电路板而言是难以承受的。 一旦具有大数量的寄存器,相关的指令字(opcode)将会需要更多的比特位(使用宝贵的RAM)来定位寄存器。 基于上述原因,微处理器设计师尽可能使指令做更多的工作。这导致单个指令做全部的工作:读入两个加数,相加,并将计算结果直接写入内存;另一个例子是从内存读取两个数据,但计算结果存储在寄存器内;第三个例子是从内存和寄存器各读取一个数据,其结果再次写入内存;以此类推。这种微处理器设计原理,在精简指令集(RISC)的思路出现后,最终被人称为复杂指令集。 当时设计的一个通常目标是为每个指令都提供所有的寻址模式,称为“正交性”。这给微处理器增加了一些复杂性,但理论上每个可能的命令均可单独调整。相对于使用更简单的指令,这样做能够使设计速度更快。 这类设计最终可以由功率谱的两端来表述,6502在一端,VAX在功率谱的另一端。单价25美元的1MHz 6502芯片只有一个通用寄存器,但它非常精简的单周期内存访问接口允许一个字节宽度的操作,其效率和使用更高时钟频率的设计一致,例如主频4MHz的Zilog Z80使用相同慢速的记忆芯片(大约近似300ns)。另一方面,VAX则是一种小型机,它的每个CPU至少需要三个机架来放置。其显著特点是,它支持的内存访问模式数目多得惊人,并且每条指令都可以使用任一种模式。 RISC设计原理 1970年代后期,IBM(以及其它类似企业组织)的研究人员显示,大多数正交寻址模式基本上已被程序员所忽略。这是编译器的使用逐渐增多而汇编语言的使用相对减少所导致的。值得注意的是,由于编写编译器的难度很大,当时编译器并不能充分利用CISC处理器所提供的各种特性。尽管如此,广泛应用编译器的趋势已然很明显,从而使得正交寻址模式变得更加无用。 这些复杂操作很少被使用。事实上,相比用更精简的一系列指令来完成同一个任务,用单一复杂指令甚至会更慢。这看上去有些自相矛盾,却源自于微处理器设计者所花的时间和精力:设计者一般没有时间去调整每一条可能被用到的指令,通常他们只优化那些常用的指令。一个恶名昭著的例子是VAX的INDEX指令,执行它比执行一个循环还慢。 几乎就在同时,微处理器开始比内存运行得更快。即便是在七十年代末,人们也已经认识到这种不一致性至少会在下一个十年继续增加,到时微处理器将会比内存的速度快上百倍。很明显,需要有更多寄存器(以及后来的缓存)来支持更高频率的操作。为此,必须降低微处理器原本的复杂度,以节省出空间给新增的寄存器和缓存。 不过RISC也有它的缺点。当需要一系列指令用来完成非常简单的程序时,从存储器读入的指令总数会变多,因此也需要更多时间。在当时的工业和设计领域,对RISC的性能优劣有大量持续不断的争论。 提升中央处理器性能的方法 增加寄存器的大小 增进内部的平行性 增加高速缓存大小 增加核心时脉的速度,但是此举便会导致IC从晶体管取电的功率增加,因此要遵照IC的状况予以增加核心时脉才行,此动作类似于超频。 加入其它功能,如I/O和计时器 加入向量处理器(SIMD),如VISAltiVec、SSE(Streaming SIMD Extensions) 避免附加。使朝向省电化(battery-constrained)或小型化的应用 集成多个核心 硬件多线程技术 精简指令集设计中常见的特征: 统一指令编码(例如,所有指令中的op-code永远位于同样的比特位置、等长指令),可快速解译: 泛用的寄存器,所有寄存器可用于所有内容,以及编译器设计的单纯化(不过寄存器中区分了整数和浮点数); 单纯的寻址模式(复杂寻址模式以简单计算指令序列取代); 硬件中支持少数资料类型(例如,一些CISC电脑中存有处理字节字符串的指令。这在RISC电脑中不太可能出现)。
  • [技术干货] 鲲鹏应用开发——基于编译型语言
    编译型语言应用执行过程大部分应用可以通过重新编译即可移植到鲲鹏平台预处理命令: gcc -E hello.c -o hello.i,预处理完成后使用命令: cat hello.i可以看到预处理后的代码编译命令: gcc -s hello.i -o hello.s汇编命令: gcc -c hello.c -o hello.o链接处理可分为:静态链接:函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者—组相关函数的代码。动态链接:函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。编译移植–参数处理不同架构下差异化GCC编译选项查询(gcc7.3为例)可在如下链接查看:链接:https://gcc.gnu.org/onlinedocs/gcc-7.3.0/gcc/Submodel-Options.html#Submodel-Options-mabi=lp64在gcc4.8.5不支持,推荐gcc7.3及以上版本编译编译–宏处理汇编指令替换–C/C++中内联汇编语法内联的汇编语句以asm开头,有时也会使用_asm_,后面可加参数”_volatile__"表示编译器不要优化代码,后面的指令保留原样汇编指令替换–替换方法方法一:如果有相同功能的ARM汇编指令,则直接替换方法二:如果没有相同功能的ARM汇编指令,则重新开发代码来替换
  • [技术干货] Python 笔记 02 —— 程序设计语言
    ## 1.2 程序设计语言 [点击跳转 -> 目录列表](https://bbs.huaweicloud.com/forum/thread-118776-1-1.html) ### 1.2.1 程序设计语言概述 程序设计语言是计算机能够理解和识别用户操作意图的一种交互体系,它按照特定规则组织计算机指令,使计算机能够自动进行各种运算处理。 **程序设计语言分为3个大类:** - **机器学习** - 机器语言是一种二进制语言,它直接使用二进制代码表达指令,是计算机硬件可以直接识别和执行的程序设计语言。 - **汇编语言** - 汇编语言是一种使用助记符与机器语言中的指令进行一一对应,在计算机发展早起能帮助程序员提高编程效率。 - **高级语言** - 高级语言是一种接近自然语言的一种计算机程序设计语言,可以更容易地描述计算问题并利用计算机解决计算问题。 > **扩展:通用汇编语言** > > 通用汇编语言指能够用于编写多种用途程序的编程语言(相对于专用编程语言)。 > > 例如Python 语言是一个通用语言,可以用于编写各种类型的应用,该语言的语法没有专门用于特定应用的程序元素。 > > HTML语言则是一个专用编写语言,它利用超链接将文本、图像、音/视频等资源组成起来形成Web页面。尽管有些编程员不包含针对特定应用的程序元素,但由于语言所应用的领域比较狭窄,也被认为是专用编程语言。 > > 编程语言: > > C、C++、C#、Go、Java、Python > > 专用编程语言: > > HTML、JavaScript、MATLAB、PHP、SQL、Verilog ### 1.2.2 编译和解释 **高级语言按照计算机执行方式的不同可分成两类:静态语言和脚本语言。** - 静态语言采用编译执行 - 脚本语言采用解释执行 **编译**是将源代码转换成目标代码的过程,通常源代码是高级语言代码,目标代码是机器语言代码,执行变异的饿及刷机此功能需称为编译器。 **解释**是将源代码逐条转换成目标代码同时逐条运行目标代码的过程。 **编译和解释的区别**: - 编译是一次性翻译,一旦程序被编译,不再需要编译程序或者源代码。 - 解释则实在每次程序执行时都需要解释器和源代码。 - 这两者的区别类似外语资料翻译和实时的同声传译。 **采用编译的优点:** - 对于相同源代码,编译所产生的的目标代码执行速度更快。 - 目标代码不要编译器就可以运行,在同类型操作系统上使用灵活。 **采用解释的优点:** - 解释执行需要保留源代码,程序纠错和维护十分方便。 - 只要 存在结婚时期,源代码可以在任何操作系统上运行,可移植性高。 > 采用编译执行的编程员是静态语言,例:C、Java > > 采用结婚时执行的编程语言是脚本语言,例:JavaScript、PHP > > Python语言是一种被广发使用的高级通过脚本编程语言,虽采用解释执行方式,但它的解释器也保留了编译器的部分功能随着程序运行,解释器也会生成一个完整代码。 [返回目录](https://bbs.huaweicloud.com/forum/thread-118776-1-1.html)
  • [技术干货] 汇编语言之8088的寻址方式分享
    1.指令bai集:cpu能够执行的指令的集合。2.指令:cpu所能够执行的操作。3.操作数:参加指令运算的数据。决定操作数地址的寻址方式1、立即寻址                          MOV    DX,8000H2、直接寻址                          MOV    AX,DS:[2000H]3、寄存器寻址                      MOV    DS,AX4.寄存器间接寻址               MOV    BX,[BP]5、寄存器相对寻址               MOV    AL,[SI-2]6、基址、变址寻址               MOV    AX,[BP][DI]7、基址、变址、相对寻址    MOV    AX,DISP[BX][SI]8.隐含寻址                          MOV    BL决定转移地址的寻址方式下列四种形式:  (1)段内相对转移                      JMP    SHORT     ARTX  (2)段内间接转移                     JMP     CX ;JMP     WORD    PTR   [BX]   (3)段间直接转移                           JMP    FAR   PTR   ADD1  (4)段间间接转移                     JMP         DWORD       PTR      [BP][DI]
  • [技术讨论] 编译器语言程序 从源码到可执行程序的过程
    编译器语言 从源码到可执行程序的过程C、C++   编译器   汇编语言程序  汇编器  目标文件 (目标库的库函数文件)   链接器   可执行的代码   加载器  存储器1。将源码文件(C C++ GO语言)通过编译器编译成汇编语言程序2.汇编器将汇编语言程序会编程成目标文件3.链接器负责把前面的目标文件结合目标库的库函数文件  根据操作系统程序规范链接成可执行的二进制文件4.在执行上述机器文件时  加载器将其加载内存中交给机器执行。
  • 开源软件优化-优化编译器编译规则提升软件性能
    本文基于分析Go社区在ARM64平台对浮点变量比较的优化方案,向读者介绍如何通过增加编译规则使编译器“更聪明”,获得更优的指令组合,从而提升软件的执行速度。编译器的作用是将高级语言的源代码翻译为低级语言的目标代码。通常为了便于优化处理,编译器会将源代码转换为中间表示形式(Intermediate representation),很多编译优化过程都是作用在这个形式上,如下面将介绍的通过给编译器添加编译规则优化性能。在编译Go语言代码时通常使用Go语言编译器,它包括语法分析、AST变换、静态单赋值SSA PASS、机器码生成等多个编译过程。其中在生成SSA中间表示形式后进行了多个编译优化过程PASS,每个PASS都会对SSA形式的函数做转换,比如deadcode elimination会检测并删除不会被执行的代码和无用的变量。在所有PASS中lower会根据编写好的优化规则将SSA中间表示从与体系结构(如X86、ARM等)无关的转换为体系结构相关的,这是通过添加大量编译规则实现的,是本文的主要关注点。1. 浮点变量比较场景浮点数在应用开发中有广泛的应用,如用来表示一个带小数的金额或积分,经常会出现浮点数与0比较的情况,如向数据库录入一个商品时,为防止商品信息错误,可以检测录入的金额是否大于0,当用户购买产品时,可能需要先做一个验证,检测账户上金额是否大于0,如果满足再去查询商品信息、录入订单等,这样可以在交易的开始阶段排除一些无效或恶意的请求。很多直播网站会举行年度活动,通过榜单展现用户活动期间累计送出礼物的金额,排名靠前的用户会登上榜单。经常用浮点数表示累计金额,活动刚开始时,需要屏蔽掉积分小于等于0的条目,可能会用到如下函数:func comp(x float64, arr []int) {    for i := 0; i < len(arr); i++ {        if x > 0 {            arr[i] = 1        }    }}使用Go compile工具查看该函数的汇编代码(为便于理解,省略了部分无用代码):go tool compile -S main.go"".comp STEXT size=80 args=0x20 locals=0x0 leaf        0x0000 00000 (main.go:3)        TEXT    "".comp(SB), LEAF|NOFRAME|ABIInternal, $0-32#-------------------------将栈上数据取到寄存器中------------------------------..................................        0x0000 00000 (main.go:4)        MOVD    "".arr+16(FP), R0         // 取数组arr长度信息到寄存器R0中..................................        0x0004 00004 (main.go:4)        MOVD    "".arr+8(FP), R1           // 取数组arr地址值到寄存器R1中        0x0008 00008 (main.go:4)        FMOVD   "".x(FP), F0                 // 将参数x放入F0寄存器        0x000c 00012 (main.go:4)        MOVD    ZR, R2                          // ZR表示0,此处R2 清零#---------------------------for循环执行逻辑----------------------------------        0x0010 00016 (main.go:4)        JMP     24                                   // 第一轮循环直接跳到条件比较 不增加i        0x0014 00020 (main.go:4)        ADD     $1, R2, R2                       // i++        0x0018 00024 (main.go:4)        CMP     R0, R2                            // i < len(arr) 比较        0x001c 00028 (main.go:4)        BGE     68                                   // i == len(arr) 跳转到末尾#--------if x > 0---------        0x0020 00032 (main.go:5)        FMOVD   ZR, F1                         // 将0复制到浮点寄存器F1        0x0024 00036 (main.go:5)        FCMPD   F1, F0                          // 将浮点寄存器F0和F1中的值进行比较#--------arr[i] = 1-------        0x0028 00040 (main.go:5)        CSET    GT, R3                            // 如果F0 > F1 : R3 = 1        0x002c 00044 (main.go:5)        CBZ     R3, 60                             // R3 == 1 即 x <= 0 跳转到60        0x0030 00048 (main.go:6)        MOVD    $1, R3                         // x > 0        0x0034 00052 (main.go:6)        MOVD    R3, (R1)(R2<<3)         // 将切片中值赋值为1        0x0038 00056 (main.go:6)        JMP     20                                  // 跳转到20 即循环操作i++处#--------x <= 0 跳到i++----        0x003c 00060 (main.go:6)        MOVD    $1, R3                         // x <= 0        0x0040 00064 (main.go:5)        JMP     20......................................................................................................................................................可以看到对于浮点数与0的比较,上述代码首先将0放入F1寄存器,之后使用FCMPD命令将F0寄存器中的变量值x与F1寄存器中的0值进行比较:这里对汇编性能优化有一定基础的读者可能会产生疑问,为什么一个浮点变量与常数0的比较要都放入寄存器才能进行,这里需要了解ARMV8的浮点数比较指令FCMP,它有两种用法: 1. 将两个浮点寄存器中的值进行比较; 2. 将一个浮点寄存器中的值与数值0比较;可以看到对于FCMP指令,虽然浮点数与几乎所有常量比较都必须先放入寄存器中,但与0比较是一个特例,不需要将0放入一个浮点寄存器中,可以直接使用FCMP F0, $(0) 进行比较,因此上述生成的汇编代码并不是最优的2. 优化编译规则提升浮点变量比较性能看起来是个不复杂但大量出现的问题,编译器却做不到最优化,让代码爱好者倍感失望,怎么解决呢?下面是Go语言社区浮点值变量与0比较的编译规则优化案例,它通过简单地增加编译规则给编译器赋能:image优化后所有的浮点值变量在与0的比较运算中都会受益。为便于读者直观的看到具体的SSA中间表示和优化前后的变化,下面通过Go编译器工具查看详细的编译过程,编译器会将SSA PASS的详细过程记录到一个ssa.html文件,使用浏览器打开后能够直观的看到每个SSA PASS对中间表示形式的修改,先看下编译规则优化前的效果图:imageSSA PASS过程很多,主要关注最后一幅图,它是SSA PASS执行完的最终形式,注意图中v24和v20处,优化前将常量0放入寄存器F1中,将数组元素放入寄存器F0中,然后才会调用FCMPD浮点比较指令比较F1和F0中的值,并根据比较结果更新状态寄存器:image现在问题已经很清晰了,优化的目的就是期望将两条指令:FMOVD (0.0), F1和FCMPDF1,F0转变为一条指令FCMPD(0.0), F0在这个优化中让编译器更智能的技术就是如下的SSA编译规则,采用S-表达式形式,它的作用就是找到匹配的表达式并转换为编译器期望的另一种效率更高或体系结构相关的表达式,如下图所示:image上述优化中让编译器变得更聪明是因为他新学习到了下面的转换规则,初步理解编译规则语法后不难理解:// 将浮点数与0比较优化为表达式"FCMP $(0.0), Fn"(FCMPS x (FMOVSconst [0])) -> (FCMPS0 x)                              // 32位浮点数x与常数0比较 -> FCMPS0 x(FCMPS (FMOVSconst [0]) x) -> (InvertFlags (FCMPS0 x))         // 常数0与32位浮点数x比较 -> (FCMPS0 x) 结果取反(FCMPD x (FMOVDconst [0])) -> (FCMPD0 x)                            // 64位浮点数x与常数0比较 -> FCMPD0 x(FCMPD (FMOVDconst [0]) x) -> (InvertFlags (FCMPD0 x))       // 常数0与64位浮点数x比较 -> (FCMPD0 x) 结果取反-------------------比较结果取反规则-----------------------(LessThanF (InvertFlags x)) -> (GreaterThanF x)                        // 取反(a < b) -> a > b(LessEqualF (InvertFlags x)) -> (GreaterEqualF x)                      // 取反(a <= b) -> a >= b(GreaterThanF (InvertFlags x)) -> (LessThanF x)                        // 取反(a > b) -> a < b(GreaterEqualF (InvertFlags x)) -> (LessEqualF x)                      // 取反(a >= b) -> a <= b注:由于不涉及,为便于理解,在上述例子中忽略了 <type>-类型、[auxint]-变量值、{aux}-非数值变量值、 [&& extra conditions]:-条件表达式等常用的表达式类型字段,感兴趣的读者可以根据上文列举的资料进一步探究细致的读者可能已经意识到另一个问题了,规则里面有两个精简的操作码FCMPS0和FCMPD0,他们为什么更优呢?首先根据名字也许已经猜到他们分别表示单精度浮点数(32bit)与0比较和双精度浮点数(64bit)与0比较,具体含义如下图所示:image现在读者已经了解了编译器SSA规则优化的各个组成部分,整理一下思路,将各部分串联起来可以画出如下精简的架构图,在Go编译器中编译规则优化是SSA PASS的重要组成部分,他帮助编译器将一些体系结构无关的通用表达式转换为更高效的表达式,如对于冗余的条件判断取反表达式,去掉取反操作,直接对判断条件取反,如invert(<=)转变为>,体系结构无关表达式转为与体系结构(ARM64、X86等)相关的表达式:image增加上述编译规则,将编译器更新到最新版,再生成SSA PASS执行结果图,可以看到最终两条指令变成了一条:image3. 代码详解下面是优化代码详解: 1. 操作码定义,这里增加了两个新的浮点数与0比较操作码://--------------定义一个寄存器的输入参数掩码,此处fp表示所有浮点数寄存器----------------fp1flags  = regInfo{inputs: []regMask{fp}}..............................//--------------------------增加两个浮点数与0比较的操作码----------------------------// 定义操作FCMPS0,将浮点寄存器中的参数(float32)与0进行比较,使用汇编指令FCMPS{name: "FCMPS0", argLength: 1, reg: fp1flags, asm: "FCMPS", typ: "Flags"},// 定义操作FCMPD0,将浮点寄存器中的参数(float64)与0进行比较,使用汇编指令FCMPD{name: "FCMPD0", argLength: 1, reg: fp1flags, asm: "FCMPD", typ: "Flags"},2.          根据操作码自动生成opGen.go:{    name:   "FCMPS0",                       // 操作名    argLen: 1,                                     // 参数个数    asm:    arm64.AFCMPS,                 // 对应的机器指令,此处为ARM64平台的FCMPS    reg: regInfo{        inputs: []inputInfo{                  // 支持的输入参数寄存器            {0, 9223372034707292160},  // F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31        },    },},{    name:   "FCMPD0",                       // 操作名    argLen: 1,                                     // 参数个数    asm:    arm64.AFCMPD,                 // 对应的机器指令,此处为ARM64平台的FCMPD    reg: regInfo{        inputs: []inputInfo{                  // 支持的输入参数寄存器            {0, 9223372034707292160},  // F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 F31        },    },},3.          操作码FCMPS0和FCMPD0转为prog形式,每个prog对应具体的一条指令,链接时会用到。   case ssa.OpARM64FCMPS0,                          // FCMPS0 -> FCMPS $(0.0), F0        ssa.OpARM64FCMPD0:                            // FCMPD0 -> FCMPD $(0.0), F0        p := s.Prog(v.Op.Asm())                          // FCMPS | FCMPD        p.From.Type = obj.TYPE_FCONST             // $(0.0) 的类型为常数        p.From.Val = math.Float64frombits(0)    // 比较的数 $(0.0)        p.Reg = v.Args[0].Reg()                           // 第二个源操作数,即用于比较的浮点数寄存器F04.          在ARM64.rules中加入新的SSA编译规则// 将浮点数与0比较优化为表达式"FCMP $(0.0), Fn"(FCMPS x (FMOVSconst [0])) -> (FCMPS0 x)                               // 32位浮点数x与常数0比较 -> FCMPS0 x(FCMPS (FMOVSconst [0]) x) -> (InvertFlags (FCMPS0 x))          // 常数0与32位浮点数x比较 -> (FCMPS0 x) 结果取反(FCMPD x (FMOVDconst [0])) -> (FCMPD0 x)                             // 64位浮点数x与常数0比较 -> FCMPD0 x(FCMPD (FMOVDconst [0]) x) -> (InvertFlags (FCMPD0 x))        // 常数0与64位浮点数x比较 -> (FCMPD0 x) 结果取反-------------------比较结果取反规则-----------------------(LessThanF (InvertFlags x)) -> (GreaterThanF x)                        // 取反(a < b) -> a > b(LessEqualF (InvertFlags x)) -> (GreaterEqualF x)                      // 取反(a <= b) -> a >= b(GreaterThanF (InvertFlags x)) -> (LessThanF x)                        // 取反(a > b) -> a < b(GreaterEqualF (InvertFlags x)) -> (LessEqualF x)                      // 取反(a >= b) -> a <= b5.          根据ARM64.rules自动生成的Go转换代码://--------------在lower pass中以下规则会挨个进行匹配,匹配后执行转换----------------case OpARM64FCMPD:    return rewriteValueARM64_OpARM64FCMPD_0(v)case OpARM64FCMPS:    return rewriteValueARM64_OpARM64FCMPS_0(v)case OpARM64GreaterEqualF:    return rewriteValueARM64_OpARM64GreaterEqualF_0(v)case OpARM64GreaterThanF:    return rewriteValueARM64_OpARM64GreaterThanF_0(v)case OpARM64LessEqualF:    return rewriteValueARM64_OpARM64LessEqualF_0(v)case OpARM64LessThanF:    return rewriteValueARM64_OpARM64LessThanF_0(v)//------------------------x(float64)与0比较 转为 FCMPD0 x------------------------func rewriteValueARM64_OpARM64FCMPD_0(v *Value) bool {    b := v.Block    _ = b    // match: (FCMPD x (FMOVDconst [0]))    // cond:    // result: (FCMPD0 x)    for {        _ = v.Args[1]        x := v.Args[0]        v_1 := v.Args[1]        if v_1.Op != OpARM64FMOVDconst {            break        }        if v_1.AuxInt != 0 {                                                                   // 如果不是与0比较,则退出            break        }        v.reset(OpARM64FCMPD0)                                                      // 修改OpARM64FCMPD指令为OpARM64FCMPD0        v.AddArg(x)        return true    }    // match: (FCMPD (FMOVDconst [0]) x)    // cond:    // result: (InvertFlags (FCMPD0 x))    for {        _ = v.Args[1]        v_0 := v.Args[0]        if v_0.Op != OpARM64FMOVDconst {            break        }        if v_0.AuxInt != 0 {                                                                    // 如果不是与0比较,则退出            break        }        x := v.Args[1]        v.reset(OpARM64InvertFlags)                                                   // 修改OpARM64FCMPD指令为OpARM64InvertFlags        v0 := b.NewValue0(v.Pos, OpARM64FCMPD0, types.TypeFlags) // 添加一个表示OpARM64FCMPD0指令的value(SSA表示一个值)        v0.AddArg(x)        v.AddArg(v0)        return true    }    return false}//------------------------x(float32)与0比较 转为 FCMPS0 x------------------------func rewriteValueARM64_OpARM64FCMPS_0(v *Value) bool {    b := v.Block    _ = b    // match: (FCMPS x (FMOVSconst [0]))    // cond:    // result: (FCMPS0 x)    for {        _ = v.Args[1]        x := v.Args[0]        v_1 := v.Args[1]        if v_1.Op != OpARM64FMOVSconst {            break        }        if v_1.AuxInt != 0 {                              // 如果操作数不为0,退出            break        }        v.reset(OpARM64FCMPS0)                  // 修改OpARM64FCMPS指令为OpARM64FCMPS0        v.AddArg(x)        return true    }    // match: (FCMPS (FMOVSconst [0]) x)    // cond:    // result: (InvertFlags (FCMPS0 x))    for {        _ = v.Args[1]        v_0 := v.Args[0]        if v_0.Op != OpARM64FMOVSconst {            break        }        if v_0.AuxInt != 0 {            break        }        x := v.Args[1]        v.reset(OpARM64InvertFlags)        v0 := b.NewValue0(v.Pos, OpARM64FCMPS0, types.TypeFlags)        v0.AddArg(x)        v.AddArg(v0)        return true    }    return false}//--------------带反转标志的浮点数比较:invert(x >= 0) 转为 x <= 0----------------func rewriteValueARM64_OpARM64GreaterEqualF_0(v *Value) bool {    // match: (GreaterEqualF (InvertFlags x))    // cond:    // result: (LessEqualF x)    for {        v_0 := v.Args[0]        if v_0.Op != OpARM64InvertFlags { // 不需反转,此转换规则不适用,退出继续后面的规则            break        }        x := v_0.Args[0]        v.reset(OpARM64LessEqualF)         // 修改OpARM64GreaterEqualF指令为OpARM64LessEqualF        v.AddArg(x)        return true    }    return false}//--------------带反转标志的浮点数比较操作:invert(x > 0) 转换 x < 0-----------------func rewriteValueARM64_OpARM64GreaterThanF_0(v *Value) bool {    // match: (GreaterThanF (InvertFlags x))    // cond:    // result: (LessThanF x)    for {        v_0 := v.Args[0]        if v_0.Op != OpARM64InvertFlags { // 不需反转,此转换规则不适用,退出继续后面的规则            break        }        x := v_0.Args[0]        v.reset(OpARM64LessThanF)          // 修改OpARM64GreaterThanF指令为OpARM64LessThanF        v.AddArg(x)        return true    }    return false}//-------------带反转标志的浮点数比较操作:invert(x <= 0) 转为 x >= 0----------------func rewriteValueARM64_OpARM64LessEqualF_0(v *Value) bool {    // match: (LessEqualF (InvertFlags x))    // cond:    // result: (GreaterEqualF x)    for {        v_0 := v.Args[0]        if v_0.Op != OpARM64InvertFlags { // 不需反转,此转换规则不适用,退出继续后面的规则            break        }        x := v_0.Args[0]        v.reset(OpARM64GreaterEqualF)    // 修改OpARM64LessEqualF指令为OpARM64GreaterEqualF        v.AddArg(x)        return true    }    return false}//-------------带反转标志的浮点数比较操作:invert(x < 0) 转为 x > 0----------------func rewriteValueARM64_OpARM64LessThanF_0(v *Value) bool {    // match: (LessThanF (InvertFlags x))    // cond:    // result: (GreaterThanF x)    for {        v_0 := v.Args[0]        if v_0.Op != OpARM64InvertFlags { // 不需反转,此转换规则不适用,退出继续后面的规则            break        }        x := v_0.Args[0]        v.reset(OpARM64GreaterThanF)     // 替换OpARM64LessThanF指令为OpARM64GreaterThanF        v.AddArg(x)        return true    }    return false}4. 动手实验感兴趣的读者可以按照本章自己动手执行一遍,体验编译规则优化如何帮助编译器变得更聪明: - 环境准备 1. 硬件配置:鲲鹏(ARM64)云Linux服务器-通用计算增强型KC1 kc1.2xlarge.2(8核|16GB) 2. Go语言发行版 1.12.1 — 1.12.17,此处开发环境准备请参考文章:Go在ARM64开发环境配置 3. Go语言github源码仓库下载,此处通过Git安装和使用进行版本控制。 4. 测试代码 5. 编译规则代码生成工具•             操作步骤# 准备一个测试目录如/usr/local/src/cd /usr/local/src# 拉取测试用例代码git clone https://github.com/OptimizeLab/sample# 进入compile/ssa/opt_float_cmp_0_by_SSA_rule/srccd /usr/local/src/sample/compile/ssa/opt_float_cmp_0_by_SSA_rule/src# Go语言发行版1.12没有包含这个优化的编译规则,因此直接使用发行版自带的Go编译器# 获取并查看优化前的ssa.htmlGOSSAFUNC=comp go tool compile main.go# 使用Go benchmark命令测试性能并记录在文件before-ssa-bench.txt中go test -bench BenchmarkFloatCompare -count=5 > before-ssa-bench.txt# 接下来使用优化后的Go编译器获取ssa.html# 找到一个放置Go源码仓的目录,如/usr/local/src/expmkdir /usr/local/src/expcd /usr/local/src/exp# 通过git工具拉取github代码托管平台上golang的代码仓git clone https://github.com/golang/go# 拉取的最新源码已经包含了这个优化,因此可以直接编译获得最新的Go编译器# 进入源码目录cd /usr/local/src/exp/go/src# 编译Go源码,生成Go开发环境bash ./make.bash# 切换回测试代码目录cd /usr/local/src/sample/compile/ssa/opt_float_cmp_0_by_SSA_rule/src# 指定GOROOT目录,GOSSAFUNC关键字选择要展示的函数,本文中是comp,生成优化后的ssa.htmlGOROOT=/usr/local/src/exp/go; GOSSAFUNC=comp go tool compile main.go# 使用Go benchmark命令测试性能并记录在文件after-ssa-bench.txt中GOROOT=/usr/local/src/exp/go; go test -bench BenchmarkFloatCompare -count=5 > after-ssa-bench.txt# benchstat 对比结果benchstat before-ssa-bench.txt after-ssa-bench.txt# 值得一提的是开发新的编译规则时,只需要编写.rules和Ops.go文件,并通过[编译规则代码生成工具]生成规则转换执行代码# 自动生成的文件包括opGen.go 和 rewriteARM64.gocd /usr/local/src/exp/go/src/cmd/compile/internal/ssa/gengo run *.go5. 结论上述优化案例最终使ARM64平台上所有浮点变量与0比较的性能都得到优化,虽然提升较小,但由于其能在用户无需任何改动的情况下使所有符合上述规则的海量代码得到优化,因此是一个非常有价值的优化。 
  • 开源软件优化-应用CPU SIMD加速技术优化软件性能
    本文基于分析Go语言开源社区在ARM64平台对通用字符串比较的优化方案,向读者介绍如何通过SIMD(单指令多数据流)并行化技术提升软件的执行速度。SIMD即单指令多数据流(Single Instruction Multiple Data)指令集,是通过一条指令同时对多个数据进行运算的硬件加速技术,能够有效提高CPU的运算速度,主要适用于计算密集型、数据相关性小的多媒体、数学计算、人工智能等领域。Go语言是一种快速、静态类型的开源语言,可以用来轻松构建简单、可靠、高效的软件。目前已经包括丰富的基础库如数学库、加解密库、图像库、编解码等等,其中对于性能要求较高且编译器目前还不能优化的场景,Go语言通过在底层使用汇编技术进行了优化,其中最重要的就是SIMD技术。1. 字符串比较的性能问题先看一个常见的问题,在各行各业的服务系统中,用户登录需要验证用户名或ID,订购货物需要对比货物ID,出行需要验证票号等等,这些都离不开字符串比较操作,字符串实际上就是字节数组,在Go语言中可以表示成[]byte的形式,字符串的比较即两个数组中对应byte的比较。因此可以直观的写出如下的比较函数代码:func EqualBytes(a, b []byte) bool {    if len(a) != len(b) {       //长度不等则返回false        return false    }    for i, _ := range a {        if a[i] != b[i] {           //按顺序比较数组a和数组b中的每个byte            return false        }    }    return true}似乎看起来还不错,逻辑没有问题,但是这样的实现就够了吗?是否能满足所有的使用场景呢?本文通过性能测试来给出答案。通过Go benchmark进行测试得出如下数据:用例名用例含义执行测试数量每操作耗时 time/op单位时间处理数据量BenchmarkEqual/0-8在8核下比较0B字符串3306695483.64 ns/opBenchmarkEqual/1-8在8核下比较1B字符串2276328825.27 ns/op189.74 MB/sBenchmarkEqual/6-8在8核下比较6B字符串1322297499.09 ns/op660.35 MB/sBenchmarkEqual/9-8在8核下比较9B字符串10000000010.1 ns/op893.80 MB/sBenchmarkEqual/15-8在8核下比较15B字符串8317380114.4 ns/op1041.32 MB/sBenchmarkEqual/16-8在8核下比较16B字符串7995528315.0 ns/op1069.79 MB/sBenchmarkEqual/20-8在8核下比较20B字符串6735393817.8 ns/op1124.26 MB/sBenchmarkEqual/32-8在8核下比较32B字符串4570656626.2 ns/op1219.49 MB/sBenchmarkEqual/4K-8在8核下比较4KB字符串4219562844 ns/op1440.18 MB/sBenchmarkEqual/4M-8在8核下比较4MB字符串3343496666 ns/op1199.52 MB/sBenchmarkEqual/64M-8在8核下比较64MB字符串1866481026 ns/op1009.44 MB/s[注]ns/op:每次函数执行耗费的纳秒时间;MB/s:每秒处理的兆字节数据;B:字节如表所示,随着处理数据量的增加,耗时上升明显,当数据量达到4M时,耗时接近3.5毫秒(3496666 ns/op),作为一个基础操作来讲性能表现较差。2. Go语言社区字符串比较的SIMD优化方案那么字符串比较的性能问题是否有优化的办法呢?本文就以Go语言社区对字符串比较的优化案例揭开SIMD技术优化的神秘面纱: 1. 优化的补丁在Go社区官网可见。imageimage由于优化前后的代码都是汇编,为便于读者学习代码,首先将上节的Go代码例子与优化前的Equal汇编代码对比,通过如下直观的对比关系图来展示:imageimage如图所示,两者实现逻辑是对应的。此处对于一行Go代码a[i]!=b[i],需要四条汇编指令:1.          两条取数指令,分别将切片数组a和b中的byte值取到寄存器中;2.          通过CMP(比较指令)对比两个寄存器中的值,根据比较结果更新状态寄存器;3.          BEQ(跳转指令)根据状态寄存器值进行跳转,此处是等于则跳转到loop标签处,即如果相等则继续下一轮比较。现在可以正式开始分析Equal的SIMD优化版了,这里的核心思路是通过单指令同时处理多个byte数据的方式,大幅减少数据加载和比较操作的指令条数,发挥SIMD运算部件的算力,提升性能。下图是使用SIMD技术优化汇编代码前后的对比图,从图中可以看到优化前代码非常简单,循环取1 byte进行比较,使用SIMD指令优化后,代码被复杂化,这里可以先避免陷入细节,先理解实现原理,具体代码细节可以在章节[优化后代码详解]再进一步学习。此处代码变复杂的主要原因是进行了分情况的分块处理,首先循环处理64 bytes大小的分块,当数组末尾不足64 bytes时,再将余下的按16 bytes分块处理,直到余下长度为1时的情况,下图直观的演示了优化前后的对比关系和优化后分块处理的规则:imageimage3. 优化前代码详解优化前的代码循环从两个数组中取1 byte进行比较,每byte数据要耗费2个加载操作、1个比较操作、1个数组末尾判断操作,如下所示://func Equal(a, b []byte) boolTEXT bytes·Equal(SB),NOSPLIT,$0-49//---------数据加载------------    // 将栈上数据取到寄存器中    // 对数组长度进行比较,如果不相等直接返回0    MOVD a_len+8(FP), R1        // 取数组a的长度    MOVD b_len+32(FP), R3      // 取数组b的长度    CMP R1, R3                         // 数组长度比较    BNE notequal                      // 数组长度不同,跳到notequal    MOVD a+0(FP), R0              // 将数组a的地址加载到通用寄存器R0中    MOVD b+24(FP), R2            // 将数组b的地址加载到通用寄存器R2中    ADD R0, R1                         // R1保存数组a末尾的地址//-----------------------------//--------数组循环比较操作-------loop:    CMP R0, R1                         // 判断是否到了数组a末尾    BEQ equal                           // 如果已经到了末尾,说明之前都是相等的,跳转到标签equal    MOVBU.P 1(R0), R4             // 从数组a中取一个byte加载到通用寄存器R4中    MOVBU.P 1(R2), R5             // 从数组b中取一个byte加载到通用寄存器R5中    CMP R4, R5                         // 比较寄存器R4、R5中的值    BEQ loop                             // 相等则继续下一轮循环操作//-----------------------------//-------------不相等-----------notequal:    MOVB ZR, ret+48(FP)          // 数组不相等,返回0    RET//-----------------------------//-------------相等-------------equal:    MOVD $1, R0                       // 数组相等,返回1    MOVB R0, ret+48(FP)    RET//-----------------------------4. 优化后代码详解优化后的代码因为做了循环展开,所有看起来比较复杂,但逻辑是很清晰的,即采用分块的思路,将数组划分为64/16/8/4/2/1bytes大小的块,使用多个向量寄存器,利用一条SIMD指令最多同时处理16个bytes的优势,同时也减少了边界检查的次数。汇编代码解读如下(代码中添加了关键指令注释):// 函数的参数,此处是通过寄存器传递参数的// 调用memeqbody的父函数已经将参数放入了如下寄存器中// R0: 寄存器R0保存数组a的地址// R1: 寄存器R1数组a的末尾地址// R2: 寄存器R2保存数组b的地址// R8: 寄存器R8存放比较的结果TEXT runtime·memeqbody<>(SB),NOSPLIT,$0//---------------数组长度判断-----------------// 根据数组长度判断按照何种分块开始处理    CMP    $1, R1    BEQ    one    CMP    $16, R1    BLO    tail    BIC    $0x3f, R1, R3    CBZ    R3, chunk16    ADD    R3, R0, R6//------------处理长度为64 bytes的块-----------// 按64 bytes为块循环处理chunk64_loop:// 加载RO,R2指向的数据块到SIMD向量寄存器中,并将RO,R2指针偏移64位    VLD1.P (R0), [V0.D2, V1.D2, V2.D2, V3.D2]    VLD1.P (R2), [V4.D2, V5.D2, V6.D2, V7.D2]// 使用SIMD比较指令,一条指令比较128位,即16个bytes,结果存入V8-v11寄存器    VCMEQ  V0.D2, V4.D2, V8.D2    VCMEQ  V1.D2, V5.D2, V9.D2    VCMEQ  V2.D2, V6.D2, V10.D2    VCMEQ  V3.D2, V7.D2, V11.D2// 通过SIMD与运算指令,合并比较结果,最终保存在寄存器V8中    VAND   V8.B16, V9.B16, V8.B16    VAND   V8.B16, V10.B16, V8.B16    VAND   V8.B16, V11.B16, V8.B16// 下面指令判断是否末尾还有64bytes大小的块可继续64bytes的循环处理// 判断是否相等,不相等则直接跳到not_equal返回    CMP    R0, R6                             // 比较指令,比较RO和R6的值,修改寄存器标志位,对应下面的BNE指令    VMOV   V8.D[0], R4    VMOV   V8.D[1], R5                   // 转移V8寄存器保存的结果数据到R4,R5寄存器    CBZ    R4, not_equal    CBZ    R5, not_equal                   // 跳转指令,若R4,R5寄存器的bit位出现0,表示不相等,跳转not_equal    BNE    chunk64_loop                  // 标志位不等于0,对应上面RO!=R6则跳转chunk64_loop    AND    $0x3f, R1, R1                   // 仅保存R1末尾的后6位,这里保存的是末尾不足64bytes块的大小    CBZ    R1, equal                         // R1为0,跳转equal,否则向下顺序执行..............................................................................................//-----------循环处理长度为16 bytes的块------------chunk16_loop:    VLD1.P (R0), [V0.D2]    VLD1.P (R2), [V1.D2]    VCMEQ    V0.D2, V1.D2, V2.D2    CMP R0, R6    VMOV V2.D[0], R4    VMOV V2.D[1], R5    CBZ R4, not_equal    CBZ R5, not_equal    BNE chunk16_loop    AND $0xf, R1, R1    CBZ R1, equal//-----处理数组末尾长度小于16、8、4、2 bytes的块-----tail:    TBZ $3, R1, lt_8    MOVD.P 8(R0), R4    MOVD.P 8(R2), R5    CMP R4, R5    BNE not_equallt_8:    TBZ $2, R1, lt_4    MOVWU.P 4(R0), R4    MOVWU.P 4(R2), R5    CMP R4, R5    BNE not_equallt_4:    TBZ $1, R1, lt_2    MOVHU.P 2(R0), R4    MOVHU.P 2(R2), R5    CMP R4, R5    BNE not_equallt_2:    TBZ     $0, R1, equalone:    MOVBU (R0), R4    MOVBU (R2), R5    CMP R4, R5    BNE not_equal//-----------------判断相等返回1----------------equal:    MOVD $1, R0    MOVB R0, (R8)    RET//----------------判断不相等返回0----------------not_equal:    MOVB ZR, (R8)    RET上述优化代码中,使用VLD1(数据加载指令)一次加载64bytes数据到SIMD寄存器,再使用VCMEQ(相等比较指令)比较SIMD寄存器保存的数据内容得到结果,相比传统用的单字节比较方式,提高了64byte数据块的比较性能。大于16byte小于64byte块数据,使用一个SIMD寄存器一次处理16byte块的数据,小于16byte数据块使用通用寄存器保存数据,一次比较8byte的数据块。5. 动手实验感兴趣的读者可以根据下面的命令自己执行一遍,感受SIMD技术的强大。•             环境准备1.          硬件配置:鲲鹏(ARM64)云Linux服务器-通用计算增强型KC1 kc1.2xlarge.2(8核|16GB)2.          Go语言发行版 >= 1.12.1,此处开发环境准备请参考文章:Go在ARM64开发环境配置3.          Go语言github源码仓库下载,此处通过Git安装和使用进行版本控制。•             操作步骤 如下操作包含在鲲鹏服务器上进行编译测试的全过程,本文已经找到了优化前后的两个提交记录,优化前的commit id:0c68b79和优化后的commit id:78ddf27,可以按如下步骤进行操作:# 找到一个放置Go源码仓的目录,如/usr/local/src/expmkdir /usr/local/src/expcd /usr/local/src/exp# 通过git工具拉取github代码托管平台上Go的代码仓git clone https://github.com/golang/go# 进入源码目录cd /usr/local/src/exp/go/src# 根据优化前的提交记录0c68b79创建一个新的分支before-simd,这个分支包含优化前的版本git checkout -b before-simd 0c68b79# 切换到分支before-simd,此时目录下的代码文件已经变成了优化前的版本git checkout before-simd# 编译Go源码,生成Go开发环境bash make.bash# 把当前目录设置为GOROOT目录export GOROOT=/usr/local/src/exp/go# 使用Go benchmark命令测试性能并记录在文件before-simd-bench.txt中go test bytes -v -bench ^BenchmarkEqual$ -count=5 -run  ^$ > before-simd-bench.txt# 根据优化后的提交记录78ddf27创建一个新的分支after-simd,这个分支包含优化后的版本git checkout -b after-simd 78ddf27# 切换到分支after-simd,此时目录下的代码文件已经变成了优化后的版本git checkout after-simd# 再次编译Go源码,生成Go开发环境bash make.bash# 使用Go benchmark命令测试性能并记录在文件after-simd-bench.txt中go test bytes -v -bench ^BenchmarkEqual$ -count=5 -run  ^$ > after-simd-bench.txt# benchstat 对比结果benchstat before-simd-bench.txt after-simd-bench.txt# 最后,可以根据提交记录78ddf27查看代码变更git show 78ddf276. 优化结果如章节[动手实验]所述使用Go benchmark记录优化前后的数据。获得结果后通过使用Go benchstat进行优化前后的性能对比。结果如下表格:用例名用例含义优化前每操作耗时 time/op优化后每操作耗时 time/op耗时对比优化前单位时间处理数据量优化后单位时间处理数据量处理数据量对比Equal/1-8在8核下比较1B字符串5.43ns ± 0%5.41ns ± 0%-0.26% (p=0.048 n=5+5)184MB/s ± 0%185MB/s ± 0%~ (p=0.056 n=5+5)Equal/6-8在8核下比较6B字符串10.0ns ± 0%6.2ns ± 0%-38.30% (p=0.008 n=5+5)598MB/s ± 0%972MB/s ± 0%+62.56% (p=0.008 n=5+5)Equal/9-8在8核下比较9B字符串12.3ns ± 0%6.6ns ± 0%-46.67% (p=0.029 n=4+4)729MB/s ± 0%1373MB/s ± 0%+88.35% (p=0.008 n=5+5)Equal/15-8在8核下比较15B字符串17.0ns ± 0%7.4ns ± 2%-56.20% (p=0.008 n=5+5)881MB/s ± 0%2015MB/s ± 2%+128.78% (p=0.008 n=5+5)Equal/16-8在8核下比较16B字符串17.8ns ± 0%5.8ns ± 0%-67.36% (p=0.008 n=5+5)901MB/s ± 0%2755MB/s ± 0%+205.94% (p=0.016 n=4+5)Equal/20-8在8核下比较20B字符串20.9ns ± 0%8.0ns ± 0%-61.75% (p=0.000 n=5+4)956MB/s ± 0%2502MB/s ± 0%+161.84% (p=0.016 n=5+4)Equal/32-8在8核下比较32B字符串30.4ns ± 0%7.6ns ± 0%-74.85% (p=0.008 n=5+5)1.05GB/s ± 0%4.19GB/s ± 0%+297.49% (p=0.008 n=5+5)Equal/4K-8在8核下比较4KB字符串3.18µs ± 0%0.21µs ± 0%-93.49% (p=0.000 n=5+4)1.29GB/s ± 0%19.70GB/s ± 0%+1428.63% (p=0.008 n=5+5)Equal/4M-8在8核下比较4MB字符串3.69ms ± 1%0.65ms ± 4%-82.36% (p=0.008 n=5+5)1.14GB/s ± 1%6.45GB/s ± 4%+466.96% (p=0.008 n=5+5)Equal/64M-8在8核下比较64MB字符串63.3ms ± 0%17.4ms ± 5%-72.54% (p=0.008 n=5+5)1.06GB/s ± 0%3.86GB/s ± 5%+264.45% (p=0.008 n=5+5)[注]ns/op:每次函数执行耗费的纳秒时间;     ms/op:每次函数执行耗费的毫秒时间;     MB/s:每秒处理的兆字节数据;     GB/s:每秒处理的G字节数据;上表中可以清晰的看到使用SIMD优化后,所有的用例性能都有所提升,其中数据大小为4K时性能提升率最高,耗时减少了93.49%;每秒数据处理量提升14.29倍
  • [技术干货] 【高校优秀案例】研究型汇编实验指导书
    研究型汇编实验指导书授课老师:赵宏智北京交通大学计算机与信息技术学院副教授。主要研究领域包括:高效能边缘计算、轻量级人工智能算法、高通量AI芯片体系结构、可重构计算、系统性能优化方法等。课程设计特点:基于鲲鹏处理器(Arm64架构处理器)的汇编语言实验,包括4个实验:实验1:基础实验:Aarch64架构下的HelloWorld汇编代码、C代码调用汇编程序、C代码内嵌汇编代码。实验2:利用鲲鹏处理器的流水线来优化汇编代码性能:充分利用鲲鹏920处理器的“访存单元支持每拍2条读或写访存指令”来提升汇编代码的性能。实验3:Aarch64的增强型SIMD运算:Aarch64中的NEON单元专用于支持Adavanced(增强型) SIMD(Single Instruction Multiple Data)的并行运算,完全符合IEEE754双精度标准,适合多媒体数据处理等任务。实验4:ArmV8中的密码运算:ArmV8-A提供Cryptography密码指令集,采用哈希指令集来实现SHA256算法。实验5:将linux X86下的汇编代码翻译为linux Arm64下的汇编代码。GDB的基本调试命令用法、GNU ARM汇编语法、Arm64汇编研究型实验指导书,详情见附件。
  • [技术干货] 性能之巅:定位和优化程序CPU、内存、IO瓶颈(上)
     摘要:性能优化指在不影响系统运行正确性的前提下,使之运行得更快,完成特定功能所需的时间更短,或拥有更强大的服务能力。#一、思维导图#二、什么是性能优化?性能优化指在不影响系统运行正确性的前提下,使之运行得更快,完成特定功能所需的时间更短,或拥有更强大的服务能力。##关注不同程序有不同的性能关注点,比如科学计算关注运算速度,游戏引擎注重渲染效率,而服务程序追求吞吐能力。服务器一般都是可水平扩展的分布式系统,系统处理能力取决于单机负载能力和水平扩展能力,所以,提升单机性能和提升水平扩展能力是两个主要方向,理论上系统水平方向可以无限扩展,但水平扩展后往往导致通信成本飙升(甚至瓶颈),同时面临单机处理能力下降的问题。##指标衡量单机性能有很多指标,比如:QPS(Query Per Second)、TPS、OPS、IOPS、最大连接数、并发数等评估吞吐的指标。CPU为了提高吞吐,会把指令执行分为多个阶段,会搞指令Pipeline,同样,软件系统为了提升处理能力,往往会引入批处理(攒包),跟CPU流水线会引起指令执行Latency增加一样,伴随着系统负载增加也会导致延迟(Latency)增加,可见,系统吞吐和延迟是两个冲突的目标。显然,过高的延迟是不能接受的,所以,服务器性能优化的目标往往变成:追求可容忍延迟(Latency)下的最大吞吐(Throughput)。延迟(也叫响应时间:RT)不是固定的,通常在一个范围内波动,我们可以用平均时延去评估系统性能,但有时候,平均时延是不够的,这很容易理解,比如80%的请求都在10毫秒以内得到响应,但20%的请求时延超过2秒,而这20%的高延迟可能会引发投诉,同样不可接受。一个改进措施是使用TP90、TP99之类的指标,它不是取平均,而是需确保排序后90%、99%请求满足时延的要求。通常,执行效率(CPU)是我们的重点关注,但有时候,我们也需要关注内存占用、网络带宽、磁盘IO等,影响性能的因素很多,它是一个复杂而有趣的问题。#三、基础知识能编写运行正确的程序不一定能做性能优化,性能优化有更高的要求,这样讲并不是想要吓阻想做性能优化的工程师,而是实事求是讲,性能优化既需要扎实的系统知识,又需要丰富的实践经验,只有这样,你才能具备case by case分析问题解决问题的能力。所以,相比直接给出结论,我更愿意多花些篇幅讲一些基础知识,我坚持认为底层基础是理解并掌握性能优化技能的前提,值得花费一些时间研究并掌握这些根技术。##CPU架构你需要了解CPU架构,理解运算单元、记忆单元、控制单元是如何既各司其职又相互配合完成工作的。你需要了解CPU如何读取数据,CPU如何执行任务。你需要了解数据总线,地址总线和控制总线的区别和作用。你需要了解指令周期:取指、译指、执行、写回。你需要了解CPU Pipeline,超标量流水线,乱序执行。你需要了解多CPU、多核心、逻辑核、超线程、多线程、协程这些概念。##存储金字塔CPU的速度和访存速度相差200倍,高速缓存是跨越这个鸿沟的桥梁,你需要理解存储金字塔,而这个层次结构思维基于着一个称为局部性原理(principle of locality)的思想,它对软硬件系统的设计和性能有着极大的影响。局部性又分为时间局部性和空间局部性。### 缓存现代计算机系统一般有L1-L2-L3三级缓存。比如在我的系统,我通过进入 /sys/devices/system/cpu/cpu0/cache/index0 1 2 3目录下查看。size对应大小、type对应类型、coherency_line_size对应cache line大小。每个CPU核心有独立的L1、L2高速缓存,所以L1和L2是on-chip缓存;L3是多个CPU核心共享的,它是off-chip缓存。L1缓存又分为i-cache(指令缓存)和d-cache(数据缓存),L1缓存通常只有32K/64KB,速度高达4 cycles。L2缓存能到256KB,速度在8 cycles左右。L3则高达30MB,速度32 cycles左右。而内存高达数G,访存时延则在200 cycles左右。所以CPU->寄存器->L1->L2->L3->内存->磁盘构成存储层级结构:越靠近CPU,存储容量越小、速度越快、单位成本越高,越远离CPU,存储容量越大、速度越慢、单位成本越低。### 虚拟存储器(VM)进程和虚拟地址空间是操作系统的2个核心抽象。系统中的所有进程共享CPU和主存资源,虚拟存储是对主存的抽象,它为每个进程提供一个大的、一致的、私有的地址空间,我们gdb调试的时候,打印出来的变量地址是虚拟地址。操作系统+CPU硬件(MMU)紧密合作完成虚拟地址到物理地址的翻译(映射),这个过程总是沉默的自动的进行,不需要应用程序员的任何干预。每个进程有一个单独的页表(Page Table),页表是一个页表条目(PTE)的数组,该表的内容由操作系统管理,虚拟地址空间中的每个页(4K或者8K)通过查找页表找到物理地址,页表往往是层级式的,多级页表减少了页表对存储的需求,命失(Page Fault)将导致页面调度(Swapping或者Paging),这个惩罚很重,所以,我们要改善程序的行为,让它有更好的局部性,如果一段时间内访存的地址过于发散,将导致颠簸(Thrashing),从而严重影响程序性能。为了加速地址翻译,MMU中增加了一个关于PTE的小的缓存,叫翻译后备缓冲器(TLB),地址翻译单元做地址翻译的时候,会先查询TLB,只有TLB命失才会查询高速缓存(L1-2-3)。## 汇编基础虽然写汇编的场景越来越少,但读懂汇编依然很有必要,理解高级语言的程序是怎么转化为汇编语言有助于我们编写高质量高性能的代码。对于汇编,至少需要了解几种寻址模式,了解数据操作、分支、传送、控制跳转指令。理解C语言的if else、while/do while/for、switch case、函数调用是怎么翻译成汇编代码。理解ebp+esp寄存器在函数调用过程中是如何构建和撤销栈帧的。理解函数参数和返回值是怎么传递的。## 异常和系统调用异常会导致控制流突变,异常控制流发生在计算机系统的各个层次,异常可以分为四类:中断(interrupt):中断是异步发生的,来自处理器外部IO设备信号,中断处理程序分上下部。陷阱(trap):陷阱是有意的异常,是执行一条指令的结果,系统调用是通过陷阱实现的,陷阱在用户程序和内核之间提供一个像过程调用一样的接口:系统调用。故障(fault):故障由错误情况引起,它有可能被故障处理程序修复,故障发生,处理器将控制转移到故障处理程序,缺页(Page Fault)是经典的故障实例。终止(abort):终止是不可恢复的致命错误导致的结果,通常是硬件错误,会终止程序的执行。系统调用:## 内核态和用户态你需要了解操作系统的一些概念,比如内核态和用户态,应用程序在用户态运行我们编写的逻辑,一旦调用系统调用,便会通过一个特定的陷阱陷入内核,通过系统调用号标识功能,不同于普通函数调用,陷入内核态和从内核态返回需要做上下文切换,需要做环境变量的保存和恢复工作,它会带来额外的消耗,我们编写的程序应避免频繁做context swap,提升用户态的CPU占比是性能优化的一个目标。
总条数:50 到第
上滑加载中