Memory Access
Address Alignment
在内存中存取一个变量最高效的形式是将其放在一个能够被它的长度整除的地址上。
(void *)&variable % sizeof(variable) == 0
所谓的按某个长度对齐就是这个意思。GCC编译器会主动帮咱们解决这些事件。比拟非凡的形式是将一个大型的构造体,或者动态数组按64byte的形式对齐:
int BigArray[1024] __attribute__((aligned(64)));
这次要是思考到CPU的Cache Line长度多为64byte,变量按64对齐能够使其正好开始于一个Cache Line,缩小Cache Miss/False Sharing以及利用CPU的高级指令集并行计算。
Note: _attribute _((aligned(x)))有时只对全局变量无效,而对局部变量有效。
Huge Page
大页技术是以后风行的一种性能优化技术。在Linux零碎中有一套简单的过程虚拟地址和内存物理地址的转换机制,简单的细节咱们不去关怀,只须要晓得Linux是通过页(Page)这一机制(Look-up table)来确立两者的对应关系的。简略的类比就是在一本2000页的书中找到某一个章节,远比在一本2页的书中简单。思考到传统页面4KB的大小和大页2GB的小大之差,这个类比还不是那么失当。
在CPU中,须要以缓存的模式存储一些转换关系,这种缓存成为TLB Cache。应用大页能够缩小TLB Cache Miss。
Virtual addr maps to physical addr
Note: Huge Page能够在绝大部分状况之下进步性能,但并不是所有状况下都能够起到晋升性能的成果。对于内存,须要综合思考各种因素,进步性能的根本策略还是以空间换工夫。具体的剖析文章请
DPDK学习路线以及视频解说+qun832218493获取
======================================================
1.dpdk PCI原理与testpmd/l3fwd/skeletion
2.kni数据流程
3.dpdk实现dns
4.dpdk高性能网关实现
5.半虚拟化virtio/vhost的减速
NUMA
严格来说NUMA并不是一种性能优化技术,而是一种内存架构。
NUMA Architecture
每一个CPU Core都与它本地连接的内存间接相连,独享总线,具备最快的读写速度。如果去近程(remote)内存去读写的话,则须要跨CPU Core执行。在DPDK中,有一整套精美且高效的内存调配和管理机制,联合大页和NUMA等机制,根本准则是,将一个CPU Core须要解决的数据都放在离它最近的内存上。
相干的实现能够参考DPDK中memseg
memzone
等内存机制相干代码的实现,这里能够有专门文章介绍。
Polling Mode Drive(PMD)
是DPDK实现的优化Linux网络接管发送性能的模块,官网有具体的介绍材料。Click
Memory Pool
对于须要频繁填充开释的内存空间能够采纳内存池的形式事后动态分配一整块内存区域,而后对立进行治理,从而省去频繁的动态分配和开释过程,既进步了性能,同时也缩小了内存碎片的产生。
内存池多以队列的模式组织闲暇或占用内存。在DPDK中,还思考了地址对齐,以及CPU core local cache等因素,以晋升性能。
这里提到的内存对齐不同于后面仅仅将变量放在一个适合的地址“数目”上,而是综合思考了内存通道(channel)和rank,将变量(比方一个三层网络的Pkt),均匀散布于不同的channel之上(多依附padding),能够缩小channel拥塞,显著晋升性能。如图:
Two Channels and Quad-ranked DIMM Example
对于供多个线程同时应用的内存池,为了缩小对内存池的读写抵触,能够思考Local Cache的机制。即内存池为每一个线程/CPU Core保护一个Local Cache,本地的CPU Core对其操作是没有竞争的。每个CPU Core都是以bulk的模式从内存池中申请数据写入Local Cache或者将Local Cache的数据写入内存池。这样便大幅缩小了读写抵触。
Local cache per core
Linker Considerations
能够了解的一个简略事实是,如果常常应用的函数被贮存在指令内存的同一区域,甚至存储程序和调用程序统一,那么程序整体执行的效率将会有所晋升。
一个简略的办法就是尽量应用static
函数:
static void f(void)
将同一模块中的函数在链接阶段放在一起。但很多时候,处于程序模块化编程思考,模块之间相互调用的函数和办法并没有被显式地置于同一处指令内存,此时能够在要害函数集中采纳:
_attaribute_(section(X))
将函数显式地置于Read only(program memory)Section X,不便一起调用。同时也能够map文件的模式安顿指定。
Note: 同样的准则也实用于变量。
CPU
Advanced Instruction Set
应用先进的CPU指令集,带来的次要益处是能够并行实现向量化的操作,也就是所谓的SIMD(Single-Instruction-Multiple-Data)操作。
当须要对大型数据集执行雷同的操作的时候,向量操作能够带来显著的性能晋升。例如图像处理、大型矩阵计算、网络数据包还有内存复制操作等。
Note: 对于数据间有相互依赖和操作上有继承的运算,比方排序,并不适宜向量操作。
先进的指令集个别包含SSE
SSE2
AVX
AVX512
YMM
ZMM
。这些指令集对数据贮存的地址都有比拟严格的要求,比方256bit-YMM
要求数据按32对齐,512-bit ZMM
要求数据按64对齐。
对于向量操作,个别心愿合乎如下条件:
- 小型的数据类型:
char
short
int
float
- 对大型数据集执行相似的操作
- 数据对齐
- 数据集长度能够被向量长度整除
Note: 能够将要害函数写为针对不同数据集的不同版本,视运行环境编译运行。
Compiler
Branch Predication
CPU是以流水线的形式执行程序指令。所谓流水线,能够简略了解为在执行一个指令的同时,读取下一条指令。对于程序中大量呈现的if else
while
for
? :
等含有条件判断的情景,CPU须要可能正确提取下一条指令以便流水线能够晦涩执行上来。一旦提取的是谬误分支的指令,尽管不影响程序运行的后果,但整条流水线都会被清空,再从新读入正确分支的指令,对程序运行效率影响颇大。
CPU个别都有硬件分支预测器,但咱们也能够用likely()/unlikely()
等形式显示指定,另外在设计程序的时候也以使分支判断具备肯定的规律性为好,比方一组通过排序的输出数据。
Branchless Code
为了最大限度减小Branch mispredication对性能带来的影响,能够将一些常见的分支判断转换为Branchless的模式。比方返回两个数中较大的值,个别能够写做:
int max = (x > y) ? x : y;
这里其实隐含了一个条件判断。如果用branchless的模式,同样的性能能够写做为:
int max = x ^ ((x ^ y) & -(x < y));
当有大量调用,同时输出无甚规律性的时候能够思考采纳Branchless code。一个比拟全面的技巧共计在:Click。
loop-unrolling
Loop-unrolling的一大益处就是能够缩小循环分支预测的次数。对于简略的循环,CPU其实能够很好的实现分支预测的工作,但对于嵌套的循环,或者循环外部会扭转循环次数的循环,分支预测就变得艰难。loop-unrolling的特点能够用如下的例子阐明:
int i;for (i = 0; i < 20; i++) { if (i % 2 == 0) { FuncA(i); } else { FuncB(i); } FuncC(i);}
下面这个执行了20次的循环能够用loop-unrolling开展:
int i;for (i = 0; i < 20; i += 2) { FuncA(i); FuncC(i); FuncB(i + 1); FuncC(i + 1);}
- Cons:
- 循环只执行10次,缩小了一半
- 能够更精确得被CPU的branch predictor预测
- 没有了循环体内的
if
分支 - Pros
- 如果循环计数器是奇数,则须要特地的解决。
Note: Loop-unrolling也须要思考实用场合。次要实用于循环体的分支是次要的性能热点的时候。
Anti-aliasing
当有多个指针指向同一处物理内存(变量)的时候,称为pointer aliasing。作为编译器,并不能确认两个雷同类型的指针是否指向同一处地址,即对其余指针的操作,是否会影响另外的指针所指向的内存。这就要求每次碰到这两个指针其中的任何一个的时候,都须要从新从内存中读取以后值。示例如下:
void Func1 (int a[], int *p) { int i; for (i = 0; i < 100; i++) { a[i] = *p + 2; }}void Func2() { int list[100]; Func1(list, &list[8]);}
在Func1
中,有必要每次都从新读入*p
,并且从新计算*p + 2
,因为在Func2
的调用中,与list[8]
产生aliasing。对编译器来讲,它须要思考这种“实践上的可能”,从而付出大量的重复劳动。
当程序能够确认两个指针不会产生aliasing的时候,能够用关键字__restrict__
给编译器以明确的批示。
Prefetch
应用prefetch指令能够帮忙咱们提前预存一个将要应用的变量至CPU缓存:
_mm_prefetch
但在理论应用过程中要特地小心,古代CPU都有本人的硬件prefetch机制,如果不是通过测试,确认性能有所提高,尽量不要轻易应用该指令。这里有一篇材料对此有具体解释:Click
Note:须要确认CPU反对SSE指令集
Multi-threads
Lock-less
个别将GCC提供的一些原子操作视为“Lock-less code”。这些操作包含一些原子自增,CAS等操作。
__sync_fetch_and_add(type* ptr, type value)__sync_compare_and_swap(type *ptr, type oldval, type newval)etc...
这些操作尽管外表上没有了锁的痕迹,但实际上其汇编指令还是存在一个#lock锁总线的操作。所以也不用对其性能抱太高冀望。对于所有对于锁的操作,须要强调的是,锁自身并不影响性能,只有对锁的争抢才影响性能。
Local Cache
如同之前介绍的那样,还有一种策略是将工作尽量划分为不相互依赖的各局部,别离交给不同的CPU Core去解决,仅仅在后果汇总的时候有大量的锁操作。在DPDK中大量利用了这种思维。
Core Affinity
将一个工作指定交给某个CPU Core解决,能够缩小上下文切换和context switch的次数,以及进步缓存命中率。在Linux程序中能够通过
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);
来设定线程的CPU亲和性。
False Sharing
False Sharing也是在多线程操作中须要防止的缓存生效的问题。如果两个变量别离被两个线程操作,但它们呈现在同一条Cache Line中,则两个线程之间还是会相互影响。任何一个线程对该Cache Line的写操作,都会失整条Cache Line在另外一个线程处生效。如下图:
对此最简略的方法,是能够增加Cache Padding将两个变量分隔在不同的Cache Line之中,或者以Cache Line Size对齐的形式分配内存。