0x10 前言
SCPI是一个对人或者说用户十分友好的语言,采用了人性化的抽象与对于用户很友善的组成方式。
但是对于某些机器的设计就会很难受,而且当前的机器会在日后的不停更新导致当前的程序越来越呈现一种指数级别的裂变。这种裂变是冗余的、灾难性的,因此需要一个简单的压缩方式或者说压缩算法进行数据的去冗余,提炼出干练的短句。
阅读本文之前,您需要掌握的技能有:
技能名称 | 技能熟练度 | 技能教程链接 |
---|---|---|
C语言 | 熟悉 | 暂无 |
数据结构 | 熟悉 | 暂无 |
0x20 简单介绍
这是一个简单的SCPI指令:
A:B:C:D:E
可以看到一个简单的层级结构,A为第一级,一般代表一个大类。B为一个中间点,是一个主要的选项,C、D、E为当前的选择支类,他们可能并不是很重要,但是可能很多指令存在。
这些指令也有可能是可隐藏的,比如:
[A:]B:C[:D]:E
这种指令下,B:C:E
、A:B:C:D:E
、A:B:C:E
、B:C:D:E
都是对的,只要保证[]以外的全部都有即可。
而一般的指令都会有两个格式,写入与获取。
写入就是很简单的加参数,读取更简单,一般直接在指令后加上?
即可。
所以一个标准的指令一般有两类:
- 加参数的写入指令
- 加?的读取指令,有的指令会加参数读取。
0x30 基本思想
可以看到,这边的指令具有明显的冗余位置:
当前的指令必定会有读取和写入,而他们大概率是长得一样的,这种压缩就取决于当前是否需要了,这里仅讲述个人认为较为简单的压缩思想:去重查找。
去重的核心就是找到重复的数据。然后把当前的数据使用特殊的方式映射到表中,随后可以在需要的地方进行还原。
这里因为单板的计算能力有限,所以笔者使用了较简单的查表法进行还原,但是采样的方式较为麻烦。
0x40 查重
当前的数据存在及其严重的数据冗余,这里使用很简单的比对方式:主要步骤为数据截取、数据比对、数据连接。
笔者这里选择的是双向链表的形式,一方面是锻炼自己的数据操作能力与数据架构能力,一方面也是根据当前不定长度与不定个数的数据结构的较为简单的生成。
这里的数据仅仅需要可用的数据,也就是忽略了一切标点符号的连续字符。每段字符串都是一节可用的数据。并且为了单板的拼接方便,这里还使用了数据ID的标记针对当前的字符串做出专有的定义。这样在最后只需要生成以ID从小到大排列的顺序生成一个数组即可,这样就可以直接根据ID拼接数组形成最后的指令。
根据上面的要求就可以得到一个明确的基本数据结构:
typedef struct
{
unsigned int ID;
char* String;
}
这个数据还缺了上下的链表,于是需要这样:
typedef struct S_STRING
{
struct S_STRING* prev;
struct S_STRING* next;
unsigned int ID;
char* String;
}S_STRING_NODE;
这个就是一个基本的双向链表,传输的指针也不需要必须要是特定的字符串,只需要一个数据的指针即可。所以还需要一个当前的数据长度的指示位。
typedef struct S_STRING
{
struct S_STRING* prev;
struct S_STRING* next;
unsigned int ID;
char* String;
unsigned int length;
}S_STRING_NODE;
这就是一个节点。
有了一个节点,就可以向里面填充数据。数据的结构虽然是严格意义上的双向,但是还是需要一个头作为笔者编程时调试的锚点。也好判断当前的数据结构是否正确。于是笔者定义了一个作为锚点的根数据,随后的数据全都是衔尾蛇一般的双向数据链表。
假设输入的数据是这样子的
SYSTem:REMote
SYSTem:RWLock
SYSTem:GreenDream
这里数据最后的输出结果就是:
这个样子。(这个图已经是笔者能够找到的最好用的图片了)
这样就会将原本46字符(算上占位符)的字符减小变成约32个字符。压缩了大约70%。
随着数据越加越多,对于原本的数据压缩甚至可以做到10%(笔者试验了某功能较多的机器,各类指令约有800条,原指令约为22k,压缩完成后的数据约为2.2~2.5K)
就其根本是因为这种层级指令组合本身就不适合使用简单粗暴的方式进行数据检索。更应该是使用类似字典查表法进行依次比对。
0x50 二次映射
前文为了降低阅读量(笔者最近太忙以至于并没时间写关于SCPI的构成),就省略了SCPI中,指令的基本构成法则。
下面介绍下基本的构成规则:
- 指令主要分为必须要的指令、可省略的指令。
- 指令方向有设置与读取。
- 指令层级以
:
结尾。
于是出现了四种构成指令层级的方式:?
、[:
、:]
、:
。
而具体的指令格式则为当前需要的要求组成,就像上文那样的组成即可。
现在已经有了一个完整的数据去重的链表。下面需要做的就是把这个链表做映射,并将映射的数据导出。
导出的前提是有一个结构化的表格类似的数据结构。笔者自己做了一个类似于Hash Table的数据结构,总体的格式类似于思维导图的形式。
可以看出来,总体是一个{形式的一个数据结构。因此也就出现了两个必须要解决的问题:数据格式的定义,数据格式的拼接。
可以看到,这两个问题属于一个问题:如何优雅的利用数据的解析形式,完成对于整个映射的构造。
上图可以看到,整个数据结构有三类节点:根节点、指令头部节点、跟随的后续指令节点。
根节点仅具备区域边界的指向性,也就是说只负责指定当前数据结构的区间的第一个数据与最后一个数据。从而使得整个指令头部形成一个衔尾蛇的循环结构。而所有的指令头部节点全部都会指向根节点,以保证当前节点可以迅速回到根,从而进入其他的节点。从图片上可以看出,头部节点具有四个方向,而整体的数据构成了两个双向链表与一个循环结构。也就是说当前的节点需要四个指针向量。
typedef struct S_1
{
struct S_1* prev;
struct S_1* next;
struct S_1* supper;
struct S_1* child;
}S_S;
这就是两个指针表示前后,两个指针表示上下。随后就需要ID了。
typedef struct S_1
{
struct S_1* prev;
struct S_1* next;
struct S_1* supper;
struct S_1* child;
unsigned int Id;
}S_S;
这个时候还少了最关键的指令的层级表示。也就是对于字符串的?
、[:
、:]
、:
。
因为只有四个状态,正好就是两个比特位。这样就可以得到一个数据了。
考虑到北京大学图书馆-牛津词典检索的记录单词的个数,一般来说20比特的数据就可以直接得到几乎全部的英语单词量了。但是一般人都不会用这么多的单词构架指令数。所以当前的数据结构暂时可以只使用32比特的数据定义,其中前拆出2位数据作为层级表示的定义,剩下的作为检索的ID使用。
typedef enum
{
NORMAL,
LEFT,
RIGHT,
RETURN
}E_S;
typedef union
{
E_S special:2;
unsigned id:14;
}U_S
typedef struct S_1
{
struct S_1* prev;
struct S_1* next;
struct S_1* supper;
struct S_1* child;
U_S Point;
}S_S;
这样就形成了一个基本的数据结构。
笔者的能力有限,也只有结合当前的结构,在某些情况下选择性的更新某些数据指针。例如
- 在根节点中,上下分别指向根节点和空闲。而前后分别指向第一个和最后一个节点。
- 头部节点中,上下则指向根节点与跟随节点,左右分别指向相对于现在的先后添加的头部节点。
- 而跟随节点仅包含前后结构,最前面的跟随节点指向头部与随后的跟随节点
- 后面的跟随节点则前后都是跟随节点。
总体上的组合就是这样组合,虽然实际上拥有四个层级,但是数学告诉人们,只需要三道横线就可以画出四个区域。
0x60 组合对比
笔者对当前的压缩方式计算了一下,对于选择的样本:860个指令,一个真实的机器指令集,约22000字符。
压缩前字符22000,约为20k。
压缩后的的单词约为270个,仅占用有效9位有效ID字节。而所有的单词占用的字符约为2000,也就是2k。
经过映射的数据节点约为3800个,每个数据节点的有效数据约为2byte,也就是约为7kb。
随后需要合成的临时数据空间暂时定义为300b,这个区间足可以定义绝大多数的指令。而且仅占用RAM不会占用ROM。可以随时动态申请。
也就是大约需要10KB的实际占用空间。也就压缩了约50%的实际空间。
当然,如果能够再利用一个字节将读取与写入的指令绑定会得到更大的压缩效率(保守估计约为80%左右),但是对于解析器的要求很大,必须要在解析器构建之前就进行一定的前置适配。
以上。
更多
本文首发自 记: 对于SCPI指令以及相同类型指令解析器的指令压缩方式,更多文章可进入我的博客详查。