2024总结
2024 总结
总结一下 2024 年写码的一些经验和收获。
算法
没有什么突破性的创造,世上比我高明的人太多,catchup 都是很大的问题。记录一下遇到的高端操作好了。
Buffer Pool & Ring Buffer
低延时操作中不能在使用时 new
对象,因为这会导致底层 sbrk
申请内存空间,且得到的对象碎片化特别严重,从而延时飙升。习得的操作是初始化时申请一个内存池,自己的对象放在池中。
Ring Buffer 解决信息传递的生产者-消费者问题。只有一个读者和一个写者时,维护读位置和写位置可以在不加互斥锁的情况下实现消息队列。但是此过程依然需要保证 memory barrier
,即进程在写入和读取的先后顺序上始终保证。此过程可以用 atomic
变量实现,也可以直接插入 rmb
或者 wmb
这样的汇编。在 x86
架构上原子变量本来就时保证全序的,不必纠结到底是什么屏障最终都一样。原子变量的延时高于单纯的 mb
,应该是有别的附加功能在里面。
OS
低延时与CPU调度
如果程序被调度下cpu,将会带来很大的上下文切换的延时。所以程序在运行时需要绑定CPU核心。最简单的方式是 taskset
:
1 | taskset -c 0,1,2,3 <your_program> |
这表示程序将在 0-3 核心上运行。或者也可以只绑定一个核心。使用 lscpu -e
或者 lscpu
查看cpu信息。
即使绑定了单一核心,进程依然可能被调度下cpu让给其他进程。要想cpu调度时永远不抢占,需要隔离cpu核心,再把进程放上去。在 /etc/default/grub
中找到 GRUB_CMDLINE_LINUX_DEFAULT
行,并添加 isolcpus
参数
1 | GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=4,5,6,7" |
然后更新 grub
1 | sudo update-grub |
验证是否成功
1 | taskset -cp 1 |
输出不包括 4,5,6,7
即成功。
IRQ balance 设置问题还没考虑
应当关闭超线程。
重新思考万物即文件
零拷贝指的是数据不要到用户层转一圈。mmap或者sendfile都考虑的是这个问题。
mmap是Linux提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。
sendfile直接指定文件描述符和套接字,发送指定大小的数据。但是这里从内核缓冲区到套接字缓冲区仍然可能有cpu拷贝。如果硬件支持DMA,此操作不用cpu完成,而是直接从内核缓冲区DMA数据到网卡。
读文件时mmap未必快,且占用真实内存越来越大。经过实验,python中使用 with open(file) as f
的读取方式效率是最高的,且代码最简洁。
profiler and core dump
两个很重要的需要root权限的工具。前者用于监控程序的开销,后者在程序跑飞时查看原因。回头补上。
1 | ulimit -c unlimited # 允许生成 无限制大小 的 core dump 文件 |
在 core dump 时,使用 gdb 调试:
1 | gdb <exe_file> <dump_file> |
1 | [Thread debugging using libthread_db enabled] |
C++
Log 技巧
如果程序要打日志,则字符串加IO操作必然慢到爆炸。本人还没能搞定一套特别舒服的日志库,所以只能论述一下原理。
首先,你的写日志一定是不能和核心逻辑在一个线程上运行的,起码得保证这个日志是核心逻辑没事干了再去处理。这里是开线程还是进程,怎么处理异步、线程池、缓冲区,说实话我还没想明白。
其次,调用日志记录时,最好把所有的内容都放在单独的参数中,最终的字符串放在写日志的逻辑里生成。这里参考一下 printf()
,搞一个可变参数列表接收参数,回头再处理。
最后,日志库怎么保证线程安全?很显然写文件不能是我一条写到一半中间插入了另一条,对写入操作加锁可以实现,但是有没有更好的解决方案?
只能说 C++
的开源日志库都乱七八糟,最基本的问题都不能很好回答。网上论坛动不动捧一踩一,反正也未见得完美。
计时。精确的clock使用
rdtsc`。
刚看到一条外国 prop 大佬的贴子。高频的交易 FPGA+share memory。实盘上没有book management,O3 + 没有log,log放在replay处理。非实时的risk control。
Class
几点重要事项:
-
父类中是虚函数,子类继承时自动时虚函数。子类继承时最好写上
override
关键字。 -
析构函数是虚函数
如果基类的析构函数不是虚函数,删除一个指向派生类对象的基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类的资源没有得到正确释放,可能导致资源泄漏或未定义行为。
-
静态成员函数
static
static
关键字的类函数无需实例即可调用(使用类名)。 -
不要
memset
一个类的实例,虚函数表可能被破坏!
初始化
对于聚合类型(没有用户定义的构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、虚函数 和 成员不使用访问控制(如 private 或 protected)的 结构体 或 类)可以使用命名初始化指定部分变量的值:
1 |
|
这里初始化列表的变量顺序不可违背。
接口与实现分离
暴露给用户的类只是封装,类里面有一个Impl类的指针。这个Impl类只有前向声明(forward declaration)。文件夹下的 person.hpp
personImpl.hpp
person.cpp
personImpl.cpp
展示了基本的概念。用如下命令编译为动态链接库:
1 | g++ -fPIC -shared -o libperson.so person.cpp personImpl.cpp |
使用时只要引入 person.hpp
并链接 libperson.so
:
1 | g++ personuse.cpp -L. -lperson -o person |
好处多多,自行揣摩。
一点思考:这里的参数能不能完美转发?
Hint:完美转发参数的要求是什么
两点思考:只是传递引用,在哪个类的参数定义为引用?
CRTP
CRTP(Curiously Recurring Template Pattern) 主要的特征是:类 Derived
作为模板参数传递给其基类 Base
,即 Base<Derived>
。它可以为代码带来一些重要的优化或能力,如静态多态性、类型安全等。
通过强转指针类型,可以调派生类的函数,如果函数为 static
,则可以直接调用模板类的函数。
1 | /* |
此处虽然接口和实现分离了,但是并没有产生虚函数。调用时几乎没有额外开销。
函数指针与包装
1 |
|
std::bind
可以将函数和参数“绑定”在一起,返回一个可调用的对象。这个对象可以被传递给线程池中的线程。std::function
用于存储这个可调用对象。
1 |
|
STL
遍历一个 STL 的过程中不要轻易删除元素!!!
其他
完美转发、线程池、异步操作、future
关键字等等。八股永远学不完,感觉目前水平也还没能够用上,函数中有引用能够处理干净,偶尔有个 template
都不错了。这条路还有很长要走。
Python
最大的教训就是不要有自己编译安装的执念。都python了就不要在意编译时是不是能多一个更适合当前机器的参数了,搞到最后缺一个库递归安装最后把自己爆了简直让人破防。如果是自己用,直接 conda 搞个env是最方便的。
如果是系统缺包,直接 sudo
安装在 ubuntu 22.04
之后就不行了,所以要在最后加上 --break-system-packages
强行安装。这里常见的问题是 docker
需要降级 requests
包:
1 | sudo pip install requests==2.31.0 --break-system-packages |
jupyter
虽好,亦有问题。
- 如果修改了底层的package,重新运行 cell 未必能加载新的,需要重启内核。
- 在
pybind11
生成的package运行时,存在大量输出直接爆output的情况。这中间的IO缓冲肯定有问题但是我找不到,直接转成.py
文件就好了。
Web 相关
都用 Python
了就不要太在意性能,而应当注重结果的可读性以及时间成本。快速可视化展示的神器。
flask
适合用于 Web API
的交互,处理请求并作出相应。请注意 API
的无状态性,响应结果不要依赖于前一次的请求。
flask
的程序不要死的太快,死了也不要让使用者有察觉或者影响下一次调用。不要直接用 python myapp.py
运行程序,而要安装 uwsgi
来守护。控制一下每个进程最多处理多少个请求就重启,别内存炸了。
可视化界面使用 streamlit
展示。完全不用考虑前后端,针对特定的 URI
安排访问时后台生成数据的逻辑,安排交互按钮需要实现的反应,把内容直接展示出来就好了。依然使用守护进程,不要突然挂了。至于后台是调本地文件还是查找数据库完全分离不受限。
Gtest& Pytest
两个测试框架
千万不要把判定语句放在被多次调用的函数或者宏里面,而是直接放在返回值位置,否则即使告诉了哪一行出错,依然不知道具体是哪一步出了问题。GDB
是个好东西,python
也是可以用 GDB
调试的,这部分能力我太乐色,日后想办法提升一下。
奇技淫巧与其他
延迟可以直观理解吗
总是说低延迟,多少算低?有交易公司说自己可以做到 ns
级相应。
考虑一个标准的千兆网络,其传输速度大约为 120 MB/s
。说明其传输 120 Byte
的数据需要 1 us
。一条行情,包括报文头部、标的信息、成交信息或者snapshot信息,大约就在这个数量级。接收到这个信息都要 1000 ns
,下单估计也是这个数,还假设了没有传输的时间。简单估算,cpu加减的时间在 ns
数量级,如果有什么奇技淫巧可以线速度处理的话这部分倒是真纳秒级别(那就是纯简单逻辑发单)。但是你估算几个不平衡因子,加上EMA窗口在总体加权一下,怎么着量级也在微秒了。因此,总体来说报单响应在 us
级别我觉得是合理的,优化时也应当考虑这个量级的优化。
之前从未发现消息发送的延迟都在
us
级别,这倒是给我很大震撼。这样来看,好的交易系统应当选择直连且带有网卡加速,万万不可依赖转发的信息。
OK 看到有25G低延迟光纤网卡,延迟可以缩小一级。DPDK或者加速卡,直接把数据DMA zero-copy,这边2025年期待一下能不能看懂。
零拷贝不是0次拷贝,是内核缓存区到应用缓存区0次拷贝
RDMA Kernel Bypassing
proxy
Linux下终端设置代理
1 | export http_proxy="http://10.1.100.130:8080" |
windows终端设置代理
好像没用 回头看看怎么回事
1 | set http_proxy=http://10.1.100.130:8080 |
换行符的坑
不要随便认为换行就是 \n
,有可能是 \r\n
。在一个字符串中,希望得到 content-type:
之后这行的内容,不要按照 \n
切割,否则都不知道怎么死的。参考的方式如下:
1 | std::string get_type(const std::string &input) |
文件命名的坑
千万不要随意起文件名,尤其不要让文件名和系统中存在的文件名一样,比如 tuple.cpp
numpy.py
之类的。不然在import的时候可能就会调到自己的文件,且此问题较难发现!!!
CMake
如果新版本的 gcc
链接了老版本的 gcc(GCC 4.x)
生成的 so
文件,在 std::string
的处理上可能会有问题。最常见的是 std::string
作为返回值的函数直接找不到了。
在
GCC 5
版本之前,C++
标准库使用的是旧的ABI
(应用二进制接口),它与C++11
标准中的一些新特性(如std::string
和std::vector
的实现)不兼容。为了支持C++11
和之后版本的特性,GCC
引入了新的ABI
。从GCC 5
开始,默认使用新的ABI
。
在 CMakeLists.txt
中,指定使用旧版本 ABI
:
1 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_USE_CXX11_ABI=0") |
贴一个基本的模板:
1 | cmake_minimum_required(VERSION 3.10) |