交易系统开发技能及面试之低延迟编程技术

Posted BBinChina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了交易系统开发技能及面试之低延迟编程技术相关的知识,希望对你有一定的参考价值。

概要

关于低延迟相关技术,需要我们掌握cpu cache工作原理、kernel bypass、非阻塞编程、编译优化、硬件优化( CPU pipelines,FPGA)、软件优化(高效算法和数据结构)

CPU Caches

Cacheline(缓存行):内存与缓存的交互总是以cachelline大小进行拷贝。比如:cpu传输一个整型变量时(4或8字节),采用的是一个cacheline大小(主流cpu是64字节)进行传输,可以理解cacheline为最小缓存单位。

缓存的类型:指令缓存(程序指令),数据缓存(程序数据),虚拟内存与物理内存的映射缓存。

缓存等级:cpu寄存器 -> L1缓存 -> L2 缓存 -> L3缓存 -> 主存, 越靠近cpu寄存器的位置速度越快。

缓存一致性:对在不同的缓存和主存之间的数据一致性处理。

时间局部性 Temporal Locality:指最近使用的数据更有可能再被使用。

空间局部性 Spatial Locality:如果内存中某个位置的数据正在被cpu所使用,那么靠近这个内存位置的内存数据很可能在不久的将来被引用。

缓存预热 :当数据第一次被cpu引用时,数据被从主存放置到缓存中。缓存预热是为了增加缓存命中,而预测即将使用的数据并把其加载到缓存中。

**CPU 绑定,pinning **: 指将进程和某个或者某几个 CPU 关联绑定,绑定后的进程只能在所关联的 CPU 上运行,可提高缓存命中率。

Q1 分析代码段性能

分别采用行、列访问数据的方式:

// row major order
std::vector<double> v(n * n);
double sum0.0;

for (std::size_t i = 0; i < n; i++) 
    for (std::size_t j = 0; j < n ; j++) 
        sum += v[i * n + j];
    


// column major order
std::vector<double> v(n * n);
double sum0.0;

for (std::size_t j = 0; j < n; j++) 
    for (std::size_t i = 0; i < n ; i++) 
        sum += v[i * n + j];
    

根据空间局部性和Cacheline,采用行式访问数据性能更好,因为cpu读取数据时采用cacheline大小将内存空间上线性数据加载到缓存中。即采用行方式遍历数据时,下一个数据已在Cache中,提高了Cache的命中率。

Q2 采用alignas进行性能优化

观察以下代码:

struct Data 
	char c;
	char d;
;

Data data;

void thread1_func() 
	int sum = 0;
	for (int i = 0; i < 100000000; ++i) 
		sum += data.c;
	


void thread2_func() 
	int sum = 0;
	for (int i = 0; i < 100000000; ++i) 
		data.d = i % 256;
	

采用两个线程并发执行以上两个函数,因为Data结构的c\\d都是1个字节,而cacheline普遍为64个字节,所以当执行data.d的更新时,cpu会采用cacheline大小更新缓存到主存的数据,导致data.c无法命中缓存。因为data.d需要更新,那么我们可以设置缓存只更新data.d大小(1个字节),所以可以使用alignas制定内存对齐大小。

struct alignas(1) Data 
	char c;
	char d;
;

Q3 缩小锁粒度

long long sum = 0;

void sumUp() 
	long long tmp = 0;
	for (int i = 0; i < 10000; ++i) 
		tmp += 1;
	

	std::lock_guard<std::mutex> lockGuard(myMutex);
	sum += tmp;

当有多个线程对全局变量sum进行操作时,可以在对sum进行操作时才进行锁,而sum的累计先采用本地变量tmp进行计算后才更新sum变量。

kernel bypass

Kernel bypass是一种绕过OS与网络栈或者其他硬件进行数据交互的技术,通过减少用户态与内核态的数据拷贝(也叫零拷贝zero-copy)来提升性能。

非阻塞编程

通常将的NIO方式,比如sockets 或者 设计一套异步事件响应系统,在生产中常用Actor模型。
这一块的知识在前篇系列文章都有讲到过。

