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

论坛 期权论坛 期权     
期权匿名问答   2021-8-25 08:27   17416   20
面试的时候问到了这个问题,本人是做基于C++的电信后台业务的,常用的性能优化手段也略知一二,但是这个问题就苦于无从入手了
分享到 :
0 人收藏

20 个回复

正序浏览
21#
期权匿名回答  16级独孤 | 2021-8-25 08:38:01 发帖IP地址来自 天津
有些回答的确很扯淡。
线程切换,信号量混洗的时间,都是毫秒级别的,如果真的要求微秒级的反应速度,就必须保证,交易期间不能发生任何线程切换,所以各类锁之类的都不能用。
有人居然说到多线程,异步io,那肯定是不可能达到要求的。
20#
期权匿名回答  16级独孤 | 2021-8-25 08:37:15 发帖IP地址来自 中国
在金融衍生品市场中,做市商(Market Maker)肩负着为期权期货产品报价(Quoting)的义务。“低延迟”对于这类公司而言至关重要,如果你的速度比别人快,同样的报价就可以优先成交,错误报价可以快速撤回,还可以抓市场上的错误定价进行套利。显然,人工下单肯定不可行,而且面对种类繁多的产品,人工报价容很易出现失误,所以我们需要开发交易系统来实现“低延迟”。
如今,大部分衍生品交易系统都是用C++实现,这固然与C++的一些优良特性密不可分,当然也有历史方面的原因。金融衍生品大约发展成熟于20世纪80年代,当时世界上主流的编程语言有C,C++,Fortran等。现在C++主要的竞争对手Java和C#都还没有出现。而C和Fortran并不太适合写大型程序,所以,C++在衍生品交易领域就成了主流的选择。
C++相比于虚拟机语言Java和C#,它直接把源程序编译为机器码,同时可以在编译及链接期间进行优化,以获得性能的提升。相比于动态语言Python和Lua,它减少了运行时的动态类型检测。因为C++没有垃圾回收(GarbageCollection)机制,所以不用担心延迟的不确定性。又因为它能直接编译成机器码,可以做底层优化,例如使用内部函数和嵌入汇编语言。
此外,C++做并行计算也相对比较容易,比如可以直接用CUDA。但是C++也存在诸多问题,比如编译链接速度慢且容易出错,缺乏其他语言常见功能的支持,开发效率低等等。但是C++也一直在发展,相信越来越多的问题会得到解决。所以,如果你想开发高性能的服务器程序,那么C++是一个很好的选择。
但是,低延迟与C++并不能划等号。有些公司用经过优化的JVM,用稍显小众的Ocaml, Haskell, Erlang等语言实现交易系统,也有不输C++的性能。与整体系统架构设计相比,编程语言的影响并没有那么大。交易公司也会租用交易所的机位,用光纤直连,以及把不需要经常变动的部分用硬件实现等等来降低延迟
招聘 中/高级C++ Developer- 上海(顶尖金融交易公司)
#C++#Linux#多线程#C++11#3-10年工作经验#香港交易市场#比特币交易#加密数字货币交易
有意者私信聊
19#
期权匿名回答  16级独孤 | 2021-8-25 08:37:02 发帖IP地址来自 北京
【招聘 Java、C++、Python算法】量化交易系统相关开发

工作地点:北京市朝阳区东三环北路甲26号博瑞大厦A座1608室
简历投递邮箱:hr@lzjf.com
福利待遇:
年终多薪+带薪节假日+六险一金+团建活动+下午茶+节日礼品
工作经验要求:硕士应届生或7年以内开发经验
学历要求:重点大学本科及以上学历
薪资范围:17k~40k

