原创内容,转载请注明: [http://www.ssdfans.com] 谢谢!
作者: 王豪迈
想要和作者还有《大话存储》作者冬瓜哥、《PCI Express体系结构导读》作者王齐、《蛋蛋读NVMe》作者蛋蛋等全世界的大牛讨论SSD及存储相关技术?加nanoarch为微信好友,拉你进ssdfans微信群。
欢迎给ssdfans投稿,投稿就能加入ssdfans作者群,和冬瓜哥,蛋蛋等大咖切磋武艺,还有稿酬拿.
作者王豪迈目前是XSKY CTO,为Ceph核心开发者,为Ceph贡献了4%的代码。
高速缓存即使在C程序员中也很少接触,因为对高速缓存的显式操作是依赖于处理器架构的,不同架构导致很大的不同,并且硬件对高速缓存已经做了很大的优化,使得软件部分可以忽略高速缓存部分。但是这里,我们依然会介绍在x86和x86-64下对高速缓存部分做优化处理的方式。
在多核处理器众多的情况下,我们无法忽视多核并行情况的存在,但是单执行体运行是多核并行的基础。因此,对单核执行的缓存优化是多核并行的子集,我们可以分开考虑。从这里也可以看出,多核并行实质是增加了软件的复杂度。多数情况下,多核并行需要考虑更多的东西。
以下分析基于《What every programmer should know about memory》(http://lwn.net/Articles/250967/)系列,并且参考了其他众多blog。
Contents
一、忽略缓存写入
二、高速缓存访问优化
L1 Data缓存访问优化
L1 Instrument缓存访问
L2和更高层缓存访问
TLB访问
预取
三、多线程优化
并发问题
原子操作
总线带宽考虑
一、忽略缓存写入
在平时的程序中,很多时候可能产生了数据但是短期内不适用。如一个写线程专门负责向内存写入数据节点,而高速缓存中一行大小是大于节点大小的,所以高速缓存中一行可以容纳多个节点。当它写入第一个节点到缓存,在写下一个节点时,缓存会刷新脏数据到内存并重新读取。这就导致了很大的性能问题。
我们可能通过写合并来实现提高性能,在X86和x86-64结构中,gcc提供了一些内置函数用于实现这一方法:
| 12345678910111213 | #includevoid _mm_stream_si32( int *p, int a);void _mm_stream_si128( int *p, __m128i a);void _mm_stream_pd( double *p, __m128d a);#includevoid _mm_stream_pi(__m64 *p, __m64 a);void _mm_stream_ps( float *p, __m128 a);#includevoid _mm_stream_sd( double *p, __m128d a);void _mm_stream_ss( float *p, __m128 a); |
值得注意的是,由于x86架构中内存屏障的存在,绕过缓存可能和缓存的顺序写差不多,而且通常会带来随机写的损失。
二、高速缓存访问优化
L1 Data缓存访问优化
我们都知道在代码中应该尽可能提高空间和时间局部性来提高对缓存的利用,但是优化很大程度上取决于一行缓存的大小。
在L1d缓存中,如果在Linux上,我们可以通过系统内置命令得到L1 Data的行缓存大小:
getconf LEVEL1_DCACHE_LINESIZE
或者通过/sys/devices/cpu/…来查看每个逻辑cpu的缓存大小。
如果是mac osx:
sysctlbyname(“hw.cachelinesize”, &line_size, &sizeof_line_size, 0, 0);
通常情况下,不同层次的缓存的行大小是相同的。
比如大矩阵的计算,我们可以通过将大矩阵分为缓存行大小字节的小矩阵来进行局部计算,通常情况下可以提高4倍的速度。
另一种情形是结构数组:
| 12345 | struct foo {int a;long fill[7];int b;}; |
在编译器内存对齐优化后,这个foo结构用了68个字节存储,而本身这个结构只有64字节,这时候访问一个结构我们就需要两行缓存。
我们可以通过gcc内置命令来指定对齐:
struct strtype {
…members…
} __attribute((aligned(64)));
但是如果有大量的结构数组时,内存对其的结构访问速度是不对齐结构访问速度的3倍。因此,即使架构是支持不对齐访问,不对齐的结构也需要谨慎考虑。
L1 Instrument缓存访问
好的代码局部性主要有两方面:
1. 条件跳转被很好预测到
2. 减少代码量来取得循环展开和内联的平衡
3. 代码内存对齐
在条件跳转上,实际上我们可以通过对编译器做两种方案来提高分支预测的准确性。第一是先编译后运行会得到一份性能报告,编译器重新利用这份报告生成代码会极大提高分支预测准确度。第二是显式的指定分支判断。
gcc中支持__builtin_expect来指定分支。
| 123 | long __builtin_expect( long EXP, long C);#define unlikely(expr) __builtin_expect(!!(expr), 0)#define likely(expr) __builtin_expect(!!(expr), 1) |
我们通过likely宏在条件跳转上显式的指定分支。
在代码量上,intel core2有一个特征:当一个循环少于18个指令,只需要4个解码指令,最多有4个分支指令,这个小循环会加快16倍的执行速度。
在代码内存对齐上跟数据对齐类似的因素,但是线性的读取指令会取到更好的效果。
L2和更高层缓存访问
这跟L1相比,很大的改变是缓存未击中的惩罚大大提高,而且通常L2以上缓存会被多个核共享,实际每个执行体的缓存大小会大大少于总缓存大小。
TLB访问
提高TLB的缓存命中率主要靠减少页数量和减少TLB查询深度,但是这些因素很大程度上无法控制。
能被程序员影响的只有通过mmap的MAP_FIXED选项来直接访问,但是这个动作也是危险的,很有可能是page fault。必须对所选的请求地址清楚。
预取
分为硬件预取和软件预取,硬件预取依赖于处理器架构,但好处是不需要对代码做改动。软件预取通过显式调用函数来预取。在x86和x86-64架构上可以采取以下函数:
| 123456789 | #includeenum _mm_hint{_MM_HINT_T0 = 3,_MM_HINT_T1 = 2,_MM_HINT_T2 = 1,_MM_HINT_NTA = 0};void _mm_prefetch( void *p, enum _mm_hint h); |
三、多线程优化
并发问题
在多处理器架构下,多线程并行写入同一内存位置时,由于缓存一致性问题,会导致很大的性能问题,这种现象也称为False Sharing。多处理器缓存一致性通过MESI协议(en.wikipedia.org/wiki/MESI_protocol)实现,缓冲访问一致通过复杂的状态来实现。而x86和x86-64架构实现的是缩略版的MESI协议。在多核情况下,类似情况会有所好转,但是也会有一定的性能损耗。
简易的解决方案是通过将共享变量写入不同的缓存行来实现提高。但是如果变量较多会导致较大的空间使得缓存命中降低。
我们可以将共享全局变量放入不同的段中来减小数据占用。
| 1234 | int foo = 1;int bar __attribute__((section( ".data.ro" ))) = 2;int baz = 3;int xyzzy __attribute__((section( ".data.ro" ))) = 4; |
而如果一个变量只被一个线程使用,我们可以使用线程变量:
| 1234 | int foo = 1;__thread int bar = 2;int baz = 3;__thread int xyzzy = 4; |
线程变量使得不同线程有自己的同名变量,但是如果该变量只被一个线程使用,会造成一定的浪费,因为线程变量是在创造线程时产生,过多的线程变量会使得产生线程变慢并且带来数据空间浪费。
原子操作
目前x86和x86-64架构主要实现了CAS(compare and swap)和Bit Test原语。(http://en.wikipedia.org/wiki/Atomic_operation)
总线带宽考虑
多线程如果在多核和多处理器下并行,考虑以下情况:两个线程分别在两个处理器的一个核上运行,运行一段时间后被内核抢占然后调度运行。当两个线程重新恢复运行时,如果又被分配到不同的核上,导致原来的缓存失效使得数据迁移忙。
我们可以通过设置cpu和核的亲和度(affinity)来绑定线程到某个核上。
| 12345678910 | #includeint pthread_setaffinity_np(pthread_t th, size_t size,const cpu_set_t *cpuset);int pthread_getaffinity_np(pthread_t th, size_t size, cpu_set_t *cpuset);int pthread_attr_setaffinity_np(pthread_attr_t *at,size_t size, const cpu_set_t *cpuset);int pthread_attr_getaffinity_np(pthread_attr_t *at, size_t size,cpu_set_t *cpuset); |
经作者同意,转载自博客http://www.wzxue.com/cephfs-update/
喜欢就请分享转发!
怎么阅读ssdfans其他文章?进入www.ssdfans.com ,用搜索框搜索关键字即可。
不想错过后续精彩文章?长按或扫描下面二维码关注ssdfans就可以了!

ssdfans微信群介绍
技术讨论群 覆盖2000多位中国和世界华人圈SSD以及存储技术精英
固件、软件、测试群 固件、软件和测试技术讨论
异构计算群 讨论人工智能和GPU、FPGA、CPU异构计算
ASIC-FPGA群 芯片和FPGA硬件技术讨论群
闪存器件群 NAND、3D XPoint等固态存储介质技术讨论
企业级 企业级SSD、企业级存储
销售群 全国SSD供应商都在这里,砍砍价,会比某东便宜20%!
工作求职群 存储行业换工作,发招聘,要关注各大公司招聘信息,赶快来
高管群 各大SSD相关存储公司高管和创始人、投资人
想加入这些群,请微信扫描下面二维码或加nanoarch为微信好友,介绍你的姓名-单位-职务,注明群名,拉你进群。
