C++低延迟tips

参考资料以C++为核心语言的高频交易系统是如何做到低延迟的?

个人感想: 醍醐灌顶,感觉量化对于低延时高性能的要求非常高,很有学习的必要。

一句话总结版本

  1. 使用内存池,拒绝malloc和new
  2. 使用std::array,拒绝vector
  3. 多线程避免伪共享,可以用空间换时间,上下文切换及其的耗时间
  4. 一个线程占据一个CPU,对性能有要求的主机绝对不能发生IO
  5. 同一台主机交换数据使用零copy或者bypass,不同的主机使用RDMA
  6. 进程交换信息使用共享内存

1. 限制动态分配内存

相关的知识背景:glibc默认的malloc背后有复杂的算法,当堆空间不足时会调用sbrk(),当分配内存很大时会调用mmap(),这些都是系统调用,似乎会比较慢,而且新分配的内存被first touch时也要过很久才能准备好。

可取的做法:尽量使用vector或者array(初始化时分配足够的空间,之后每次使用都从里面取出来用)。尽量使用内存池。如果需要二叉树或者哈希表,尽量使用侵入式容器(boost::intrusive)。

性能测试:我测试的分配尺寸有64和8128两种。首先,我测试了glibc malloc的性能,分配64字节耗时98(sd247)ns,分配8128字节需要耗时1485(sd471)ns。其次,我写了一个多进程安全的内存池,分配64字节需要29(sd15)ns,分配8128字节需要22(sd12)ns。【内存池的细节见注释6】。最后,我单独测试了sbrk()和first touch的性能,但是数据不记得了。

2.使用内存池

参考资料:内存池介绍与经典内存池的实现

相关的知识背景:malloc和new比较慢,为什么慢?考虑到了多线程问题,同时可能有内存碎片的问题->内存碎片会导致什么?OOM访问磁盘,我们无法接受->如何解决? 使用内存池

内存池的优点:
(1)针对特殊情况,例如需要频繁分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程保护。也不需要维护内存空闲表的额外开销,从而获得较高的性能。

(2)由于开辟一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性,提升了程序性能。

(3)比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。

3.使用轮询,尽量避免阻塞

相关的知识背景:上下文切换是非常耗时的,其中固定的消耗包括(cpu流水线被冲掉、各种寄存器需要被保存和恢复、内核中的调度算法要被执行),此外,缓存很有可能出现大量miss,这属于不固定的时间消耗。

可取的做法:使用带有内核bypass功能的网卡。每个进程或者线程都独占一个cpu核【isolcpus和irqbalance的细节见注释3】,并且不停地轮询,用以保证快速响应。尽量避免任何可能导致阻塞的事件(如mutex),某些注定很慢的活动(比如把log写到磁盘上)应该被独立出来放到别的cpu上,不能影响主线程。

性能测试:网上有一篇博客[tsunanet, 2010]测试了mode switch、thread switch、process switch的耗时,但是这篇文章太早了,以后我要用我的新cpu重新测一下。这篇博客里面,系统调用只需要<100ns,线程/进程切换需要>1us(不包括缓存miss的时间)。

4. 传递消息时使用无锁队列

相关的知识背景:我只关注基于数组的无锁队列,其中:spsc队列是wait-free的,不论是入队出队都可以在确定的步数之内完成,而且实现时只需要基本的原子操作【为什么这很重要见注释7】;mpmc队列的实现方式则多种多样,但都会稍微慢一点,因为它们需要用一些比较重的原子操作(CAS或者FAA),而且有时它们需要等待一段不确定的时间直到另一个线程完成相应操作;另外,还有一种multi-observer的『广播队列』,多个读者可以收到同一条消息广播,这种队列也有sp和mp类型的,可以检查或者不检查overwrite;最后,还有一种队列允许存储不定长的消息。

可取的做法:总的来说,应该避免使用mp类型的队列,举例:如果要用mpsc队列,可以使用多个spsc来达成目的,并不需要mp队列;同理,如果是消息广播,也可以使用多个sp队列来取代一个mp队列;如果广播时observer只想订阅一部分消息,那么可以用多个spsc+有计数功能的内存池【具体做法见注释2】;如果要求多个观察者看到多个生产者的消息,并且顺序一致,那只能用mp队列了。总结一下,mp类型的队列应该尽量避免,因为当多个生产者同时抢占队列的时候,延时会线性增长。

性能测试:我写了一个mp类型的广播队列,传输的数据是8字节int,当只有一个生产者时,传输的延时是105(sd26)ns。增加观察者会使延时略微变大,增加生产者会使延时急剧变大(我用rdtsc指令控制不同生产者同时发送消息)。对于这个队列来说,它的延时只略高于跨核可视延时【测试结果见注释8】,所以应该算是不错了。

5.考虑缓存对速度的影响

相关的背景知识:现在的机器内存是十分充足的,但是缓存还是很小,因此所有节省内存的技巧都还有用武之地。

可取的做法:尽量让可能被同时使用的数据挨在一起;减少指针链接(比如用array取代vector,因为链接指向的地方可能不在缓存里);尽量节省内存(比如用unique_ptr<Data[]>取代vector,比如成员变量按照从大到小排序,比如能用int8的地方就不用int16);指定cpu affinity时考虑LLC缓存(同核的两个超线程是共享L1,同cpu的两个核是共享L3,不同NUMA核是通过QPI总线);会被多个核同时读写的数据按照缓存行对齐(避免false sharing)。

交换数据

不同主机使用RDMA,同一台主机使用bypass旁路

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值