软件开发管培生招聘
岗位职责:
Java方向:
负责量化科技平台Java后台设计、开发;
Java核心技术与框架:mina或netty框架、socket底层通讯机制等;
C++方向:
开发交易策略平台,开发行情系统,包括市场实时和历史行情处理;  开发交易系统核心交易引擎、股票、期货等交易规则;  负责证券交易接口和期货CTP接口的开发封装。
Python算法方向:
量化分析系统开发
数据系统开发
信用风险系统开发
数据的读写,包括mysql 和 h5文件
清洗数据,要求对pandas, numpy,矩阵运算比较熟悉,熟悉金融数据的特点,至少熟悉一个统计包,对线性回归,凸优化有了解
任职要求:
1、985、211统招本科及以上学历,2017或2018年毕业,应届生硕士优先。
2、扎实的编程技术,掌握Java、C++、Python里的至少一门开发语言,对技术有浓厚的兴趣和钻研精神;
3、渴望在技术领域有重大的突破;
4、对量化交易有一定的兴趣,渴望在这一领域成为新一批大咖:技术上的高度与深度,行业上的全局性和量化专业知识,独特高效的团队管理能力。
18#
期权匿名回答  16级独孤 | 2021-8-25 08:36:34 发帖IP地址来自 福建
系统的扩展性和高速是一对矛盾,没法兼顾。纯软件方案,很多HTF是用C和汇编干,不写日志,单线程,网络全部muticast(多购买成熟成品,比如29west),楼上有提到tcp的有点扯淡,实际不可能。 也有用c++搞,这个要求有点高,不相信的话可以打开boost源代码的任意一个文件,如果可以流畅看懂那么基本达标,系统几乎没有任何继承,大部分工作都在编译时刻完成,非常重的模板嵌套,大量使用mpl,tbb,各种lockfree算法
17#
期权匿名回答  16级独孤 | 2021-8-25 08:36:18 发帖IP地址来自 北京
从战略上来说,避免任何可能的阻塞,以可预测的方式IO,包括但不限于:网络IO,磁盘,内存,甚至CPU的各级缓存。具体展开来讲:

网络IO:
使用非面向连接的协议,因为面向连接的协议有窗口,有可能引起阻塞。
磁盘:
使用Write Ahead Log(WAL),避免使用数据库。
内存:
1. 避免动态分配内存。(一次随机内存访问花费的时间和顺序访问对于CPU来说花费的时间相差两个数量级)
2. 零内存拷贝,不一定能做到,总之是拷贝的次数越少越好。
3. 数据无需编码解码,可以直接顺序从内存读取,直接可以在网络上收发。
CPU:
避免false sharing,cache miss等。

再具体到C++:
使用Pod,使用placement new,最好连Pod都不使用,直接读写ByteArray。

再具体到代码:
推荐Martin Thompson的两个库:Disruptor & SBE,这两个应该都有c++版本。
16#
期权匿名回答  16级独孤 | 2021-8-25 08:35:29 发帖IP地址来自 中国
FPGA是给手残的人用的,嘿嘿。因为很多人用不好强大复杂的硬件和OS,所以只好去用FPGA了。就如同不会开航母的人只好去开小舢板。
首先要够快拿到数据,你接的switch是不是ECN那边最快的switch。
其次你能多快的速度在开盘的瞬间处理100w条quote?FPGA,算了吧。想并发的同学可以试试GPU,比FPGA还犀利。一旦并发,你就已经输了。
第三你要意识到unpredictable delay并不是你的敌人,而是你的朋友。
15#
期权匿名回答  16级独孤 | 2021-8-25 08:34:51 发帖IP地址来自 北京
从C++角度提几点:避免用不必要的virutal function,因为这样compiler无法做足够的优化,在compile的时候不知道哪个函数会被用。用memory pool,用lock free data structure (Intel TBB),避免kernel call. 善用cache。少用std::map, std::unordered_map,用std::array, std::vector.用pool allocator. 别用std::string.

Colo大家都在用,没有什么特别的优势。FPGA是肯定有用的,那些在几个us水平的应该都是用了FPGA的。

Java美国有一家顶级的quant fund做HFT在用。我知道有些地方用Java写低延时的系统是要改写JVM的,不然一个GC延迟就上去了。
14#
期权匿名回答  16级独孤 | 2021-8-25 08:34:13 发帖IP地址来自 北京
《暗池》一书里讲了,他们做高频交易的,都是找交易所附近的房子租,并不是他们的算法比别人的好,而是能在别人大量买入一支或一类股票之前获取到信息,然后自己抄在他们前面买。反之如果提前获取别人在大量卖出一支或一类股票,他也能赶在别人之前卖出。大家都这样做的一个危害就是极易造成闪崩,我发现别人卖了,我也赶紧卖,越卖越跌。近几年美国股票闪崩比过去几十都多,就跟这个有关。
那本书讲的是美国,国内我也咨询过一些人,他们也说做这个要离那个地方近——那个好像不叫交易所,叫中登还是什么,反正就是最后买股票钱交易的地方。
这算是物理优化吧,你条件没人家好,可能计算机上再怎么优化也没用。
13#
期权匿名回答  16级独孤 | 2021-8-25 08:33:55 发帖IP地址来自 中国
回答问题的有多少是高频交易员?目前国内最重要的是你能比别人先拿到同一时间戳的Tick数据。搞不定前面毫秒级差异,再是挖空心思优化系统及程序那十几微秒,都是徒劳。
12#
期权匿名回答  16级独孤 | 2021-8-25 08:33:03 发帖IP地址来自 北京
1. 成熟的消息驱动框架
2. Zero copy/kernel bypass
3. CPU isolation
4. 精简业务层逻辑

