如果您将每个时间戳与前一个时间戳之间的间隔以微秒表示(即整数),则示例文件中每个位深度的值分布为:
所以 52.285% 的值是 0 或 1,只有少数其他值低于 64(2~6 位),27.59% 的值是 7~12 位,分布相当均匀2.1% 左右,最高 20 位,20 位以上只有 3%,最多 25 位。
查看数据,也很明显,有许多多达 6 个连续零的序列。
这些观察让我想到了使用每个值的可变位大小,如下所示:
00 0xxxxx 0(xxxxx 是连续零的个数)
00 1xxxxx 1(xxxxx为连续1个数)
01 xxxxxx xxxxxxxx 2-14 位值
10 xxxxxx xxxxxxxx xxxxxxxx 15-22 位值
11 xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 23-30 位值
快速测试表明,这导致每个时间戳的压缩率为 13.78 位,这与您的目标 10 位并不完全一致,但对于一个简单的方案来说,这并不是一个糟糕的开始。
再分析样本数据后,我观察到有很多连续的0和1的短序列,比如0 1 0,所以我用这个替换了1字节的方案:
00xxxxxx 00 = 标识一个字节值
xxxxxx = 序列表中的索引
序列表:
索引~seq 索引~seq 索引~seq 索引~seq 索引~seq 索引~seq
0 0 2 00 6 000 14 0000 30 00000 62 000000
1 1 3 01 7 001 15 0001 31 00001 63 000001
4 10 8 010 16 0010 32 00010
5 11 …………
11 101 27 1101 59 11101
12 110 28 1110 60 11110
13 111 29 1111 61 11111
对于具有 451,210 个时间戳的示例文件,这会将编码文件大小降低到 676,418 字节,即每个时间戳 11.99 位。
对上述方法的测试表明,在较大间隔之间有 98,578 个单零和 31,271 个单零。所以我尝试使用每个较大间隔的 1 位来存储它是否后跟零,这将编码大小减少到 592,315 字节。当我使用 2 位来存储较大的间隔之后是 0、1 还是 00(最常见的序列)时,编码大小减少到 564,034 字节,即每个时间戳 10.0004 位。
然后我更改为存储单个 0 和 1 并使用以下大间隔而不是前一个间隔(纯粹是出于代码简单的原因),并发现这导致文件大小为 563.884 字节,或 每个时间戳 9.997722 位!
所以完整的方法是:
存储第一个时间戳(8 个字节),然后将间隔存储为:
00 iiiiii 最多 5 个(或 6 个)零或一的序列
01 XXxxxx xxxxxxxx 2-12 位值 (2 ~ 4,095)
10 XXxxxx xxxxxxxx xxxxxxxx 13-20 位值 (4,096 ~ 1,048,575)
11 XXxxxx xxxxxxxx xxxxxxxx xxxxxxxx 21-28 位值 (1,048,576 ~ 268,435,455)
iiiiii = 序列表中的索引(见上文)
XX = 前面有一个零(如果 XX=1)、一个一(如果 XX=2)或两个零(如果 XX=3)
xxx... = 12、20 或 28 位值
编码器示例:
#include <stdint.h>
#include <iostream>
#include <fstream>
using namespace std;
void write_timestamp(ofstream& ofile, uint64_t timestamp) { // big-endian
uint8_t bytes[8];
for (int i = 7; i >= 0; i--, timestamp >>= 8) bytes[i] = timestamp;
ofile.write((char*) bytes, 8);
}
int main() {
ifstream ifile ("timestamps.txt");
if (! ifile.is_open()) return 1;
ofstream ofile ("output.bin", ios::trunc | ios::binary);
if (! ofile.is_open()) return 2;
long double seconds;
uint64_t timestamp;
if (ifile >> seconds) {
timestamp = seconds * 1000000;
write_timestamp(ofile, timestamp);
}
while (! ifile.eof()) {
uint8_t bytesize = 0, len = 0, seq = 0, bytes[4];
uint32_t interval;
while (bytesize == 0 && ifile >> seconds) {
interval = seconds * 1000000 - timestamp;
timestamp += interval;
if (interval < 2) {
seq <<= 1; seq |= interval;
if (++len == 5 && seq > 0 || len == 6) bytesize = 1;
} else {
while (interval >> ++bytesize * 8 + 4);
for (uint8_t i = 0; i <= bytesize; i++) {
bytes[i] = interval >> (bytesize - i) * 8;
}
bytes[0] |= (bytesize++ << 6);
}
}
if (len) {
if (bytesize > 1 && (len == 1 || len == 2 && seq == 0)) {
bytes[0] |= (2 * len + seq - 1) << 4;
} else {
seq += (1 << len) - 2;
ofile.write((char*) &seq, 1);
}
}
if (bytesize > 1) ofile.write((char*) bytes, bytesize);
}
ifile.close();
ofile.close();
return 0;
}
解码器示例:
#include <stdint.h>
#include <iostream>
#include <fstream>
using namespace std;
uint64_t read_timestamp(ifstream& ifile) { // big-endian
uint64_t timestamp = 0;
uint8_t byte;
for (uint8_t i = 0; i < 8; i++) {
ifile.read((char*) &byte, 1);
if (ifile.fail()) return 0;
timestamp <<= 8; timestamp |= byte;
}
return timestamp;
}
uint8_t read_interval(ifstream& ifile, uint8_t *bytes) {
uint8_t bytesize = 1;
ifile.read((char*) bytes, 1);
if (ifile.fail()) return 0;
bytesize += bytes[0] >> 6;
for (uint8_t i = 1; i < bytesize; i++) {
ifile.read((char*) bytes + i, 1);
if (ifile.fail()) return 0;
}
return bytesize;
}
void write_seconds(ofstream& ofile, uint64_t timestamp) {
long double seconds = (long double) timestamp / 1000000;
ofile << seconds << "\n";
}
uint8_t write_sequence(ofstream& ofile, uint8_t seq, uint64_t timestamp) {
uint8_t interval = 0, len = 1, offset = 1;
while (seq >= (offset <<= 1)) {
seq -= offset;
++len;
}
while (len--) {
interval += (seq >> len) & 1;
write_seconds(ofile, timestamp + interval);
}
return interval;
}
int main() {
ifstream ifile ("timestamps.bin", ios::binary);
if (! ifile.is_open()) return 1;
ofstream ofile ("output.txt", ios::trunc);
if (! ofile.is_open()) return 2;
ofile.precision(6); ofile << std::fixed;
uint64_t timestamp = read_timestamp(ifile);
if (timestamp) write_seconds(ofile, timestamp);
while (! ifile.eof()) {
uint8_t bytes[4], seq = 0, bytesize = read_interval(ifile, bytes);
uint32_t interval;
if (bytesize == 1) {
timestamp += write_sequence(ofile, bytes[0], timestamp);
}
else if (bytesize > 1) {
seq = (bytes[0] >> 4) & 3;
if (seq) timestamp += write_sequence(ofile, seq - 1, timestamp);
interval = bytes[0] & 15;
for (uint8_t i = 1; i < bytesize; i++) {
interval <<= 8; interval += bytes[i];
}
timestamp += interval;
write_seconds(ofile, timestamp);
}
}
ifile.close();
ofile.close();
return 0;
}
由于我正在使用的 MinGW/gcc 4.8.1 编译器中的 long double output bug,我不得不使用此解决方法:(对于其他编译器,这不应该是必需的)
void write_seconds(ofstream& ofile, uint64_t timestamp) {
long double seconds = (long double) timestamp / 1000000;
ofile << "1" << (double) (seconds - 1000000000) << "\n";
}
未来读者注意:此方法基于对示例数据文件的分析;如果您的数据不同,它不会提供相同的压缩率。