编程技巧

1、 避免动态内存分配
采用内存池(对象池),减少内存的分配释放以及内存碎片的产生。

2、 采用位运算替换数值运算
比如 n/2 可以采用 n >> 1的方式性能更高。

3、利用cacheline,让即将使用到的数据更紧凑的在同一个cacheline大小范围内,提高cache命中率。

4、交易系统场景里需要使用到浮点数值来表示数据,比如股票价格10.01,而优化的方式是使用整型:1010(price),小数点位为 2(factor)这种形式。

5、因为cacheline预加载cache,当数据量少时,采用线性查找比二分查找性能更好

6、

编译优化

采用__builtin_expect做分支预测。它是gcc编译器引入的一个指令,允许程序员将代码中最有可能执行的分支告诉编译器。具体的写法

__builtin_expect(EXP,N) // 其中EXP可以为变量,也可以为表达式

意思是,EXP==n的概率很大。

	//预测x更大几率为false
	if (__builtin_expect(x, 0)) 
		foo();
		...
	
	else 
		bar();
		...
	

以上代码生成汇编:

	cmp $x,0
		jne _foo
	_bar:
		call bar
			...
		jmp after_if
	_foo:
		call foo
		...
	after_if:

简化版的cpu流水线(cpu pipeline)

分支预测器位于整个CPU核心流水线的差不多最前端部分,靠近IF的级。从指令缓存里面读取指令时,需要由分支预测器来判断从哪里读取。
所以采用分支预测实际上是优化了分支预测期的缓存。

Q4 分析性能

随机初始化数组值,然后统计随机数中大于等于128的值

	const unsigned arraySize = 32768;
	int data[arraySize];
	for (unsigned c = 0; c < arraySize; ++c) 
		data[c] = std::rand() % 256;
	

	
	clock_t start = clock();
	long long sum = 0;
	for (unsigned i = 0; i < 100000; ++i) 
		//primary loop
		for (unsigned c = 0; c < arraySize; ++c) 
			if (data[c] >= 128) 
				sum += data[c];
			
		
	

	double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

	std::cout << elapsedTime << endl;//17.979

初始化完数值后,先进行排序,再统计大于等于128的值

	const unsigned arraySize = 32768;
	int data[arraySize];
	for (unsigned c = 0; c < arraySize; ++c) 
		data[c] = std::rand() % 256;
	

	//先进行排序
	std::sort(data, data + arraySize);
	
	clock_t start = clock();
	long long sum = 0;
	for (unsigned i = 0; i < 100000; ++i) 
		//primary loop
		for (unsigned c = 0; c < arraySize; ++c) 
			if (data[c] >= 128) 
				sum += data[c];
			
		
	

	double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

	std::cout << elapsedTime << endl; //5.909

排序后的执行性能提升了至少3倍,而且你使用vscode2019的诊断工具时,可以看到cpu有个跃式的进度跳动。
排序后的性能高是因为现代cpu都采用了长流水线工作方式,也即是前文讲到的cpu pipeline,在执行指令时会采用分支预测方式,在统计数值时:

if (data[c] >= 128) 
	sum += data[c];

而如果没有排序的话,分支预测的错误性更高且随机,那么指令缓存毫无意义,排序后使分支预测为常态。

漫谈

如果是高频交易系统的话,最好采用集中式服务,比如让数据、计算都在一个进程,让数据跟计算更紧凑,而不是采用当前的互联网微服务开发思维。

还有就是当前越来越重要的FPGA矩阵编程,可以饶过CPU在FPGA上进行市场数据分析

以上是关于交易系统开发技能及面试之低延迟编程技术的主要内容,如果未能解决你的问题,请参考以下文章

交易系统开发技能及面试之低延迟编程技术

交易系统开发技能及面试之无锁编程(Lock-free)

交易系统开发技能之并发编程面试题

交易系统开发技能及面试之TechCoding

交易系统开发技能及面试之TechCoding

交易系统开发技能及面试之TechCoding