有些交易所还嫌这不够快,在自己的matching engine边上摆起了机架租给这些做高频交易的...
11#
期权匿名回答  16级独孤 | 2021-8-25 08:32:27 发帖IP地址来自 广东广州
- 共享内存
- 无锁队列等无锁数据结构
- 避免动态分布内存
- 避免false sharing
- 尽量fit进cacheline,传递的数据越少越好
- 少log,甚至不log
- onload或者exablaze
- 核隔离
- interrupt绑定
- 各种book management的trick,可以考虑不同的数据结构,张开想象的翅膀
- 全系统无锁
-------------------------
2019-03-22 更
国内的“门道”是令人发指的。。。主要是分析清楚哪个地方是什么级别的延迟,安排好解决问题的优先级。
10#
期权匿名回答  16级独孤 | 2021-8-25 08:31:59 发帖IP地址来自 北京
早上看到,随便讲一点和CPP本身有关的特性。编译期决定,比如meta programming. 非锁同步. cache friendly optimization等等。速度实际上还要调。而不经过内核TCP栈的也是必须的。同机内通信用shared memory,集群内用组播
9#
期权匿名回答  16级独孤 | 2021-8-25 08:31:17 发帖IP地址来自 北京
问题中限定语言是C++,可讨论的范围就比较精简了。现有的答案都在谈系统架构层次上的东西,略显跑题。我对C++了解不多,但我尝试以一名C++程序员的视角,从基本思路出发做一个分析,抛砖引玉。

首先我们要明确系统的需求。所谓交易系统,从一个应用程序的角度来说,有以下几个特点:
    一定是一个网络相关的应用,假如机器没联网,肯定什么交易也干不了。所以系统需要通过TCP/IP连接来收发数据。数据要分两种,一种从交易所发过来的市场数据,流量很大,另一种是系统向交易所发出的交易指令,相比前者流量很小,这两种数据需要在不同的TCP/IP连接里传输。因为是自动化交易系统,人工干预的部分肯定比较小,所以图形界面不是重点。而为了性能考虑,图形界面需要和后台分开部署在不同的机器上,通过网络交互,以免任何图形界面上的问题导致后台系统故障或者被抢占资源。这样又要在后台增加新的TCP/IP连接。高频交易系统对延迟异常敏感,目前(2014)市面上的主流系统(可以直接买到的大众系统)延迟至少在100微秒级别,顶尖的系统(HFT专有)可以做到10微秒以下。其他答案里提到C++随便写写延迟做到几百微秒,是肯定不行的,这样的性能对于高频交易来说会是一场灾难。系统只需要专注于处理自己收到的数据,不需要和其他机器合作,不需要担心流量过载。

有了以上几点基本的认识,我们可以看看用C++做为开发语言有哪些需要注意的。

首先前两点需求就决定了,这种系统一定是一个多线程程序。虽然对于图形界面来说,后台系统相当于一个服务端,但这部分的性能不是重点,用常用的模式就能解决(也许这里你可以介绍一下常用的C++ Client/Server库,或者内嵌Web Server之类,相信应该有丰富的选择,这里不展开讨论)。而重要的面向交易所那端,系统其实是一个客户端程序,只需要维护好固定数量的连接就可以了。为延迟考虑,一定要选择异步I/O(阻塞的同步I/O会消耗时间在上下文切换),这里有两点需要注意:
    是否可以在单线程内完成所有处理?考虑市场数据的流量远远高于发出的交易指令,在单线程内处理显然是不行的,否则可能收了一大堆数据还没开始处理,错过了发指令的最佳时机。有答案提到要压低平时的资源使用率,这是完全错误的设计思路。问题同样出在上下文切换上,一旦系统进入IDLE状态,再重新切换回处理模式是要付出时间代价的。正确的做法是在线程同步代码中保持对共享变量/内存区的疯狂轮询,一旦有消息就立刻处理,之后继续轮询,这样是最快的处理方式。(顺带一提现在的CPU一般会带有环保功能,使用率低了会导致CPU进入低功耗模式,同样对性能有严重影响。真正的低延迟系统一定是永远发烫的!)

