以微秒精度压缩 unix 时间戳

Posted

技术标签:

【中文标题】以微秒精度压缩 unix 时间戳【英文标题】:Compressing unix timestamps with microseconds accuracy 【发布时间】:2016-07-14 17:19:34 【问题描述】:

我的文件由一系列具有微秒精度的实时 unix 时间戳组成,即时间戳永远不会减少。所有需要编码/解码的时间戳都来自同一天。文件中的示例条目可能类似于 1364281200.078739,它对应于自纪元以来的 1364281200078739 微秒。数据间隔不均匀且有界。

我需要实现大约 10 位/时间戳的压缩。目前我可以通过计算连续时间戳之间的差异来压缩到平均 31 位/时间戳。我该如何进一步提高?

编辑:

我们将压缩程度计算为(编码文件的大小,以字节为单位)/(时间戳数)*8。我在“。”之前将时间戳分为两部分。并在它之后。整数部分非常恒定,两个整数部分时间戳之间的最大差异为 32,因此我使用 0-8 位对其进行编码。精度部分是非常随机的,所以我忽略了前导位并使用 0-21 位写入文件(最大可以是 999999)。但是我的编码文件的大小为 4007674 字节,因此压缩为 71.05 位/TS。我也写“。”以及两个时间戳之间的空格,以便稍后解码。如何改进编码文件的大小?

这里是部分数据集的链接 - http://pastebin.com/QBs9Bqv0

这里是以微秒为单位的差分时间戳值的链接 - http://pastebin.com/3QJk1NDV 黑白时间戳的最大差异为 - 32594136 微秒。

【问题讨论】:

在当前文件中我有 451210 个条目。我们需要无损压缩。以下是文件中的示例条目 - 1364281200.078739 1364281232.672875 1364281232.788200 1364281232.792756 1364281232.793052 1364281232.795598..... 我认为这是您真正需要在某个地方上传更大的样本数据集的问题之一,如果整个数据太大,可能需要一个小时的数据? 我已在问题中添加了指向数据集的链接。 【参考方案1】:

压缩整数(尤其是排序整数)是一个经过充分研究的研究课题。您可能想使用this project。

【讨论】:

【参考方案2】:

如果您将每个时间戳与前一个时间戳之间的间隔以微秒表示(即整数),则示例文件中每个位深度的值分布为:

所以 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";

未来读者注意:此方法基于对示例数据文件的分析;如果您的数据不同,它不会提供相同的压缩率。

【讨论】:

我们计算压缩度为(编码文件的字节大小)/(时间戳数)*8。我在“。”之前将时间戳分为两部分。并在它之后。整数部分非常恒定,两个整数部分时间戳之间的最大差异为 32,因此我使用 0-8 位对其进行编码。精度部分是非常随机的,所以我忽略了前导位并使用 0-21 位写入文件(最大可以是 999999)。但是我的编码文件的大小为 4007674 字节,因此压缩为 71.05 位/TS。如何改进编码文件的大小? 我也写'.'以及两个时间戳之间的空格,以便稍后解码。如何改进编码文件的大小?还添加了相关信息 我加了一个例子;我希望这能让事情变得更清楚。 在这种情况下解码将如何工作?在某些情况下,例如 1364331598.975142 1364331599.056643 精度小于最后一个精度值。 @learner 我决定尝试为此编写代码,以重新熟悉 C++。我刚刚发布了第一个工作版本。如果您不再需要它,请不要担心,我正在练习它。【参考方案3】:

如果您需要以微秒精度进行无损压缩,请注意 10 位可以让您数到 1024。

如果您的事件时间是随机的,并且您实际上需要您指定的微秒精度,这意味着您的差分时间戳的差异不能超过大约 1 毫秒,而不会超过您的 10 位/事件预算。

根据对数据的快速浏览,您可能无法完全生成 10 位/时间戳。但是,您的差异是正确的第一步,您可以做得比 31 位更好——我会对样本数据集进行统计,并选择反映该分布的二进制前缀编码。

您应该确保您的代码有空间在必要时对较大的间隙进行编码,因此请考虑基于universal code。

【讨论】:

【参考方案4】:

如果不查看数据差异的直方图,就很难知道。我会尝试使用Rice Code 对差异进行编码,选择参数以获得针对差异分布的最佳压缩。

【讨论】:

以上是关于以微秒精度压缩 unix 时间戳的主要内容,如果未能解决你的问题,请参考以下文章

从 Avro 将 unix 时间戳(以秒为单位)导入 Bigquery 中的正确时间戳

java中的当前时间(以微秒为单位)

HSQLDB (HyperSQL) - 如何以毫秒精度将 UNIX 时间戳作为数字获取

微秒级 TCP 时间戳

你认识一个 16 位的时间戳吗?

防止 Excel 在时间戳中舍入微秒