C++数据库项目性能优化总结

一、规范:

1.降低代码复杂度(CPU)。

2.减少内存申请释放。

3.合理利用异步多线程(进程)和锁。

4.算法及数据结构

5.使用性能高的三方库接口。

6.减少与三方库持久化交互,缩减数据量。

7.使用新语法新技术

8.电脑硬件分析及维护。

二、举例实操 

1.降低代码复杂度

1.1减少字符串比较

项目中用到了字段的比较,例如其中一个字段名为‘工厂检测温度指标’,如果该字段名确定,在进行修改或者创建该字段值时,不应直接用字段名去判断去操作,字符串比较较为复杂,应提前建立字段名与索引值的映射关系表,传入的字段名转为索引值后进行操作。(合理设计数据结构也能避免此问题)

#define 工厂检测温度指标Index 1
map<string, int> fieldIndexMap;
fieldIndexMap.insert(make_pair<"工厂检测温度指标",工厂检测温度指标Index>);
//
switch(map[工厂检测温度指标])
case 工厂检测温度指标Index:
//

1.2减少循环次数

多层多次循环需要检查是否有必要,能合并的合并。

能转化为map或者其他容器查询,避免三层以上循环。

即使用到了多层循环,尽量让循环次数多的在里面,循环次数少的在外面。

//循环查找可以直接用map取值
int a = map[indexString];

//多层循环小循环在外,a.size = 10,b.size()=10000
for(i<a.size)
{
    for(j<b.size)
    {}
}

1.3使用效率高最简单的容器和方法

例如能用unorderedMap,不用map,能用 vector不用map,能emplace_back不用insert、pushback等。

代码细节也需要注意,例如我们代码中出现过在map中已经找到了相应值,判断找到后又再次查找取值的问题。

Map<int,int> intMap;
//...
auto iter= intMap.find(index);
if(iter != end())
{
    int a = intMap[index];//int a = iter;
}

1.4避免不必要的复杂结构转换

这里说的转换也包括类型转换,为了保证性能尽量少转换,不使用dynamic_cast。

例如在我们的项目中,接口都是用pb定义的,我们内部又维护了一套结构。每次从网格拿到数据都需要进行转换,里面包含了内存的申请拷贝释放,序列化与反序列化,这部分耗时占据了总耗时的三分之一,但是否有必要尚待斟酌。

后续项目中发现此部分的序列化和反序列化为性能瓶颈,于是在内存中存储了一份完整数据,避免了与blob的相互转换,同时为了减少内存占用和加快查询速度,采用哈希方式存储键值。

int32VariantMap转换为->

[header][fieldInfo][fieldInfo][fieldInfo][特定类型值][特定类型值][特定类型值]

struct header
{
uint16 tag;
uint32 datalength;
uint32 fieldnum;
}
struct fieldInfo
{
uint32 offset;
}

首先计算数据总长度datalength

然后一个字段一个字段 记录偏移,填值。

1.5避免无用的复杂的产品定义导致的复杂代码

例如我们产品中规定了返回了一个单独的错误码和一个批量错误码,批量操作在全部成功时需要清空批量错误码,在部分成功时要返回部分成功错误码及各自独立的批量错误码。在全部失败时,如果失败原因相同,返回单独的错误码,清空批量错误码,原因不同时返回第一个错误码,保留所有批量错误码。这种复杂的逻辑十分没必要,目前我的做法是全部成功返回成功,批量错误码为空,只要有失败就返回失败和所有批量错误码。

int ret = 0;
vector<int> errorcodes;
//...
for(auro iter:errorcodes)
{
if(...)
{}...
}

errorcodes.clear();
//...

产品定义规定输入参数不能出现相同字段,后端为了校验字段名是否重复,遍历了批量接口的所有字段进行比较,这种校验也完全没必要,可以在前端避免这种错误输入,即使可以输入重复字段覆盖也不会有什么影响。等。

2.减少内存申请释放

2.1数组提前分配内存

如果能确定数组的大小,定义临时变量时提前分配内存,效率提升30%以上,否则vector的自动申请内存扩容会浪费性能。

vector<string> testStrVec;
testStrVec.reserve(fields.size());

2.2内存申请能用栈上的别用堆上的

2.3少建临时变量,利用指针、引用、索引代替拷贝

在优化前,仅临时变量的析构耗时占用了总耗时的六分之一,所以应保证输入数据不再进行多次拷贝。可以使用指针、引用、索引指向需要使用的数据,避免内存的申请释放拷贝赋值。

2.4代码业务逻辑调整

在我们项目中有个模块是处理写库业务的,有个模块是处理订阅回调业务的。写库业务完成后把修改的数据调用异步接口扔给订阅模块,这样各个模块独立。在我们性能测试中发现,为了拼接给订阅模块的数据,耗时比较久,于是在订阅模块加了一个接口给写库模块用以判断是否需要将数据发给订阅模块,需要的话再拼接数据调用异步接口。这样避免了临时变量的创建和处理。

3.合理利用异步多线程(进程)和锁

3.1保证请求的处理分发均匀

在使用k8s集群部署服务进行测试时,请求的分发十分不均匀,发现是使用了iptables模式导致的,后面改成ipvs后请求均匀,提升了集群性能。