现在我们知道核心的模块是一个多线程的,处理多个TCP/IP连接的模块,接下来就可以针对C++进行讨论。因为需要对接受到的每个TCP或UDP包进行处理,首先要考虑的是如何把包从接收线程传递给处理线程。我们知道C++是面向对象的语言,一般情况下最直观的思路是创建一个对象,然后发给处理线程,这样从逻辑上看是非常清晰的。但在追求低延迟的系统里不能这样做,因为对象是分配在堆上的,而堆的内存结构对我们来说是完全不透明的,没办法控制一个对象会具体分到内存的什么位置上,这直接导致的问题是本来连续收到的网络包,在内存里的分布是分散的,当处理线程需要读取数据时就会发生大量的cache miss,产生不可控的延迟。所以对C++开发者来说,第一条需要谨记的应该是,不要随便使用堆(用关键字new)。核心的数据要保证分配在连续内存里。

另一个问题在于,市场数据和交易指令都是结构化的,包含了股票名称,价格,时间等一系列信息。如果使用C++ class来对数据进行建模和封装,同样会产生不可知的内存结构。为了严格控制内存结构,应该使用struct来封装。一方面在对接收到的数据解析时可以直接定义名称,一方面在分配新对象(比如交易指令)时可以保证所有数据都分配在连续的内存区域。

以上两点是关于延迟方面最重要的注意事项(如果真正做好这两点,大概剩下的唯一问题是给系统取个好名字吧:TwoHardThings)。除此之外,需要考虑的是业务逻辑的编写。高频交易系统里注定了业务逻辑不会太复杂,但重要的是要保证正确性和避免指针错误。正确性应该可以借助于C++的特性比如强类型,模板等来加强验证,这方面我不熟悉就不多说了。高频系统往往运行时要处理大量订单,所以一定要保证系统运行时不能崩溃,一旦coredump后果很严重。这个问题也许可以多做编译期静态分析来加强,或者需要在系统外增加安全机制,这里不展开讨论了。

以下是几点引申思考:

    如何存储系统日志?如何对系统进行实时监控?如果系统coredump,事后如何分析找出问题所在?如何设计保证系统可用性,使得出现coredump之类的情况时可以及时切换到备用系统?

这些问题相信在C++框架内都有合适的解决方案,我对此了解不多,所以只列在这里供大家讨论。

注:
    从开发语言角度上说,C++只是一种选择,并不是唯一的解决方案。简单的认为低延迟就等同于用C++开发,是不正确的。其他语言同样有可能做出高性能的设计,需要根据语言特性具体分析。关于整体的软硬件架构,可以看我的另一个回答:高频交易软硬件是怎么架构的?关于C++在性能方面的一些最新发展,包括内存结构的一些分析,可以参看:Modern C++: What You Need to Know
8#
期权匿名回答  16级独孤 | 2021-8-25 08:30:49 发帖IP地址来自 北京
先说,并不是c/c++的效率是决定因素。

最大的延时来自账户席位和网络延时,一席的账户成交优先级高于二席,二席又高于散户。怎样做倒一席呢?只要账户上有足够多的钱就可以。网络延时是最大的,因此在物理位置上离交易所核心机房越近越好,能直接放进去当然最好,如果不能,也要放到ping交易前置机在1ms以内的地方。证券公司会有资源,这要求动用你的一切力量争取到最满意的位置。早年间,这是场内交易和场外交易的区别。

接下来就是算法的效率了,这个可以抽象出来跟语言没关系,大多跟数学/统计模型有关系,然后是算法的实现,c/c++/fortran/汇编的效率确实很好,而且优化的空间很大,但是如果很复杂的算法matlab可能会优化得比自己写得好,那就用matlab实现。

这还没完,操作系统也可以调优,交易接口也可以不用交易所或者证券公司给的,自己分析通信协议重新实现;如果模型很复杂,计算量超大,那么就用并行计算架构,MPI, CUDA什么的用上。如果还要求绝对的速度,就用硬件实现算法, 这时候就轮到DSP芯片, FPGA什么的上阵,最后做一个专用的黑盒子。总之呢,就是所有能提高效率的地方,都是可以想办法做的。

但是,其实你要考虑的首先是,你的速度要求有多高,或者问你的交易策略是否真的需要那么高的速度吗?其次是投入产出比,你的算法是否真的能够挣足够的钱来支持你做各层面的优化。

以上很多虽然只有一句话,但是做起来东西很多,好多我现在也只知道概念,还不会做,提供个思路供参考。

----------+

