随着互联网的高速发展,内容量的提高以及对内容智能的需求、云产业的快速突起,做为互联网的计算基石服务器的形态以及使用成为了煊赫一时的话题,全球各家大型互联网公司都持续的在服务器平台上有很是大的动做,譬如facebook的OCP等,而整个服务器的生态链也获得了促进和发展。随着服务器硬件性能的提高和网络硬件的开放,传统PC机的处理性能甚者能够和网络设备相媲美。另外一方面SDN技术的发展,基础架构网络逐渐偏向基于通用计算平台或模块化计算平台的架构融合,来支持多样化的网络功能,传统的PC机器在分布式计算平台上的优点更为明显。在这些针对海量数据处理或海量用户的服务场景,高性能编程显得尤其重要。html
本文讲述了从C10K到C10M过程当中编程模式的改变;接着介绍了Intel DPDK开发套件如何突破操做系统限制,给开发高性能网络服务的程序员带来的福音;以后总结高性能程序设计的一些其它的优化方法;最后分享咱们利用DPDK技术来实现的高性能网关设备和服务程序案例。前端
前10年中,网络程序性能优化的目标主要是为了解决C10K问题,其研究主要集中在如何管理数万个客户端并发链接,各类I/O框架下如何进行性能优化,以及操做系统参数的一些优化。java
当前,解决C10K问题的服务器已经很是多。Nginx和Lighttpd两款很是优秀的基于事件驱动的web服务框架,Tornado和Django则是基于python开发的非阻塞的web框架;Yaws和Cowboy则是用Erlang开发轻量级web框架。这些软件使得C10K已经再也不是问题了。node
今天,C10M成为新的研究主题了。也许你会感到奇怪,千万级并发不是网络设备的性能吗?那是设备厂商该作的事情吧,答案在之前是,但现在不是。在互联网设备厂商相对封闭软件体系架构中,咱们不多关注设备内部的配置。可是当你去拆开一款交换机以后就会发现,里面极可能就是咱们PC机使用的x86芯片,即便不是x86,那也是标准的RISC处理器。也就是说如今的互联网硬件设备实际上不多是硬件组成——大部分都是软件来实现的。因此千万并发应该是咱们软件开发人员应该去研究的问题!腾讯自研的Bobcat就是一个基于x86平台的自研设备,性能可达千万级别。python
咱们知道,在解决C10K问题的时候,须要将软件设计为异步模式,使用epoll来高效的处理网络读写事件。但在面对C10M问题中,这样设计是反而比较糟糕,为何这么说呢?linux
一方面在基于多线程的服务器设计框架中,在没有请求到来的时候,线程将会休眠,当数据到来时,将由操做系统唤醒对应的线程,也就是说内核须要负责线程间频繁的上下文切换,咱们是在依靠操做系统调度系统来服务网络包的调度。程序员
另外一方面在以Ngnix为表明的服务器场景,看上去仅使用一个线程监听Epoll事件来避免上下文切换,但咱们仍将繁重的事件通知工做交由操做系统来处理。web
最后要说的是,网卡驱动收包自己也是一个异步的过程,通常是当十几个或者更多的数据包到达以后经过软中断例程一次性将数据包递交到内核,而中断性能自己就不高。相比老的suse内核,tlinux系统也只不过让多队列网卡把中断分散在不一样CPU核心上来提升收发包性能,要是能避免中断就更好了。算法
在千万级并发场景下,咱们的目标是要回到最原始的方式,使用轮询方式来完成一切操做,这样才能提高性能。数据库
Unix诞生之初就是为电话电报控制而设计的,它的控制平面和数据转发平面没有分离,不适合处理大规模网络数据包。若是能让应用程序直接接管网络数据包处理、内存管理以及CPU调度,那么性能能够获得一个质的提高。
为了达到这个目标,第一个要解决的问题就是绕过Linux内核协议栈,由于Linux内核协议栈性能并非很优秀,若是让每个数据包都通过Linux协议栈来处理,那将会很是的慢。像Wind River和6 Wind Gate等公司自研的内核协议栈宣称比Linux UDP/TCP协议栈性能至少提升500%以上,所以能不用Linux协议栈就不用。
不用协议栈的话固然就须要本身写驱动了,应用程序直接使用驱动的接口来收发报文。PF_RING,Netmap和intelDPDK等能够帮助你完成这些工做,并不须要咱们本身去花费太多时间。
Intel官方测试文档给出了一个性能测试数据,在1S Sandbridge-EP 8*2.0GHz cores服务器上进行性能测试,不用内核协议栈在用户态下吞吐量可高达80Mpps(每一个包处理消耗大约200 cpu clocks),相比之下,使用Linux内核协议栈性能连1Mpps都没法达到。
多核的可扩展性对性能提高也是很是重要的,由于服务器中CPU频率提高愈来愈慢,纳米级工艺改进已是很是困难的事情了,但能够作的是让服务器拥有更多的CPU和核心,像国家超级计算中心的天河二号使用了超过3w颗Xeon E5来提升性能。在程序设计过程当中,即便在多核环境下也很快会碰到瓶颈,单纯的增长了处理器个数并不能线性提高程序性能,反而会使总体性能愈来愈低。
一是由于编写代码的质量问题,没有充分利用多核的并行性,
二是服务器软件和硬件自己的一些特性成为新的瓶颈,像总线竞争、存储体公用等诸多影响性能平行扩展的因素。
那么,咱们怎样才能让程序能在多个CPU核心上平行扩展:尽可能让每一个核维护独立数据结构;使用原子操做来避免冲突;使用无锁数据结构避免线程间相互等待;设置CPU亲缘性,将操做系统和应用进程绑定到特定的内核上,避免CPU资源竞争;在NUMA架构下尽可能避免远端内存访问。固然本身来实现无锁结构时要很是当心,避免出现ABA问题和不一样CPU架构下的内存模型的差别。
内存的访问速度永远也赶不上cache和cpu的频率,为了能让性能平行扩展,最好是少访问。
从内存消耗来看,若是每一个用户链接占用2K的内存,10M个用户将消耗20G内存,而操做系统的三级cache连20M都达不到,这么多并发链接的状况下必然致使cache失效,从而频繁的访问内存来获取数据。而一次内存访问大约须要300 cpuclocks,这期间CPU几乎被空闲。所以减小访存次数来避免cachemisses是咱们设计的目标。
指针不要随意指向任意内存地址,由于这样每一次指针的间接访问可能会致使屡次cache misses,最好将须要访问的数据放到一块儿,方便一次性加载到cache中使用。
按照4K页来计算,32G的数据须要占用64M的页表,使得页表甚至没法放到cache中,这样每次数据访问可能须要两次访问到内存,所以建议使用2M甚至1G的大页表来解决这个问题。
C10M的思想就是将控制层留给Linux作,其它数据层所有由应用程序来处理。
减小系统调度、系统调用、系统中断,上下文切换等
摒弃Linux内核协议栈,可使用PF_RING,Netmap,intelDPDK来本身实现驱动;
使用多核编程技术替代多线程,将OS绑在指定核上运行;
使用大页面,减小访问;
采用无锁技术解竞争
Intel® DPDK全称Intel Data Plane Development Kit,是intel提供的数据平面开发工具集,为Intel architecture(IA)处理器架构下用户空间高效的数据包处理提供库函数和驱动的支持,它不一样于Linux系统以通用性设计为目的,而是专一于网络应用中数据包的高性能处理。目前已经验证能够运行在大多数Linux操做系统上,包括FreeBSD 9.二、Fedora release1八、Ubuntu 12.04 LTS、RedHat Enterprise Linux 6.3和Suse EnterpriseLinux 11 SP2等。DPDK使用了BSDLicense,极大的方便了企业在其基础上来实现本身的协议栈或者应用。
须要强调的是,DPDK应用程序是运行在用户空间上利用自身提供的数据平面库来收发数据包,绕过了Linux内核协议栈对数据包处理过程。Linux内核将DPDK应用程序看做是一个普通的用户态进程,包括它的编译、链接和加载方式和普通程序没有什么两样。DPDK程序启动后只能有一个主线程,而后建立一些子线程并绑定到指定CPU核心上运行。
DPDK核心组件由一系列库函数和驱动组成,为高性能数据包处理提供基础操做。内核态模块主要实现轮询模式的网卡驱动和接口,并提供PCI设备的初始化工做;用户态模块则提供大量给用户直接调用的函数。
DPDK架构图
EAL(Environment Abstraction Layer)即环境抽象层,为应用提供了一个通用接口,隐藏了与底层库与设备打交道的相关细节。EAL实现了DPDK运行的初始化工做,基于大页表的内存分配,多核亲缘性设置,原子和锁操做,并将PCI设备地址映射到用户空间,方便应用程序访问。
Buffer Manager API经过预先从EAL上分配固定大小的多个内存对象,避免了在运行过程当中动态进行内存分配和回收来提升效率,经常用做数据包buffer来使用。
Queue Manager API以高效的方式实现了无锁的FIFO环形队列,适合与一个生产者多个消费者、一个消费者多个生产者模型来避免等待,而且支持批量无锁的操做。
Flow Classification API经过Intel SSE基于多元组实现了高效的hash算法,以便快速的将数据包进行分类处理。该API通常用于路由查找过程当中的最长前缀匹配中,安全产品中根据Flow五元组来标记不一样用户的场景也可使用。
PMD则实现了Intel 1GbE、10GbE和40GbE网卡下基于轮询收发包的工做模式,大大加速网卡收发包性能。
DPDK的核心组件
展现了DPDK核心组件的依赖关系,详细介绍能够参考《Intel Data Plane Development kit:Software Architecture Specification》。
当前Linux操做系统都是经过中断方式通知CPU来收发数据包,咱们假定网卡每收到10个据包触发一次软中断,一个CPU核心每秒最多处理2w次中断,那么当一个核每秒收到20w个包时就占用了100%,此刻它没作其它任何操做。
DPDK针对Intel网卡实现了基于轮询方式的PMD(Poll Mode Drivers)驱动,该驱动由API、用户空间运行的驱动程序构成,该驱动使用无中断方式直接操做网卡的接收和发送队列(除了链路状态通知仍必须采用中断方式之外)。目前PMD驱动支持Intel的大部分1G、10G和40G的网卡。
PMD驱动从网卡上接收到数据包后,会直接经过DMA方式传输到预分配的内存中,同时更新无锁环形队列中的数据包指针,不断轮询的应用程序很快就能感知收到数据包,并在预分配的内存地址上直接处理数据包,这个过程很是简洁。若是要是让Linux来处理收包过程,首先网卡经过中断方式通知协议栈对数据包进行处理,协议栈先会对数据包进行合法性进行必要的校验,而后判断数据包目标是否本机的socket,知足条件则会将数据包拷贝一份向上递交给用户socket来处理,不只处理路径冗长,还须要从内核到应用层的一次拷贝过程。
为实现物理地址到虚拟地址的转换,Linux通常经过查找TLB来进行快速映射,若是在查找TLB没有命中,就会触发一次缺页中断,将访问内存来从新刷新TLB页表。Linux下默认页大小为4K,当用户程序占用4M的内存时,就须要1K的页表项,若是使用2M的页面,那么只须要2条页表项,这样有两个好处:第一是使用hugepage的内存所需的页表项比较少,对于须要大量内存的进程来讲节省了不少开销,像oracle之类的大型数据库优化都使用了大页面配置;第二是TLB冲突几率下降,TLB是cpu中单独的一块高速cache,通常只能容纳100条页表项,采用hugepage能够大大下降TLB miss的开销。
DPDK目前支持了2M和1G两种方式的hugepage。经过修改默认/etc/grub.conf中hugepage配置为“default_hugepagesz=1Ghugepagesz=1G hugepages=32 isolcpus=0-22”,而后经过mount –thugetlbfs nodev /mnt/huge就将hugepage文件系统hugetlbfs挂在/mnt/huge目录下,而后用户进程就可使用mmap映射hugepage目标文件来使用大页面了。测试代表应用使用大页表比使用4K的页表性能提升10%~15%
多线程编程早已不是什么新鲜的事物了,多线程的初衷是提升总体应用程序的性能,可是若是不加注意,就会将多线程的建立和销毁开销,锁竞争,访存冲突,cache失效,上下文切换等诸多消耗性能的因素引入进来。这也是Ngnix使用单线程模型能得到比Apache多线程下性能更高的秘籍。
为了进一步提升性能,就必须仔细斟酌考虑线程在CPU不一样核上的分布状况,这也就是常说的多核编程。多核编程和多线程有很大的不一样:多线程是指每一个CPU上能够运行多个线程,涉及到线程调度、锁机制以及上下文的切换;而多核则是每一个CPU核一个线程,核心之间访问数据无需上锁。为了最大限度减小线程调度的资源消耗,须要将Linux绑定在特定的核上,释放其他核心来专供应用程序使用。
同时还须要考虑CPU特性和系统是否支持NUMA架构,若是支持的话,不一样插槽上CPU的进程要避免访问远端内存,尽可能访问本端内存。
总的来讲,为了获得千万级并发,DPDK使用以下技术来达到目的:使用PMD替代中断模式;将每个进程单独绑定到一个核心上,并让CPU从这些核上隔离开来;批量操做来减小内存和PCI设备的访问;使用预取和对齐方式来提供CPU执行效率;减小多核之间的数据共享并使用无锁队列;使用大页面。
除了UDP服务器程序,DPDK还有不少的场景能应用得上。一些须要处理海量数据包的应用场景均可以用上,包括但不局限于如下场景:NAT设备,负载均衡设备,IPS/IDS检测系统,TOR(Top of Rack)交换机,防火墙等,甚至web cache和web server也能够基于DPDK来极大地提升性能。
除了DPDK提供的一些是思想外,咱们的程序性能还能怎样进一步提升性能呢?
运算指令的执行速度是很是快,大多数在一个CPU cycle内就能完成,甚至经过流水线一个cycle能完成多条指令。但在实际执行过程当中,处理器须要花费大量的时间去存储器来取指令和数据,在获取到数据以前,处理器基本处于空闲状态。那么为了提升性能,缩短服务器响应时间,咱们能够怎样来减小访存操做呢?
少用数组和指针,多用局部变量。由于简单的局部变量会放到寄存器中,而数组和指针都必须经过内存访问才能获取数据;
少用全局变量。全局变量被多个模块或函数使用,不会放到寄存器中。
一次多访问一些数据。就比如咱们出去买东西同样,一次多带一些东西更省时间。咱们可使用多操做数的指令,来提升计算效率,DPDK最新版本配合向量指令集(AVX)可使CPU处理数据包性能提高10%以上。
本身管理内存分配。频繁调用malloc和free函数是致使性能下降的重要缘由,不只仅是函数调用自己很是耗时,并且会致使大量内存碎片。因为空间比较分散,也进一步增大了cache misses的几率。
进程间传递指针而非整个数据块。在高速处理数据包过程当中特别须要注意,前端线程和后端线程尽可能在同一个内存地址来操做数据包,而不该该进行多余拷贝,这也是Linux系统没法处理百万级并发响应的根本缘由,有兴趣的能够搜索“零拷贝”的相关文章。
现在CPU早已不是在每次取数据和指令都先去访问内存,而是优先访问cache,若是cache命中则无需访存,而访问同一个cache中数据的开销很是小。下图展现了L1~L3级cache和内存的访问延时。
三级Cache性能模型
Cache有效性得益于空间局部性(附近的数据也会被用到)和时间局部性(从此一段时间内会被屡次访问)原理,经过合理的使用cache,可以使得应用程序性能获得大幅提高。下面举一个实际的例子来让你们理解cache大小对程序性能的影响。
模拟cache大小对程序性能的影响
这里对上面的测试结果解释一下:在K<1024时,访问数组arr的步长不超过1024*4byte=4KB,而咱们的测试机器L1 cache使用的是8路组关联的4K大小的cache,一次最多cache住4K的数据,所以在K<1024时候,cache从内存读取数据次数是同样的,因此执行时间差异不大;而当K>1024时候,访问数组步长为cache大小的倍数,固然访存次数也成倍较少(能够经过perf工具来跟踪分析),所以执行时间成倍减小。Cache大小能够经过命令lscpu、cat/proc/cpuinfo,或者在目录/sys/devices/system/cpu/cpu0/cache/中进行查看。
熟知cache的大小,了解程序运行的时间和空间上局部性原来,对于咱们合理利用cache,提高性能很是重要。同时要少用静态变量,由于静态变量分配在全局数据段,在一个反复调用的函数内访问该变量会致使cache的频繁换入换出,而若是是使用堆栈上的局部变量,函数每次调用时CPU能够直接在缓存中命中它。最后,循环体要简单,指令cache也仅仅有几K,过长的循环体会致使屡次从内存中读取指令,cache优点荡然无存。
多线程中为了不上锁,可使用一个数组,每一个线程独立使用数组中的一个项,互不冲突。从逻辑上看这样的设计很是完美,但实际中运行速度并无太大改善,缘由就在下面慢慢来解释了。
cache line是cache从主存copy数据的最小单元,cpu从不直接访问主存,而是经过cache间接访问主存,在访问主存以前会遍历一遍cache line来查找主存地址是否在某个cache line中,若是没找到则将内存copy到cache line中,而后从cache line获取数据。
多核CPU中每一个核都拥有本身的L1/L2 cache,当运行多线程程序时,尽管算法上不须要共享变量,但实际执行中两个线程访问同一cache line的数据时就会引发冲突,每一个线程在读取本身的数据时也会把别人的cacheline读进来,这时一个核修改改变量,CPU的cache一致性算法会迫使另外一个核的cache中包含该变量所在的cache line无效,这就产生了false sharing(伪共享)问题。
false sharing示意图
Falsing sharing会致使大量的cache冲突,应该尽可能避免。下面是一个典型的例子,假如在CPU的4个核上分别运行4个线程,传递参数为0~3,将会触发false sharing。
false sharing测试代码
下面总结一下文章《Avoiding and IdentifyingFalse Sharing Among Threads》里面的建议:访问全局变量和动态分配内存是falsesharing问题产生的根源,固然访问在内存中相邻的但彻底不一样的全局变量也可能会致使false sharing,多使用线程本地变量是解决false sharing的根源办法。
固然,在平时编程中避免不了要使用全局变量时,这时能够将多个线程访问的变量放置在不一样的cache line中,这样经过牺牲一些内存空间来换取高性能。
首先谈到的是内存对齐:根据不一样存储硬件的配置来优化程序,性能也可以获得极大的提高。在硬件层次,确保对象位于不一样channel和rank的起始地址,这样能保证对象并并行加载。
Channel是指内存上北桥上面的独立内存接口,一个内存通道通常由64位数据总线和8位控制总线构成,不一样通道能够并行工做,当有两个channel同时工做就是咱们平时所说“双通道”,其效果等价于128位数据总线的带宽。Rank是指DIMM上经过一部分或者全部内存颗粒产生的一个64位的area,同一条内存上的不一样rank由于共享数据总线而不能被同时访问,但内存能够利用交错的片选信号来访问不一样rank中数据。
双通道、4R的DIMM
在以上的情形中,假定数据包是64bytes大小,那么最优的对齐方式是在每一个数据包间填充12bytes。
其次谈到的是字节对齐:众所周知,内存最小的存储单元为字节,在32位CPU中,寄存器也是32位的,为了保证访问更加高效,在32位系统中变量存储的起始地址默认是4的倍数(64位系统则是8的倍数),定义一个32位变量时,只须要一次内存访问便可将变量加载到寄存器中,这些工做都是编译器完成的,不需人工干预,固然咱们可使用__attribute__((aligned(n)))来改变对齐的默认值。
最后谈到的是cache对齐,这也是程序开发中须要关注的。Cache line是CPU从内存加载数据的最小单位,通常L1 cache的cache line大小为64字节。若是CPU访问的变量不在cache中,就须要先从内存调入到cache,调度的最小单位就是cache line。所以,内存访问若是没有按照cache line边界对齐,就会多读写一次内存和cache了。
为了解决单核带来的CPU性能不足,出现了SMP,但传统的SMP系统中,全部处理器共享系统总线,当处理器数目愈来愈多时,系统总线竞争加大,系统总线称为新的瓶颈。NUMA(非统一内存访问)技术解决了SMP系统可扩展性问题,已成为当今高性能服务器的主流体系结构之一。
NUMA系统节点通常是由一组CPU和本地内存组成。NUMA调度器负责将进程在同一节点的CPU间调度,除非负载过高,才迁移到其它节点,但这会致使数据访问延时增大。下图是2颗CPU支持NUMA架构的示意图,每颗CPU物理上有4个核心。
NMUA架构示意图
在Nehalem微架构下,链接CPU的双向QPI总线链接理论最大值能够达到25.6GB/s的数据传送,单向则是12.8GB/s,远方/本地延迟比是约1.5倍。所以在万兆服务器应用开发时,咱们须要仔细斟酌和合理的使用内存,避免CPU访问远端内存产生没必要要的延时,也要留心在高速处理数据包时受到QPI总线带宽的瓶颈。因为业务逻辑目前还没法作到如此简洁,QPI并未成为系统瓶颈,但腾讯Bobcat项目在处理在转发高达10G流量时就碰到了这个问题!
注意,并非公司的全部服务器都支持NUMA的,这个功能须要操做系统、CPU和主板同时支持,能够经过numactl --show来查看numa是否有效,目前L2机型是支持了NUMA架构的,普通的C一、B6等服务器则没有支持NUMA特性。
进程上下文切换(context switch,简称CS)对程序性能的影响每每会被你们忽视,但它其实一直在默默扼杀着程序的性能!上下文切换是指CPU控制权由运行任务转移到另外一个就绪任务所发生的事件:此时须要保存进程状态和寄存器值等,不只浪费了CPU的时钟周期,还会致使cache中进程相关数据失效等。
那么如何来减小进程上下文切换呢?咱们首先须要了解哪些场景会触发CS操做。首先就介绍的就是不可控的场景:进程时间片到期;更高优先级进程抢占CPU。其次是可控场景:休眠当前进程(pthread_cond_wait);唤醒其它进程(pthread_cond_signal);加锁函数、互斥量、信号量、select、sleep等很是多函数都是可控的。
对于可控场景是在应用编程须要考虑的问题,只要程序逻辑设计合理就能较少CS的次数。对于不可控场景,首先想到的是适当减小活跃进程或线程数量,所以保证活跃进程数目不超过CPU个数是一个明智的选择;而后有些场景下,咱们并不知道有多少个活跃线程的时候怎么来保证上下文切换次数最少呢?这是咱们就须要使用线程池模型:让每一个线程工做前都持有带计数器的信号量,在信号量达到最大值以前,每一个线程被唤醒时仅进行一次上下文切换,当信号量达到最大值时,其它线程都不会再竞争资源了。
现代处理器都是经过多级流水来提升指令执行速度,为了保持流水线充满待执行指令,CPU必须提早获取指令。当程序中遇到分支或条件跳转语句时,问题就来了,处理器不肯定下一条指令,这是就会使用分支预测逻辑来判断进入流水的下一条指令。
从P5处理器开始引入了分组预测机制,若是预测的一个分支指令加入流水线,以后却发现它是错误的分支,处理器要回退该错误预测执行的工做,再用正确的指令填充流水线。这样一个错误的预测会严重浪费时钟周期,致使程序性能降低。《计算机体系结构:量化研究方法》指出分支指令产生的性能影响为10%~30%,流水线越长,性能影响越大。Core i7和Xen等较新的处理器当分支预测失效时无需刷新所有流水,当错误指令加载和计算仍会致使一部分开销。
若是咱们已经明确知道一个条件发生的几率是偏大仍是偏小,那么能够在程序中显示使用likely、unlikely预处理指令,来指示编译器在生成汇编代码时候对指令进行优化,加快执行速度。
这两个宏在内核中定义以下:
#definelikely(x) __builtin_expect((x),1) #defineunlikely(x) __builtin_expect((x),0)
下面咱们从指令级并发的角度来考察从cache对程序性能的影响
int[] a = new int[2]; for (int i=0; i<steps; i++) { a[0]++; a[0]++; } // 循环1 for (int i=0; i<steps; i++) { a[0]++; a[1]++; } // 循环2
在咱们的测试机上运行这两个循环,第一个循环须要1.6s,第二个循环只要0.8s,这是为何呢?由于第一个循环体内,操做相互依赖,必须等第一次a[0]++执行完后才能执行后续操做;而第二个循环因为是不一样内存的访问,可用作到并发执行。
这个缘由其实就是和CPU中的流水线有关,像Pentium处理器就有U/V两条流水,而且能够独自独立读写缓存,循环2能够将两条指令安排在不一样流水线上执行,性能获得极大提高。另外两条流水线是非对称的,简单指令(mpv,add,push,inc,cmp,lea等)能够在两条流水上并行执行、位操做和跳转操做并发的前提是在特定流水线上工做、而某些复杂指令却只能独占CPU。
须要补充说明,由于上面举得例子很是简单,而在实际代码编写过程当中,每每使用的变量很是多,逻辑也比较复杂,数据在内存中的分布也会影响cache的命中次数,而且分支预测致使的CPU中指令乱序执行,很是难去分析执行流程。所以在程序设计中,要作到指令级并行,须要有意识的注意指令间的配对,尽可能使用简单指令,还要在顺序上减小上下文的依赖。
为了利用空间局部性,同时也为了覆盖数据从内存传输到CPU的延迟,能够在数据被用到以前就将其调入缓存,这一技术称为预取Prefetch,加载整个cache便是一种预取。CPU在进行计算过程当中能够并行的对数据进行预取操做,所以预取使得数据/指令加载与CPU执行指令能够并行进行。
预取能够经过硬件或软件控制。典型的硬件指令预取会在缓存因失效从内存载入一个块的同时,把该块以后紧邻的一个块也传输过来。第二个块不会直接进入缓存,而是被排入指令流缓冲器(Instruction Stream Buffer)中。以后,当第二个内存访问指令到来时,会并行尝试从缓存和流缓冲器中读取。若是该数据刚好在流缓冲器中,则取消缓存访问指令,并将返回流缓冲器中的数据。同时,发出起一次新的预取。若是数据并不在流缓冲器中,则须要将缓冲器清空。
软件控制则多由编译器进行。指令集会提供预取指令供编译器优化时使用。编译器则负责分析代码,并把预取指令适当地插入其中。这类指令直接把目标预取数据载入缓存。若是咱们在编程中能显示的调用预取指令,就能大大提升效率。若是读取的内容仅仅被访问一次,prefetch也没有意义。
在使用预取指令时,必须考虑调用时机和实施强度。若是过早地进行预取,则有可能在预取数据被用到以前就已经由于冲突置换被清除。若是预取得太多或太频繁,则预取数据有可能将那些更加确实地会被用到的数据取代出缓存,反而会增长开销。
一开始在处理进程开发过程当中增长了大量预取操做,可是性能反而降低了,由于在处理进程中对于每一个数据包分析逻辑比较复杂,数据预取填充的cache很快就被业务逻辑指令和数据替换了无数遍吗,所以预取必定要得当
先从一个简单的例子入手:
函数F1实现 | 函数F2实现 |
int i=0,j=0; int i=0,j=0; static a[1000][500000] = {0}; for (j=0;j<500000;j++){ for (i=0;i<1000;i++){ a[i][j] = 2*a[i][j]; } } |
int i=0,j=0; static a[1000][500000] = {0}; for (i=0;i<1000;i++){ for (j=0;j<500000;j++){ a[i][j] = 2*a[i][j]; } } |
运行左边的程序须要10s,运行右边的程序只需不到4s。产生这个现象的缘由就是右边的例子中由于a[i][0]访问cache失效后,会从内存中读取至少一条cache line数据(64bytes),所以cache中充满了a[i][0]~a[i][15]的结果,这样后面15次循环均可以命中cache了。
所以,数据cache大小很是有限,循环中数组访问的顺序很是重要,对性能的影响不容小觑。
另外,若是两个循环体能够合并到一个循环而不影响程序结果,则应该合并。由于经过合并,原来第二个循环中的指令会在指令cache中被命中。
其次,指令cache和数据cache同样都很是小,循环中编写的指令必定要精简,非循环内部的操做能够放到外面去,不然一旦循环体中指令长度超过cache大小就会致使没必要要的置换。
再次,须要考虑cache的置换策略,例如cache使用的是LRU算法的话,在编写多层嵌套循环时须要考虑被置换出去的数据越少越好。
最后,若是能少用循环的话就更好了!
4.11其它优化建议
尽可能减小函数调用,每一次调用都要进行压栈、保存寄存器和执行指令跳转等都会耗费很多时间,能够将一些小的函数写成内联,或直接用宏或语句代替。
对于提升性能空间换时间也是一个很是重要的设计理念,Bloom Filter就是一个典型的例子,还有一些位图、hash的思想也是不错的选择。
在高性能程序设计时,要减小过保护,除了会影响程序执行的一些关键路径和参数要进行校验外,其它参数不必定非得要检查,毕竟错误状况是少数。
尽可能用整型代替浮点数,少用乘除、求余运算,这些操做会占用更多的CPU周期,能提早计算的表达式要提早计算出结果。
充分利用编译器选项,-O3帮助你进行文件内部最深层次的优化,使用其它编译器如icc能编译出在x86平台下运行更快的程序。
延时计算,最近用不上的变量就不要去初始化,操做系统为了提升性能也使用COW(copy-on-write)策略,在fork子进程的时候不会当即复制进程的全部页表。
固然,当你花费大量的时间也就是为了利用硬件的某个特性,左思右想以后或许获取了一点点性能的提高,其实更重要的是在程序设计时候换一种思路,也许就会柳暗花明,从逻辑上或算法上优化得到的效果可能远远大于针对硬件特性优化的效果,在程序设计之初必定要深刻理解业务逻辑,最大程度上简化流程,针对硬件特性的优化必定要是在程序逻辑优化以后进行才能更有效。
在高性能服务器程序开发中,经过网卡中断收发报文是第一道瓶颈,为了不中断方式的网卡驱动,能够经过卸载Linux自带的网卡驱动,代而使用DPDK提供的PMD模式的驱动,避免了中断方式收发数据包,经过轮询方式直接从网卡队列收发数据包并经过DMA方式递交到应用层内存中,大大提高了收发包性能。
相比较Linux默认的4K页表,在应用程序中使用1G的大页表,能够大大减小缺页中断,提高内存的访问速度。
在CPU亲缘性设置方面,将Linux运行在单独的内核上,实际的数据收发包或者处理逻辑绑定在单独的内核上,避免进程间竞争和上下文切换。
为了方便在不一样数据中心间进行数据传输,通常经过建设专线来知足要求。可是专线建设成本高,时间长,当容量增加过快时扩容缓慢,若是在基础架构侧可以利用公网来进行一部分数据的传输,就能缓解专线的压力了。另外一方面,基于网络的容灾需求,大部分专线利用率都低于50%,同时专线是按带宽收费的,浪费了一半以上的成本,如何提升专线利用率的同时还可以低成本的保证网络容灾需求,成为一个必须解决的问题。大流量的公网传输平台在这种背景下应用而生,但要达到此目的,必定要避免成为数据转发的瓶颈。所以必须保证数据报文转发的高性能。
Bobcat是自研的利用公网来传输业务数据的一种网关设备,它具有报文收发,寻址,报文封装和校验等一些列功能。在实现上,bobcat仅使用一个CPU核心来运行Linux操做系统,依赖于Linux完整的协议栈来实现管理功能。其它CPU核心都用来作IPP(Ingress Packet Processor)、EPP(Egress Packet Processor)以及报文转发功能。
具体工做流程以下:svr将包传递到城域网核心,根据路由来决定是否跨城;包到达了Wan,则根据DSCP值来区分专线和大流量平台;在包到达Bobcat平台后,将进行目标Bobcat的查表,头部封装,同时打散成多个传输通道给于传递;bobcat收到传递过来的报文则进行头部校验,须要保证不给篡改和重复的序列号;并向接收端的Wan核心转发;wan core这根据目标地址传递man core。
因为引入多核,Bobcat须要处理报文发送的时序问题,多核的并行处理会致使EPP后的报文乱序,所以要对报文进行重排序,确保从物理端口发出去的顺序和收到的顺序一致,所以这里EPP仅有单核来处理,避免重排序的复杂性,固然系统的总体性能也取决于EPP的转发能力。在实际测试中,Bobcat在转发64bytes小包时几乎能够达到10Gbps的线速。
基于UDP的无状态特性,能够很方便的使用DPDK来提供底层的收发数据包框架,本身实现协议的解析和处理过程,而不用借助于繁重的Linux内核协议栈。
亲缘性设置方面,在一台双CPU的12颗物理核的机器上,由于开启了超线程,逻辑核实际是24个,但因为两颗CPU之间的通讯须要使用QPI,会增大报文处理时延。所以,只利用其一颗CPU的12个超核来进行数据处理。通过不断优化,双端口的转发性能也能够达到线速。这种系统架构中,处理进程共运行11个并分布在同一颗CPU的逻辑核上,其中两个disaptch进程用来收发网卡数据包,并均匀的将数据包放到和处理进程公用的9个无锁队列中。
一种UDP服务程序的亲缘性设置方案
另外本身来处理数据包,从以太网数据帧开始向上层协议分析,并进行必要的校验,只过滤出须要处理的UDP报文,不去使用复杂且低效的Linux协议栈来处理报文。
数据包的读取都是使用批量操做,当网卡队列在收到32个数据包后,一次性将数据包传输到内存中;同时使用HPET时钟(RDTSC指令也行)定时处理不够32数据包的情形,避免响应延时。
使用无锁数据结构,进程间使用无锁的环形队列,dispatch进程将数据放不一样的无锁的ring buf里,供处理进程来获取,避免了加锁的等待延时。使用了预取操做,主要用在了从网卡队列读取数据包进行检验后传递到内存的过程当中,这样在对数据帧校验过程当中的同时,也并行地将下一个数据包放到cache当中,节省了数据传输延时。
合理的使用分支预测,在大几率条件语句前加上likely,反之加unlikely。
预分配内存,依据队列大小,为每一个进程预先独立分配UDP报文处理所须要的空间,在构造UDP应答包结构的时候直接经过空闲指针链表获取便可。避免拷贝数据包。Dispatch进程将数据报文经过DMA方式传递到内存以后,只是将报文地址放入到ring buf,UDP处理进程能够在原地址处直接来解析数据包并就地修改,而后通知Dispatch是否要由网卡发出去,或者丢弃。
数据包头部和尾部预留空间,这样在修改数据包的内容或填充新的头部、尾部的时候不须要从新申请空间了,而直接在原处修改便可。减小内存访问是优化的重点,也是难点,上面的不少方法其实均可以减小内存访问,但最重要的是要在程序中避免复杂变量的拷贝,多使用指针,这也须要很是当心的编写代码。也不能为了减小内存拷贝而把全部字符串复制都修改成指针,这将致使极难维护和调试。
经过硬件指令加速hash计算,甚者直接使用intel的crc指令来计算hash,比传统纯软件hash算法性能大幅提高。避免过渡校验:一个未被修改的参数从头至尾只须要校验一次,事先能确保字符串以\0结尾后面也能够减小一些判断机制。
还有不少其它的用来提升程序性能的编程方法和技巧可能没列举出来。总之,在设计和开发系统以前须要对硬件和操做系统有较深刻的了解,并保持着以追求性能为目标的心态来编写每一个函数和模块,那么写出来的程序性能也不会差了。在高性能服务程序设计过程当中,也不可能把上述各项优化点作到最优,而是结合时间成本上的考虑,获得一个合理的优化性能目标,最重要的仍是要保证稳定性。
[1]. www.kegel.com/c10k.html ,Internet
[2]. c10m.robertgraham.com,Internet
[3]. I ntel Data Plane Development kit:Software Architecture Specification , Intel
[4]. Avoiding and Identifying False Sharing Among Threads, Intel
[5]. 《大话处理器》,清华大学出版社