http://www.chinaaet.com/article/3000087559
0 引言
CPU是电子产品的核心,代表着信息产业的发展水平。CPU发展至今已经有四十多年的历史了,实际就是Intel公司的发展历史[1]。Intel的CPU和其兼容产品占领了PC的大半江山。我国CPU战略已经发展十余年,部分领域完全具有核心技术,产业化取得积极进展,但是与国际主流厂商Intel等仍存在较大差距。国产CPU由于受多方因素制约,单核性能并不高,在2000年左右所有的微处理器厂商都转向了多核微处理器的开发。为提升CPU性能,国产CPU均为多核设计。目前,部分产品虽然在党政军部门及重要的信息系统有所应用,但是由于产业生态还未建立,所应用的场景还比较简单,可用于开发应用的编程语言也较为单一。国产CPU的发展事关我国信息产业的核心竞争力和可持续发展力。为了充分发挥国产CPU的性能,需要通过并行编程来解决。
Go语言设计目标之一就是多核编程,不使用多线程编程模型,通过基于CSP的communication通道并发编程,使得并发编程更加简便。Go语言原生支持广泛应用的X86、X64指令集,而且支持龙芯的MIPS64及飞腾的ARM64指令集。这使得Go语言程序在跨平台移植上天生具有优势。Go语言的其他一些特性使得其在国产CPU平台应用具有非常广泛的前景。
1 Go语言简介
Go语言是2009年11月Google正式宣布推出的一种编程语言。它的并发机制使得编写能够充分利用多核和网络通信的程序变得非常容易。Go语言是静态类型的语言,它的类型系统没有层级,完全垃圾回收,比典型的面向对象语言更轻量级;Go语言是一种编译型语言,它结合了解释型语言的游刃有余,动态类型语言的开发效率,以及静态类型的安全性。
Go编译器支持包括:x86、x64、ARM、arm64、ppc64、ppc64le、mips、mipsle、mips64、mips64le、s390x多种不同的CPU指令集。可以支持包括FreeBSD、Linux、Solaris和Windows等的多种操作系统。Go语言是跨平台、跨操作系统的语言,部署非常简单。Go 编译生成的是一个静态可执行文件,除了glibc外没有其他外部依赖。Go的并发性好,而且goroutine 和 channel 使得编写高并发的服务端软件变得相当容易,非常适合用来服务器编程。目前Go语言已经成功的项目包括目前比较流行的云框架Docker、NSQ等分布式框架等。
2 国产CPU平台简介及应用现状
目前,我国自主研发的处理器芯片主要包括龙芯(MIPS64指令集)、申威(Alpha指令集)、飞腾(ARM64指令集)及兆芯(X86指令集)4类。商用领域基于龙芯3B1500 CPU、飞腾FA1500A CPU生产的商用服务器占据国产服务器主流位置,龙芯、飞腾服务器分别搭载中标麒麟及银河麒麟操作系统。
在商用领域,全国产化平台刚刚起步,配套的商用软件较少,没有建立起完整的产业生态,同时又受到处理器自身性能的影响,基于上述两款主流处理器平台的服务器,主要还是用于对系统访问并发及响应时间要求不高的Web网站类网络应用服务。国产处理器应用服务的开发还是以Java语言为主,搭配开源JBoss、Ttomcat或国产中间件,数据库采用国产数据库或MySQL等开源数据库。
3 Go语言在国产平台的优势
3.1 国产平台现有的编程语言匮乏
目前,基于国产CPU的国产化平台比较成熟的仅有C、C++编译器。厂商对Java虚拟机在国产平台上进行编译、适配及优化。由于Java应用运行过程中依赖的大量Java第三方框架都是源于X86构架体系,并未针对国产CPU进行优化,一些应用在大压力或大并发等某些场景下会出现假死、宕机等情况,严重影响应用的正常运行。虽然国产平台也支持其他编程语言如Python、PHP等,但这类脚本语言的解释器也是交叉编译获得的,并非自身支持,在进行跨平台交叉编译过程中会遇到各种问题,很多问题需要软件开发人员修改软件源代码,未经过大量适配验证,很难保证其开发的应用的稳定性。
3.2 Go语言的跨平台及并发优势
Go语言原生支持龙芯CPU的MIPS64le指令集和飞腾CPU的ARM64指令集,天生具有跨平台优势。Go开发应用在任意平台开发完成后,直接编译,编译后的二进制文件在同类平台可直接拷贝运行,无需再次重新编译。除了glibc外,无需其他外部依赖。可以直接在该开发平台的任意计算机上运行,无需像Java运行那样需要虚拟机,需要配置复杂的环境变量;在作为网络服务时更不需要像Tomcat、Apache等的Web中间件。
Go语言的异构平台移植也非常简单,仅需要应用程序的源码,在异构平台上直接编译即可,且编译后的二进制文件在同类平台可直接拷贝运行。Go语言本身就具有天生的跨平台优势,大大降低了分布式异构计算平台的开发难度,非常适合在目前多构架的国产化平台上作为开发语言。
Go语言在并发方面,goroutine和channel机制提供了轻量级并发机制;在性能方面,与Java的性能不分上下,而内存资源消耗方面,相对Java和其他动态语言,具备明显的优势。在网络和HTTP应用方面,Go语言有良好的标准库和生态系统支持,而在标准库方面,已提供了处理多种网络所需的轻量级的代码库,对网络的核心协议HTTP的高并发支持,完全可以撼动Java。国产处理器由于指令集及工艺等多方面原因,导致单核计算性能不高,为提高整体计算能力均采用多核技术。如:龙芯3B1500为8核,商用服务器为双路16核(2颗3B1500),飞腾FT1500A服务器为16核。因此,充分利用多核计算能力或搭建基于国产处理器的云计算平台是目前国产化平台的提高整体性能发展的方向。
4 Go语言在国产环境下的移植
以飞腾平台为例,飞腾CPU采用ARM64构架。首先在X86平台上交叉编译出面向ARM64平台的Go语言自举编译工具,利用$GOOS=linux GOARCH=arm64./bootstrap.bash 编译命令编译出可在ARM64平台运行的Go语言自举编译工具,然后利用该自举编译工具在ARM64平台编译安装Go源码。安装完成后,Go语言会自动进行自身测试。测试完成后提示ALL TESTS PASSED,添加GOROOT至系统环境变量。Go语言的移植完成。龙芯平台移植过程与飞腾平台移植过程不同之处是在编译自举工具时GOARCH参数设置为MIPS64le。
5 Go语言的多核工作原理简介
Go语言可以快速高效地调用多核进行计算,其优势源于Go语言的Go runtime的调度器[2]。
用户空间线程和内核空间线程之间的映射关系有N:1、1:1和M:N 3种通常的线程模型。其中N:1模型是几个用户空间线程在一个OS线程上运行。该模型上下文切换非常快速,但不能利用多核系统的优点。1:1模型一个执行线程匹配一个OS线程。 它利用机器上的所有内核,但上下文切换速度较慢,因为它必须通过操作系统进行。
Go通过使用M:N调度程序来取得上述两种方式的最佳效果。它将任意数量的goroutine调度到任意数量的OS线程上。开发者可以获得快速上下文切换,并利用系统中的所有内核。这种方法的主要缺点是增加了调度器的复杂性。
为了完成调度任务,Go Scheduler使用3个主要实体,如图1所示。
M三角形表示OS线程。这是由操作系统管理的执行线程,其工作原理与标准POSIX线程相似。
G圆圈代表一个goroutine。它包括堆栈、指令指针和其他重要的调度goroutines的信息,像任何可能被阻止的channel等。
P是从N:1调度程序进入M:N调度程序的重要部分,表示调度的上下文。可以将其视为在单个线程上运行Go代码的调度程序,一个局部的调度器。
如图2所示,有2个线程(M),每个线程都有一个上下文(P),每个线程都运行一个goroutine(G)。为了运行goroutines,线程必须持有一个上下文。
P的数量可以通过GOMAXPROCS()来设置,代表了真正的并发度,即有多少个goroutine可以同时运行。可以使用它来调整Go进程到计算机的调用,例如在4核心PC上运行4个线程的Go代码。
白色的goroutines没有运行,处于就绪状态,正在等待被调度。P维护着这个队列。在Go语言里,每当goroutine执行go语句时,goroutines都会添加到队列的末尾。一旦上下文运行了一个goroutine直到调度点,它会从其运行队列中弹出一个goroutine,设置堆栈和指令指针,并开始运行goroutine。
图3中当一个OS线程被阻塞时,P可以转而投奔另一个系统线程。从图中看到,当一个线程M0陷入阻塞时,P转而在OS线程M1上运行。调度器保证有足够的线程来运行所有P。
M1可能是被创建,或者从线程缓存中取出。当syscall返回时,它必须尝试获取一个上下文来运行返回的goroutine,一般情况下,它会从其他的系统线程取得一个上下文,如果没有获取到,它就把goroutine放在一个全局队列中,放入线程缓存里。上下文会周期性地检查全局队列,否则全局队列上的goroutine永远无法执行。
如果上下文的运行队列的工作量不平衡,如图4所示,则可能会发生这种情况。P所分配的任务G很快就执行完了(分配不均),这就导致了一个上下文P空闲而系统忙碌。当一个上下文用完时,它将尝试从另一个上下文中窃取大约一半的运行队列。这确保在每个上下文上总是有工作要做,这反过来确保所有线程都以最大容量工作,每个系统线程都能充分地使用。
6 实测Go语言在国产平台的多核调用
6.1 并行积分计算原理
并行计算积分。计算积分是一个用来展示并发编程和它本身加速度(表示的是多处理器执行时间和单处理器执行时间的比值)的常见例子,例如一个函数f(x)在[a,b]上的积分:
通过循环触发goroutine(协程)来实现np个子算组的并行运算,通过Go代码来计算Pi的积分:
6.2 Go语言并行计算核心代码实现
核心代码:
上述计算中,一个计算组是通过 block(start,end int,c chan float64)这个函数实现的,这个函数计算从start到end之间的矩形面积、通道(channl)c则是用来在结束的时候进行同步,并且把计算组的结果送到主线程。主线程通过调用rutime.GOMAXPROCS(np)建立运行时所使用的CPU核数,np为使用CPU的个数,然后通过make构造一个有np大小缓存的通道,进行阻塞,确保计算并行进行。最后把执行np次的结果进行累加以获得pi的结果。
6.3 Go语言并行计算在国产CPU多核调用测试
6.3.1 测试环境
飞腾平台:FT1500A16核×1;内存16 GB;Go编译器1.8.1;操作系统:银河麒麟4.0。
龙芯平台:3B1500A8核×2;内存16 GB;Go编译器1.8.1;操作系统:中标麒麟6.0。
6.3.2 测试方法
程序内计时,每次测试3次,取最短时间。结果如图5、图6所示。
上面两组数据图展示了,在龙芯3B1500处理器和飞腾FT1500A上,并行Go计算Pi的效率,对于小的问题规模(n=105,106),使用多核不能增加的执行时间,这是因为过多的进程调度和初始化协程导致的,大部分时间没有执行并行计算。当问题规模n变大的时候(n=108,109,1010),使用多处理器能够显著地缩短计算所需的执行时间。
6.3.3 结果对比
结果对比如图7所示。
从图7可以看出,与其他公开的测试的数据基本一致,飞腾FT1500A无论单核和多核性能均比龙芯3B1500CPU计算能力快出近一倍。
6.4 Go语言在调用多核进行并行计算的性能线性特性
为了能够体现并发执行的加速度,以飞腾F1500A为例子以通过式(6)计算速度:
在这里Tnp和TMaxnp是处理器当前使用核数计算所用时间和处理器最大核数计算所用时间。结果如图8所示。
在图8中,当问题规模非常大的时候,增长几乎就是线性的了,特别是问题规模达到(n=109)的时候,几乎和拟合趋势线重合。
7 结束语
本文对Go语言在多线程领域编程、跨异构平台及编程难易程度的优势进行了阐述;对目前我国国产CPU在商业化应用领域现状进行了分析。使用Go语言在主流两款国产平台对多核调用进行测试。同时,对Go语言的多核工作原理进行了简要分析。综上所述,Go语言非常适合作为除Java外的另一种国产CPU平台应用开发语言,有较为广泛的应用前景。本文对未来Go语言在国产CPU平台上的开发及应用提供了一定的参考价值。
参考文献
[1] 芮雪,王亮亮,杨琴.国产处理器研究与发展现状综述[J].现代计算机(普及版),.2014(3):15-19.
[2] MORSING D.The Go scheduler[Z].2013.