忽然觉得题主可能理解错题了,考官想问的或许是:“如果指定你用C/C++做HFT , 你将怎么降低延时”,就是在语言和编程技巧这个层面上怎么做优化,我们都被带跑题了。我瞎猜。
7#
期权匿名回答  16级独孤 | 2021-8-25 08:30:23 发帖IP地址来自 中国
kernel bypass,使用专用网卡搭配用户态网络协议栈,比如 openonload。

Update:
其他的软件层面的优化可以做的还包括 使用 realtime scheduler,cpu pinning,memory prefaulting 等等;部署上可以采用 colocation,把机器放在交易所的机房;硬件上,可以采用专用的网络设备,当然机器配置越高越好。程序本身的优化手段和其它场景并无太多差异。
6#
期权匿名回答  16级独孤 | 2021-8-25 08:29:54 发帖IP地址来自 中国
楼上的扯蛋的比较多
算来也在一家比较大的prop干了不短时间了
最近都不说自己搞的是hft,因为很多人都声称自己搞hft,能得我们很无奈,所以我现在做的是ultra high frequency.
数据的话,t2o, tick to order, 700nanosecond
FPGA + share memory,很多std的东西都不能用,都是自己的lib, 都是单线程,没有lock,最少IO, log很少, 用C++主要是写起来简单,但大部分情况下都是C sytle
5#
期权匿名回答  16级独孤 | 2021-8-25 08:29:41 发帖IP地址来自 中国
低延迟的关键不在于效率优化,而是在于保证最差情况。
我们做音频开发,实时性要求可能比交易还高,专业程序通常要求在48k Hz的32个(甚至更少)采样点所对应的时间里不能超时。这个时间是非常短的,因为现代操作系统的时间片通常都有1毫秒,所以你不能进行任何可能导致自己被切出去的操作:
    不能做内存分配,因为可能进入和其它进程进行抢占的状态,自己被切出。不能用系统级别的锁。不能访问io,比如读写磁盘、打log。
为了达到这个目的,你需要:
    预先分配好所有的计算缓存,计算时直接用。UI线程和实时计算线程之间用原子操作通信。所有的硬盘资源读取都放到另一个线程,自己撸一个无锁的缓存机制。
4#
期权匿名回答  16级独孤 | 2021-8-25 08:28:49 发帖IP地址来自 北京航空航天大学
我们的程序的响应时间是10us(从收到行情到发出报单的响应时间),但是ping期货公司的交易前置机需要大约30us【这个数值会变化,见注释4】,所以网络延时占据了大量时间。

我所有的性能测试都是在一台DELL r630机器上运行的,这台机器有2个NUMA结点,CPU型号是E5 2643 v4(3.4GHz 6核)。所有的测试都是用rdtsc指令来测量时间,Intel官网上有一篇pdf文档[Gabriele Paoloni, 2010],讲述了如何精准地测量时间(要用cpuid来同步)。我自己做的性能测试的结果会写成“100(sd20)ns”的形式,代表平均值是100ns,标准差是20ns。我在算均值和标准差的时候会去掉最大的0.1%的数据再算,因为那些数据似乎并不是程序延时,而是cpu被调度执行别的任务了【原因见注释3】。有些性能测试在网上有现成的测试结果,我就没自己测,直接拿来用了,但是以后我会重新在我的机器上测一遍。

一些我们比较注意的点:
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.使用轮询,尽量避免阻塞
相关的知识背景:上下文切换是非常耗时的,其中固定的消耗包括(cpu流水线被冲掉、各种寄存器需要被保存和恢复、内核中的调度算法要被执行),此外,缓存很有可能出现大量miss,这属于不固定的时间消耗。
可取的做法:使用带有内核bypass功能的网卡。每个进程或者线程都独占一个cpu核【isolcpus和irqbalance的细节见注释3】,并且不停地轮询,用以保证快速响应。尽量避免任何可能导致阻塞的事件(如mutex),某些注定很慢的活动(比如把log写到磁盘上)应该被独立出来放到别的cpu上,不能影响主线程。
性能测试:网上有一篇博客[tsunanet, 2010]测试了mode switch、thread switch、process switch的耗时,但是这篇文章太早了,以后我要用我的新cpu重新测一下。这篇博客里面,系统调用只需要<100ns,线程/进程切换需要>1us(不包括缓存miss的时间)。