在服务内部一个模块使用了多个线程,为了保证时序性,同一个对象ID的请求都由一个线程处理,使用了利用ID对线程数量取余的方法得到线程序号,利用相应线程进行处理。后续发现配置了8个线程始终只有4个线程工作,原因是由于输入的ID只有奇数没有偶数,这种方法亲和性差。后续请求来的时候直接按顺序依次获取线程进行处理,同时建立ID与线程的映射关系map,如果再次收到请求从映射map中查找相应线程,如果没有找到直接使用下一个线程进行处理,避免了上述问题。也可以考虑使用无锁队列解决此问题。

actorPtr getActor(ID)
{
   actorIndex = ID % actorNum;//亲和性差
}

actorPtr getActor(ID)
{
    iter = map.find(ID)
    if(iter != end())
    {
        actorIndex = iter.second;
    }
    else
    actorIndex;
}

circleQueque<actorPtr> actor;

3.2使用异步接口

拆分业务,把不重要或者不需要在本次结果中返回的业务使用其他线程异步单独处理。

3.3合理使用多线程

在性能不足时我们使用了多线程,但经常有个问题就是时序性要保证,先到的请求要先处理。

在Kafka消费时我们遇到了这个问题,首先增大了Kafka的分区数量,九个分区三个节点,一个节点上三个线程去消费,分别对应不同的分区,这样保证了每个分区的时序性。后来发现性能还是不够,又增加了线程,一个分区三个线程去消费。为了保证分区消费的时序性,我建立了一个vector线程flag数组,同时处理,但是第一个处理完就将第一个置为false第二个置为true,第二个处理完就将第三个置为true,为true时才能进入下一个函数,这样提升了效率,保证了时序性。

vector<bool> actorFlags;

上面讲到的根据请求ID去分配线程也是合理使用多线程的方法。

3.4使用锁

并发情况下,性能的瓶颈往往在共享资源上,锁就是一个关键点。

小范围代码使用spinlock,大范围使用scopelock,明确的读写操作使用读锁和写锁。

避免在循环中使用锁,更要避免死锁的情况。

3.4使用线程池和分配器

不同线程使用不同内存池,优化性能。

4.算法及数据结构

由于我们做的是持久化库,需要范围查询,所以选择了B+树的数据结构,层数少,减少与磁盘IO次数性能高。

内存数据库建议用跳表,相对红黑树实现简单且效率不低。

字符串比较使用KMP算法。

5.使用性能高的三方库接口

我们的项目中用到了Kafka,redis,ignite,zookeeper等三方库。

例如初期在使用redis和ignite的接口时我们调用的是单个接口,后续改成了批量接口,性能提升三到五倍,随着数据量的增大还会继续提升。

例如在使用ignite删除数据时,我们调用的是clear接口,后续改成了remove接口,性能有优化,clear接口是真正从硬盘删除数据,而remove是给删除的数据打标记,故效率高且支持事务。

例如使用Kafka消费数据时发现消费速度较慢,首先我们将随机消费改成了指定分区消费,把c++接口换成了Kafka的c接口,把普通消费consume接口又换成了效率最高的consume_callback回调接口,把订阅接口subscribe改成了rd_kafka_assign后,性能满足了要求。(另外Kafka消费性能优化还要注意消费者的性能一定不能成为Kafka消费的瓶颈,处理要够快)

6.减少与三方库持久化交互,缩减数据量

这点在与redis和ignite库交互时性能表现最为明显,尤其是写持久化内容。

在业务逻辑上想办法减少与三方库的交互,例如下面说的能用一张表没必要用两张表。

要合理建表,同一份数据能用一张表不用两张表,不然需要交互两次。能用八个字段,不用十六个字段。能用数字或者转码ID做字段名,不用字符串做字段名(更不用说长字符串)。修改了一个字段就在底库改一个字段而不是重新赋值整个结构,整条数据。

缩减数据量我们主要使用了两个方法,使用flag用位与标记哪些数据被修改了需要存储,使用序列化压缩数据。要尽量避免无用的字段值的写入,尤其是string,我们早期把成员的名称写入了数据库,其实成员是有序的完全可以利用memberflag标记,去除成员名后数据量缩减了三分之一。

ignite既有持久化表,也有内存表,我们为了优化性能某些数据使用了ignite的内存表。

7.使用新语法新技术

例如C++11引入了emplace_back,比push back效率提升了20%,本质上也是少了内存的拷贝。

例如使用std::move使用右值避免构造和拷贝,使用swap等。

例如可以使用携程等。

8.电脑硬件分析及维护

分析时要注重观察CPU和内存的占用。

例如在arm测试时,性能极差,后发现内存只剩下了5m,导致每次申请内存耗时极长,缩减服务占用内存后正常。

使用多线程后,服务占用的CPU始终很低,没有提升,需要检查多线程是否起作用了,负载是否均衡,留意锁的使用。

单接口性能测试发现差别很大,可以看看CPU频率,CPU频率决定了单线程的处理性能,频率越高处理越快。

openstack的超分会严重影响性能。

持久化内容的写入最好使用固态。

CPU占用比较高,锁的使用也正常,但是同样的接口不同负载下性能差异极大,可以观察是不是CPU温度过高了。

以上就是c++数据库项目性能优化的总结,有好的建议也可以留言提给我,谢谢。

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值