背景
GitHub源码地址:https://github.com/niXman/yas
个人注释gitee地址:https://gitee.com/AriesSun/yas_comment
在yas介绍一节的示例代码中,大家应该还记得yas::compacted,这个标识的目的就是在进行序列化时,将数据进行压缩。然而,着并不意味着yas支持对所有的数据进行压缩,实际上,yas只支持对整型数据的压缩,比如,int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t等。下面就对其原理进行详细介绍。
计算stroage_size
针对整型数据,根据数值大小计算合适的storage size。
template<typename T>
static constexpr std::uint8_t storage_size(const T &v, __YAS_ENABLE_IF_IS_16BIT(T)) {
// 对于2字节整数
// 如果v < 256, 那么,使用1字节存储,
// 否则,使用2字节存储
return __YAS_SCAST(std::uint8_t, (v < (1u<<8 )) ? 1u : 2u);
}
template<typename T>
static constexpr std::uint8_t storage_size(const T &v, __YAS_ENABLE_IF_IS_32BIT(T)) {
return __YAS_SCAST(std::uint8_t,
// 二分法
(v < (1u<<16) )
? ((v < (1u<<8 )) ? 1u : 2u)
: ((v < (1u<<24)) ? 3u : 4u)
);
}
template<typename T>
static constexpr std::uint8_t storage_size(const T &v, __YAS_ENABLE_IF_IS_64BIT(T)) {
return __YAS_SCAST(std::uint8_t,
// 二分法
(v < (1ull<<32))
? (v < (1u<<16) )
? ((v < (1u<<8 )) ? 1u : 2u)
: ((v < (1u<<24)) ? 3u : 4u)
: (v < (1ull<<48) )
? ((v < (1ull<<40)) ? 5u : 6u)
: ((v < (1ull<<56)) ? 7u : 8u)
);
}
上面的三个函数分别针对16bit、32bit、64bit的整数进行处理,试图得到最小保存整数的字节数。
- 16bit的整数,可以有1字节和2字节两种选择
- 32bit的整数,可以有1字节、2字节、3字节和4字节4种选择
- 64bit的整数,可以有1字节、2字节、3字节、4字节、5字节、6字节、7字节、8字节等8种选择
如果你字节阅读代码,会发现作者使用了二分法,便于快速得到最小的保存字节数。
有符号整数的压缩存储
// for signed
template<typename T>
void write(const T &v, __YAS_ENABLE_IF_IS_SIGNED_INTEGER(T)) {
__YAS_CONSTEXPR_IF ( F & yas::compacted ) {
// 如果v为正数
if ( __YAS_LIKELY(v >= 0) ) {// 正整数处理
// 转为对应的unsigned类型
typename std::make_unsigned<T>::type uv = v;
// v >= 64,为什么要这么处理,因为第7、8bit有特殊用途
// 第8bit用来表示符号,第7bit表示是否1字节存储
if ( uv >= (1u<<6) ) {
// 计算存储size
const std::uint8_t ns = storage_size(uv);
// 写入压缩后整数字节数,字节数使用1字节进行存储,因为为正数,所以,最高bit为0
write(__YAS_SCAST(std::uint8_t, ns | __YAS_SCAST(std::uint8_t, 0u<<7)));
// 写入压缩后的数据
__YAS_THROW_WRITE_ERROR(ns != os.write(&uv, ns));
} else {
// one byte,第8bit为0, 第7bit为1
uv |= __YAS_SCAST(std::uint8_t, (1u<<6|0u<<7));
__YAS_THROW_WRITE_ERROR(1 != os.write(&uv, 1));
}
} else { // 负整数处理
// 转为对应的unsigned类型
typename std::make_unsigned<T>::type uv = std::abs(v);
// v >= 64
if ( uv >= (1u<<6) ) {
// 计算存储size
const std::uint8_t ns = storage_size(uv);
// 写入保存的字节数,负数,第8bit为1
write(__YAS_SCAST(std::uint8_t, ns | __YAS_SCAST(std::uint8_t, 1u<<7)));
// 写入压缩后数据
__YAS_THROW_WRITE_ERROR(ns != os.write(&uv, ns));
} else {
// one byte,第8bit和第7bit都为1,表述数据为负数,且为1字节存储
uv |= __YAS_SCAST(std::uint8_t, (1u<<6|1u<<7));
// 写入数据
__YAS_THROW_WRITE_ERROR(1 != os.write(&uv, 1));
}
}
} else {
__YAS_CONSTEXPR_IF ( __YAS_BSWAP_NEEDED(F) ) {
T t = endian_converter::bswap(v);
__YAS_THROW_WRITE_ERROR(sizeof(t) != os.write(&t, sizeof(t)));
} else {
__YAS_THROW_WRITE_ERROR(sizeof(v) != os.write(&v, sizeof(v)));
}
}
}
代码中的注视,已经非常详尽,就不再进行过多的赘述了。核心就是:
- 在数据长度的字节中,保存符号和是否1字节存储
- 负数先转为整数
- 将数据保存为压缩后的数据长度
压缩的有符号整数的读取
// for signed
template<typename T>
void read(T &v, __YAS_ENABLE_IF_IS_SIGNED_INTEGER(T)) {
__YAS_CONSTEXPR_IF ( F & yas::compacted ) {
// 读取第1个字节
std::uint8_t ns = __YAS_SCAST(std::uint8_t, is.getch());
// 是否为负数
const bool neg = __YAS_SCAST(bool, (ns >> 7) & 1u);
// 是否数1字节存储
const bool onebyte = __YAS_SCAST(bool, (ns >> 6) & 1u);
// 计算字节数
ns &= ~((1u << 7) | (1u << 6));
// 如果不是1字节存储
if ( __YAS_LIKELY(!onebyte) ) {
typename std::make_unsigned<T>::type av = 0;
__YAS_THROW_READ_STORAGE_SIZE_ERROR(sizeof(av) < ns);
// 读取压缩后的数据
__YAS_THROW_READ_ERROR(ns != is.read(&av, ns));
// 将数据转为实际的类型,并赋予正负号
v = (__YAS_UNLIKELY(neg) ? -__YAS_SCAST(T, av) : __YAS_SCAST(T, av));
} else { // 如果数1字节存储
v = (__YAS_UNLIKELY(neg) ? -__YAS_SCAST(T, ns) : __YAS_SCAST(T, ns));
}
} else {
__YAS_THROW_READ_ERROR(sizeof(v) != is.read(&v, sizeof(v)));
__YAS_CONSTEXPR_IF ( __YAS_BSWAP_NEEDED(F) ) {
v = endian_converter::bswap(v);
}
}
}
无符号整数的压缩存储
// for unsigned
template<typename T>
void write(const T &v, __YAS_ENABLE_IF_IS_UNSIGNED_INTEGER(T)) {
__YAS_CONSTEXPR_IF ( F & yas::compacted ) {
// 如果 v>=128
if ( v >= (1u<<7) ) {
// 计算存储size
const std::uint8_t ns = storage_size(v);
// 写存储size
write(ns);
// 写数据
__YAS_THROW_WRITE_ERROR(ns != os.write(&v, ns));
} else { // 如果 v<128, 则使用1字节存储
// one byte
T t{v};
// 第8bit置1
t |= __YAS_SCAST(std::uint8_t, 1u<<7);
// 写入数据
__YAS_THROW_WRITE_ERROR(1 != os.write(&t, 1));
}
} else {
__YAS_CONSTEXPR_IF ( __YAS_BSWAP_NEEDED(F) ) {
T t = endian_converter::bswap(v);
__YAS_THROW_WRITE_ERROR(sizeof(t) != os.write(&t, sizeof(t)));
} else {
__YAS_THROW_WRITE_ERROR(sizeof(v) != os.write(&v, sizeof(v)));
}
}
}
压缩无符号整数的读取
// for unsigned
template<typename T>
void read(T &v, __YAS_ENABLE_IF_IS_UNSIGNED_INTEGER(T)) {
__YAS_CONSTEXPR_IF ( F & yas::compacted ) {
// 读取第1个字节
std::uint8_t ns = __YAS_SCAST(std::uint8_t, is.getch());
// 是否是1字节存储
const bool onebyte = __YAS_SCAST(bool, (ns >> 7) & 1u);
// 计算数据长度
ns &= ~(1u << 7);
if ( __YAS_LIKELY(!onebyte) ) { // 如果不是1字节
__YAS_THROW_READ_STORAGE_SIZE_ERROR(sizeof(v) < ns);
// 读取压缩后的数据
__YAS_THROW_READ_ERROR(ns != is.read(&v, ns));
} else { // 如果是1字节
// 当前的ns就是要读取的数据
v = __YAS_SCAST(T, ns);
}
} else {
__YAS_THROW_READ_ERROR(sizeof(v) != is.read(&v, sizeof(v)));
__YAS_CONSTEXPR_IF ( __YAS_BSWAP_NEEDED(F) ) {
v = endian_converter::bswap(v);
}
}
}
总结
纵观yas数据压缩的原理,我认为最大的亮点是:
- 假设整数的长度不会超过64位10进制,实际也应该是这个样子的
- 使用最小可保存数据的字节数进行数据的压缩
- 针对1字节数据的存储,直接将符号、是否为1字节、1字节数据本身都存储在这个1字节数据中
整数的压缩存储,就是上面这个样子了,是不是还挺简单。浮点数因为没有进行压缩,所以这里没有提,大家可以直接阅读源码 yas_comment