3.使用共享内存作为唯一的IPC机制
相关的知识背景:共享内存只有在初始化的时候有一些系统调用,之后就可以像访问正常内存一样使用了。其他IPC机制(管道、消息队列、套接字)则是每次传输数据时都有系统调用,并且每次传输的数据都经历多次拷贝。因此共享内存是最快的IPC机制。
可取的做法:使用共享内存作为唯一的IPC机制。当然,可能需要手动实现一些东西来保证共享的数据在多进程下是安全,我们是自己实现了无锁内存池、无锁队列和顺序锁【关于seqlock的疑点见注释1】。
性能测试:我使用了boost中的Interprocess库和Lockfree库,在共享内存上建立了一个spsc队列,然后用这个队列来传送数据,代码参考了stackoverflow上的一个答案[sehe, 2014]。我传送的数据是一个8字节整数,延时是153(sd61)ns。至于其他IPC机制,我在[cambridge, 2016]看到了一些性能测试结果,通常是要几微秒到几十微秒不等。

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<Data>,比如成员变量按照从大到小排序,比如能用int8的地方就不用int16);指定cpu affinity时考虑LLC缓存(同核的两个超线程是共享L1,同cpu的两个核是共享L3,不同NUMA核是通过QPI总线);会被多个核同时读写的数据按照缓存行对齐(避免false sharing)。

【注释1】:有一篇惠普的论文[Hans-J.Boehm, 2012]大致叙述了顺序锁的实现方法,但是那里面有两点让我感到困惑。一是需要用到thread_fence,这在某些cpu上可能会影响性能(x86似乎没影响);二是被保护的内容也必须是原子变量(可以是多个原子变量,所以被保护的内容可以很长)。但这是我见过的唯一一个符合C++标准的SeqLock的实现。
【注释2】:如果有M个生产者要发消息给N个观察者,可以建M*N个spsc队列和M个内存池,观察者只能读内存池里的数据,只有对应的那一个生产者可以修改内存池。我感觉这样应该会更快,但我没测过。
【注释3】:isolcpus可以隔离出一些cpu,避免其他线程被调度到这些cpu上执行。此外,设置irq affinity可以让一些cpu尽量避免响应中断,但在/proc/interrupts里面仍然有一些项目是避免不了的,而cpu处理中断时,用户程序会有一段时间(有时高达几十微秒)无法响应,我们没法解决这个问题。
【注释4】:在不同的时间点,ping的结果会有很大差异。交易时间段内ping出来的结果是30us,其它时间段ping出来的结果可能是几百微秒。我不知道这是什么原因,可能是期货公司为了省电关掉了某些东西?
【注释6】:我们要在共享内存上使用内存池,所以不得不自己写一个。我写的内存池只能分配固定尺寸的内存块,但是用户可以建立好几个内存池,用来分配不同的尺寸。实现的过程中有两个要点。一是用无锁链表来保存空闲的内存块;二是每个线程内部有一个缓冲区,所以真正取内存块的时候是没有CAS操作的。
【注释7】:在Intel x86的cpu上,如果C++中的内存顺序只用了acquire和release,那么编译出来的汇编代码里面不会有任何内存栅栏指令;如果同时也没有RMW(读-改-写)指令的话,无锁的代码编译出来就会像是普通的代码一样了。事实上,spsc队列的延时几乎等于跨核可视延时。
【注释8】:跨核可视延时:对于一个共享变量来说,如果有一个核上面的进程或者线程修改了这个变量,另一个核需要过一段时间才能看到这个修改,这段时间被称作跨核可视延时。我不确定在这段时间内,第二个核是会看到旧的数据还是这条指令会执行很久。在我的机器上,对于同一个cpu上的不同核心,这个值是96(sd14)ns。另外,对于同一个核心上的不同超线程,这个值应该会更小;对于同一台机器上的不同cpu,这个值应该会更大。

[cambridge, 2016]:Computer Laboratory
[Gabriele Paoloni, 2010]:Code Execution Times: IA-32/IA-64 Instruction Set Architecture
[Hans-J.Boehm, 2012]:http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf
[sehe, 2014]:Shared-memory IPC synchronization (lock-free)
[tsunanet, 2010]:http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html
3#
期权匿名回答  16级独孤 | 2021-8-25 08:28:33 发帖IP地址来自 北京
虽然是一个老问题,但这确实是一个很有意思的问题。凑个热闹,根据自己的经验来谈谈。(友情提示,重头戏在本文结尾处)

Scott Meyers曾经说过这么一句话“if you are not at all interested in performance, shouldn't you be in the Python room down the hall”。系统性能对交易系统的重要性不言而喻,而低时延对高频系统来说,就是非常重要的系统性能之一,哪怕说是最重要的也不为过。

