共计 4820 个字符,预计需要花费 13 分钟才能阅读完成。
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 对齐的形式分配内存。