寒假第三轮作业(2) 数据包分类程序最终优化,附时间测试
这个作业属于哪个课程 | <福州大学2022面向对象程序设计> |
---|---|
这个作业要求在哪里 | <2022面向对象程序设计寒假作业3> |
这个作业的目标 | 优化第二轮作业程序算法及规范输入输出 |
作业正文 | 如下 |
其他参考文献 | <常见容器的时间复杂度><C++整数转字符串的几种办法> |
前言
上一次写作业还是在上一次,但是最近身体不太舒服,于是就没有碰这个作业了,本来是打算分三篇写的,但是时间确实很紧没有办法再在这个作业上多花时间了,这篇博客就会直接对程序作出最终优化。优化的思路是我第二轮作业时候想的,这轮做出实现,我能力有限实在看不太懂那些论文,而且也没有什么时间。
我的github仓库链接在这里:<MatchHelper2.0>
因为VS生成的工程有点臃肿,而这又是一个很小的任务,所以我只上传了cpp和头文件,以及手动编译生成的main.exe。
程序优化
数据规模
数据规模有所升级,一个数据包大概40w数据,六个包是2e6的级别。这样的规模理论上复杂度起码应该收缩在O(\(nlog_n\)).
我的优化思路
一种不正经的优化,也就是我之前说的记忆化的办法,对于已经查找过答案的直接O(1)给出答案。那么主要的问题在于判断“是否已经查找过”,这个过程理论上不能有太多的开销。但是这种仅仅在重复的数据较多的时候才会有作用,如果输入都没有重复或者重复很少就没有什么作用了。这道题目最正规的做法应该是把规则集和数据集都变成二进制比特串,在此基础上利用Trie做优化。
具体步骤
- 在从文件读入数据集的时候,对于每条数据建立五元组信息拼接而成的字符串索引,建立索引采用C++11出现的to_string函数转化整数为字符串。
- 外层循环遍历数据集,如果该数据已经算过答案就给出答案退出匹配,没算过则遍历规则集,计算答案并记录。
复杂度分析
假设数据集规模为M,规则集规模为N, 其中不重复的规模为R。数据集平均五元组数字总位数为n。规则集平均每条规则两条地址串总长度为L
空间复杂度
空间复杂度反而是提高了,hhh。因为多付出了容器存储索引和答案的空间嘛。但是实际应用这种肯定是要进数据库存储的,字符串索引和自增主键索引理论上也差不了太多吧(可能),算是牺牲空间换取时间了.从原来的O(\(MN\))变成了O(\(MNR\)),最坏的情况(无重复情况)变为O(\(M^2N\)).当然如果只是为了计算答案,实际上完全可以不在vector里存储数据集,直接边读边匹配,前后都可以去掉一个M因子。
时间复杂度
- 建立索引:老实说我没有查到to_string的时间复杂度。暂且按照手动实现的朴素做法计算的话,数字转成字符串的时间复杂度应该是O(\(Mn\))。就位数而言,ip地址至多十位,最长的位数不过10+10+5+5+3=33位。
- 规则集ip地址换算成十进制数字: 全部规则集要执行的运算次数为O(\(NL\))(小数字忽略快速幂算法开销).
- 匹配阶段,这个上篇说过,O(MN)变为O(MR).因为unordered_map是基于哈希的,非最坏情况查询复杂度为O(1)。
哈哈,写完突然发现这优化的啥玩意,隔这反向优化….
测试时间
原先的匹配阶段代码如下.
#include<vector>
#include"Data.h"
#include"Rule.h"
#include"match_util.h"
using std::vector;
bool check(Data& data, Rule& rule)
{
if (data.origin_ip() < rule.origin_ip_beg() ||
data.origin_ip() > rule.origin_ip_end())
return false;
if (data.receiver_ip() < rule.receiver_ip_beg() ||
data.receiver_ip() > rule.receiver_ip_end())
return false;
if (data.origin_port() < rule.origin_port_beg()
|| data.origin_port() > rule.origin_port_end())
return false;
if (data.receiver_port() < rule.receiver_port_beg()
|| data.receiver_port() > rule.receiver_port_end())
return false;
if (data.tcp() != rule.tcp() && rule.tcp() <= 255)
return false;
return true;
}
vector<int32_t> DoMatch(vector<Data> &datalist, vector<Rule> &rulelist)
{
vector<int32_t> ans;
for (Data& data:datalist)
{
bool tag = true; //检查是否有匹配到的
for (size_t i = 0; i < rulelist.size(); ++i)
{
Rule rule = rulelist.at(i);
if (!check(data, rule))
continue;
else
{
int idx = static_cast<int>(i);
ans.push_back(idx);
tag = false;
break;
}
}
if (tag)
ans.push_back(-1);
}
return ans;
}
测试时间如下:
一个包24s,那么6个包需要约148s,两分钟多,可以说是很折磨了…….
改进之后匹配阶段代码如下,其中match_map在match_util.h头文件声明,main.cpp中定义。
#include<vector>
#include<unordered_map>
#include"Data.h"
#include"Rule.h"
#include"match_util.h"
using std::vector;
using std::unordered_map;
bool check(Data& data, Rule& rule)
{
if (data.origin_ip() < rule.origin_ip_beg() ||
data.origin_ip() > rule.origin_ip_end())
return false;
if (data.receiver_ip() < rule.receiver_ip_beg() ||
data.receiver_ip() > rule.receiver_ip_end())
return false;
if (data.origin_port() < rule.origin_port_beg()
|| data.origin_port() > rule.origin_port_end())
return false;
if (data.receiver_port() < rule.receiver_port_beg()
|| data.receiver_port() > rule.receiver_port_end())
return false;
if (data.tcp() != rule.tcp() && rule.tcp() <= 255)
return false;
return true;
}
vector<int64_t> DoMatch(vector<Data>& datalist, vector<Rule>& rulelist)
{
vector<int64_t> ans;
for (Data& data : datalist)
{
if (match_map.find(data.id()) != match_map.end()) //如果哈希表中已经有算好答案
{
ans.push_back(match_map[data.id()]); //给出答案,结束当前数据匹配
continue;
}
// 没有存在的答案,下面遍历规则集寻找答案
bool tag = true; //标记是否有存在的匹配位置
for (size_t i = 0; i < rulelist.size(); ++i)
{
Rule rule = rulelist.at(i);
if (!check(data, rule))
continue;
else
{
int32_t idx = static_cast<int>(i);
ans.push_back(idx);
match_map[data.id()] = idx; // 记录答案
tag = false;
break;
}
}
if (tag)
{
ans.push_back(-1);
match_map[data.id()] = -1; //记录答案
}
}
return ans;
}
那么它的运行时间是:
还是跑了好久,不过幸好不是反向优化……如果这样计算,六个包大概是111s,优化效率大概为25%,其实有这个结果还挺意外的,比我想象的要多。但是我想到一个事情,这样的记忆化只跑一个包直接时间乘以6应该不太靠谱,因为理论上当数据量越多,出现的重复次数会越多,那么优化效率应该会更明显,所以直接跑三个包看看用时(不跑六个是因为规则集不同),同时为了减少误差,所以跑三次取平均值,因为一次跑多个包要在命令行中进行,所以为了公平起见也把原来未做优化的重新跑三次:
改进后的三次时间为:
好吧,按照这样计算其实差不多,乘以2后平均完成六个包的时间为114s,比跑单个包时间乘以6还慢了……可能是IDE的运行会做一些优化吧?
下面是不做优化代码跑的时间:
确实也会比原来直接乘6的更久。不做优化平均时间约为154.5s。那么更加准确的优化效率计算为26%,其实也差不多啦。
Windows下手动编译成main.exe
VS会自动把程序编译成项目的名称打头的exe文件,跟作业要求的main.exe不符合,所以要手动编译一下(当然如果直接重命名exe文件也可以).第一篇博客里说了要完整地达成格式要在Linux环境下,但是考虑到平台兼容性的问题,所以还是在Windows下来做了,Linux类似。
这里有一个很有意思的地方,不知道为什么,手动编译出来的exe比IDE帮忙自动生成的exe运行速度要快许多?如果用手动编译的exe运行,优化效率竟可以达到惊人的53%.
结语
寒假的作业就到这里为止了,感觉还是自己能力和时间都有限,很多地方都做的不够好,欢迎多批评指正呀.