那么在回答怎么才能做到低时延的时候,我们首先需要知道,对于一个高频交易系统来说,latency方面最大的bottleneck依次是哪些。而要知道latency的bottleneck,你又需要一个合理的测量时延的测试环境,这方面陈硕的回答已经很赞,我就不赘述了。

很有意思的是,通常取得最好效果提升的地方却是与编程语言无关的。比如,网络时延一般就是延迟方面最大的bottleneck之一。所以为了降低网络时延,我们需要colo,需要物理上与交易所的撮合机越近越好,需要高的带宽和最快的nic卡及其模式(比如选择合适的openload模式)。更高阶的,可以考虑使用fpga,或者是定制的nic卡。

与上一条息息相关的,其实就是在数据,比如市场信息,进入cpu之前(当然那种fpga进,fpga出的特殊解决方案除外),尽量减少数据拷贝以及context switches。比如,大家经常提到的Solarflare的nic卡就是通过interrupt kernel来达到kernel bypassing的效果。

以上这些说到底是为了解决更快的I/O,但这还不够,还进入讨论具体程序之前,需要一系列的server tunning。这里我只提几个比较显而易见的:1. disable hyperthreading,2. turn on over clocking, 3. disable Nagle's algorithm,4. set cpu affinity and isolation。

代码实现方面,大概可以参考以下几点(但不限于此):
    对于低时延系统,能用单线程解决问题,就千万不要多线程。
2. 一定要有一个hot-path的概念,在它范围内的代码需要仔细优化。当然,hot-path对于core dev和strategy dev的概念可能是有些许不同的。
3. 尽量让run-time的数据处理变得简单。在C++里面, 那就是template metaprogramming。能用CRTP的地方就别用dynamic polymorphism。能用expression templates来帮助计算的,就可以考虑使用它。
4. 尽量避免run-time的memory allocation。可以考虑重复使用同类的object,或者是memory pool,这样可以避免overhead,也可以减少memory fragmentation。
5. 要了解自己待处理的数据,这样在一定条件下可以允许undefined behavior的存在。比如,vector[] vs vector.at()。对于一个sub-microsecond级别的系统,safety check有时候都会expensive。
6. 利用好cache。基本的规则大概就是: 能在cache里面存下data和instructions,就不用access main memory,能在registers里面存下,就不要access cache。尽量使用contiguous blocks of memory,这也是为什么Bjarne Stroustrup本人也会推荐大家优先考虑使用vector。至于怎么写cache friendly的代码,可以参考这个: http://cppatomic.blogspot.com/2018/02/cache-friendly-code.html。
7. 注意好struct padding。也要留意在多线程情况下会出现的false sharing情况。
8. 避免不必要的branch和table lookup。使用virtual functions和大量叠加的if语句,都有可能增加cache misses和pipeline clearances的可能性。
9. 用好编译器提供的builtins,像是__expected,__prefetch之类。
10. 得了解编译器和连接器在做什么。比如,最好不要简单的假设-O2就可以帮你解决全部问题。有时候,O2/O3的优化,因为各种原因,反而会让代码变慢。比如: GCC fails to optimize aligned std::array like C array。
11. 大多数情况下,大家还是会首选用STL里面的container,但是还是需要谨慎,比如std::undered_map的性能对于低时延系统就不够用。

以上这些,虽然写下来看着感觉都不难,但都需要一定程度的经验积累,而如果有乐于分享经验的朋友或者同事,那可能就会事半功倍了,所谓他山之石可以攻玉!好了,重头戏来了!让我们来看看著名高频公司optiver的senior dev在cppcon17上都和我们分享了什么: https://www.youtube.com/watch?v=NH1Tta7purM。

其实有这样的同事真的是好事,比如DRW前员工Matt Godbolt就非常喜欢分享,他也有自己的channel: https://www.youtube.com/watch?v=fV6qYho-XVs。

细节决定成败,而很多细节知识又大多来源于经验和教训,大概这就是senior的意义吧?
2#
期权匿名回答  16级独孤 | 2021-8-25 08:28:01 发帖IP地址来自 北京
谢邀。只搞过 sell side,没搞过 buy side,只能算“实时交易”,算不上“高频交易”。工作以来一直在跟延迟做斗争,勉强可以说上几句。

要控制和降低延迟,首先要能准确测量延迟,因此需要比较准的钟,每个机房配几个带GPS和/或原子钟primary standard的NTP服务器是少不了的。而且就算用了NTP,同一机房两台机器的时间也会有毫秒级的差异,计算延迟的时候,两台机器的时间戳不能直接相减,因为不在同一时钟域。解决办法是设法补偿这个时差。另外,不仅要测量平均延迟,更重要的是要测量并控制长尾延迟,即99百分位数或99.9百分位数的延迟,就算是sell side,系统偶尔慢一下被speculator利用了也是要亏钱的。

普通的C++服务程序,内部延迟(从进程收到消息到进程发出消息)做到几百微秒(即亚毫秒级)是不需要特殊的努力的。没什么忌讳,该怎么写就怎么写,不犯低级错误就行。我很纳闷国内流传的写 C++ 服务程序时的那些“讲究”是怎么来的(而且还不是 latency critical 的服务程序)。如果瓶颈在CPU,那么最有效的优化方式是“强度消减”,即不在于怎么做得快,而在于怎么做得少。哪些可以不用做,哪些可以不提前做,哪些做一次就可以缓存起来用一阵子,这些都是值得考虑的。

网络延迟分传输延迟和惯性延迟,通常局域网内以后者为主,广域网以前者为主。前者是传送1字节消息的基本延迟,大致跟距离成正比,千兆局域网单程是近百微秒,伦敦到纽约是几十毫秒。这个延迟受物理定律限制,优化办法是买更好的网络设备和租更短的线路(或者想办法把光速调大,据说 Jeff Dean 干过)。惯性延迟跟消息大小成正比,跟网络带宽成反比,千兆网TCP有效带宽按115MB/s估算,那么发送1150字节的消息从第1个字节离开本机网卡到第1150个字节离开本机网卡至少需要 10us,这是无法降低的,因此必要的话可以减小消息长度。举例来说,要发10k的消息,先花20us CPU时间,压缩到3k,接收端再花10us解压缩,一共“60us+传输延迟”,这比直接发送10k消息花“100us+传输延迟”要快一点点。(广域网是否也适用这个办法取决于带宽和延迟的大小,不难估算的。)

延迟和吞吐量是矛盾的,通常吞吐量上去了延迟也会跟着飚上去,因此控制负载是控制延迟的重要手段。延迟跟吞吐量的关系通常是个U型曲线,吞吐量接近0的时候延迟反而比较高,因为系统比较“冷”;吞吐量上去一些,平均延迟会降到正常水平,这时系统是“温”的;吞吐量再上去一些,延迟缓慢上升,系统是“热”的;吞吐量过了某个临界点,延迟开始飙升,系统是“烫”的,还可能“冒烟”。因此要做的是把吞吐量控制在“温”和“热”的范围,不要“烫”,也不要太冷。系统启动之后要“预热”。

延迟和资源使用率是矛盾的,做高吞吐的服务程序,恨不得把CPU和IO都跑满,资源都用完。而低延迟的服务程序的资源占用率通常低得可怜,让人认为闲着没干什么事,可以再“加码”,要抵住这种压力。就算系统到了前面说的“发烫”的程度,其资源使用率也远没有到 100%。实际上平时资源使用率低是为了准备应付突发请求,请求或消息一来就可以立刻得到处理,尽量少排队,“排队”就意味着等待,等待就意味着长延迟。消除等待是最直接有效的降低延迟的办法,靠的就是富裕的容量。有时候队列的长度也可以作为系统的性能指标,而不仅仅是CPU使用率和网络带宽使用率。另外,队列也可能是隐式的,比如操作系统和网络设备的网络输入输出 buffer 也算是队列。

延迟和可靠传输也是矛盾的,TCP做到可靠传输的办法是超时重传,一旦发生重传,几百毫秒的延迟就搭进去了,因此保持网络随时畅通,避免拥塞也是控制延迟的必要手段。要注意不要让batch job抢serving job的带宽,比方说把服务器上的日志文件拷到备份存储,这件事不要在繁忙交易时段做。QoS也是办法;或者布两套网,每台机器两个网口,两个IP。

最后,设法保证关键服务进程的资源充裕,避免侵占(主要是CPU和网络带宽)。比如把服务器的日志文件拷到别的机器会占用网络带宽,一个办法是慢速拷贝,写个程序,故意降低拷贝速度,每50毫秒拷贝50kB,这样用时间换带宽。还可以先压缩再拷贝,比如gzip压缩100MB的服务器日志文件需要1秒,在生产服务器上会短期占满1个core的CPU资源,可能造成延迟波动。可以考虑写个慢速压缩的程序,每100毫秒压缩100kB,花一分半钟压缩完100MB数据,分散了CPU资源使用,减少对延迟的影响。千万不要为了加快压缩速度,采用多线程并发的办法,这就喧宾夺主了。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:400157
帖子:80032
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP