【问题标题】:Fast binary parser algorithm快速二进制解析器算法
【发布时间】:2018-05-20 10:43:33
【问题描述】:

我正在为二进制文件编写解析器。数据存储在连续的 32 位记录中。文件只需读取一次,然后将其输入到分析算法中。

现在我正在读取 1024 条记录的文件块,以避免尽可能多地因调用 fread 而不是必要的频率而产生的开销。在下面的示例中,我使用 oflcorrection、timetag 和 channel 作为算法的输出,并使用 bool 返回值来检查算法是否应该停止。另请注意,并非所有记录都包含光子,只有具有正值的记录。

使用这种方法,如果我使用将文件分解成碎片的算法的线程版本,我可以处理高达 0.5GBps 或 1.5GBps 的速度。我知道我的 SSD 读取速度至少快了 40%。我正在考虑使用 SIMD 来并行解析多条记录,但我不知道如何使用条件返回子句来做到这一点。

您知道任何其他方法可以让我将分块阅读和 SIMD 结合起来吗?一般有更好的方法吗?

谢谢

附:这些记录对应于通过分束器后到达探测器的光子或指示溢出情况的特殊记录。需要后者,因为时间标签以皮秒分辨率存储在 uint64_t 中。

 static inline bool next_photon(FILE* filehandle, uint64_t * RecNum,
                               uint64_t StopRecord, record_buf_t *buffer,
                               uint64_t *oflcorrection, uint64_t *timetag, int *channel)
{
    pop_record:
    while (__builtin_unpredictable(buffer->head < RECORD_CHUNK)) { // still have records on buffer
        ParseHHT2_HH2(buffer->records[buffer->head], channel, timetag, oflcorrection);
        buffer->head++;
        (*RecNum)++;

        if (*RecNum >= StopRecord) { // run out of records
            return false;
        }

        if (*channel >= 0) { // found a photon
            return true;
        }
    }
    // run out of buffer
    buffer->head = 0;
    fread(buffer->records, RECORD_CHUNK, sizeof(uint32_t), filehandle);
    goto pop_record;
}

请在下面找到解析功能。请记住,我对文件格式无能为力。再次感谢 Guillem。

static inline void ParseHHT2_HH2(uint32_t record, int *channel,
                                 uint64_t *timetag, uint64_t *oflcorrection)
{
    const uint64_t T2WRAPAROUND_V2 = 33554432;
    union{
        uint32_t   allbits;
        struct{ unsigned timetag  :25;
            unsigned channel  :6;
            unsigned special  :1;
        } bits;
    } T2Rec;

    T2Rec.allbits = record;

    if(T2Rec.bits.special) {
        if(T2Rec.bits.channel==0x3F) {  //an overflow record
            if(T2Rec.bits.timetag!=0) {
                *oflcorrection += T2WRAPAROUND_V2 * T2Rec.bits.timetag;
            }
            else {  // if it is zero it is an old style single overflow
                *oflcorrection += T2WRAPAROUND_V2;  //should never happen with new Firmware!
            }
            *channel = -1;
        } else if(T2Rec.bits.channel == 0) {  //sync
            *channel = 0;
        } else if(T2Rec.bits.channel<=15) {  //markers
            *channel = -2;
        }
    } else {//regular input channel
        *channel = T2Rec.bits.channel + 1;
    }
    *timetag = *oflcorrection + T2Rec.bits.timetag;
}

我想出了一个几乎无分支的解析函数,但它并没有产生任何加速。

if(T2Rec.bits.channel==0x3F) {  //an overflow record
        *oflcorrection += T2WRAPAROUND_V2 * T2Rec.bits.timetag;
    }
    *channel = (!T2Rec.bits.special) * (T2Rec.bits.channel + 1) - T2Rec.bits.special * T2Rec.bits.channel;
    *timetag = *oflcorrection + T2Rec.bits.timetag;
}

【问题讨论】:

  • label和goto语句是不可取的;目前尚不清楚是否需要它们。你完全可以在 label/goto 循环的主体周围使用for (;;)while (1)。你不应该忽略来自fread() 的返回值;它告诉您读取了多少数据(如果有)。如果忽略该返回值,则无法编写可靠的代码。
  • ParseHHT2_HH2(buffer-&gt;records[buffer-&gt;head], channel, timetag, oflcorrection); 是做什么的?顺便说一句:传递和取消引用指针看起来很昂贵。
  • 你假设,@GuillemB,你的文件总是格式正确并且没有发生 I/O 错误。这些都不是一个安全的假设。检查函数调用的返回值。
  • @JonathanLeffler 和 JohnBollinger。你当然是对的,我痴迷于试图让它快速运行,我认为另一个如果会杀了我。当然不是,因为它很少被调用。在那个话题上,关于通道条件的 if 子句的成本是巨大的。通过消除和(当然杀死之后的算法),我在一个只读取文件中光子总数的微不足道的函数上将解析速度提高了 2 倍。
  • 另外:主循环内的条件数量(加上活动表达式的数量)将有效地破坏分支预测。在任何情况下:配置文件并检查生成的汇编源代码。

标签: c performance parsing


【解决方案1】:

I/O 很可能支配函数的运行时间。也就是说,您应该首先测量速度而不进行解析,即只是 fread。可能包括解析在内的速度不会有太大差异。

如果是这样,您可以先集中精力优化该瓶颈。查看 linux 工具 fio,特别是使用不同的 --ioenginge=(也是 libaio)。如果您使用的是 NVMe 磁盘,请查看英特尔 SPDK。

除此之外,您还可以进一步优化解析。您可以避免使用(*RecNum)++,更重要的是避免循环中的第一个 if 子句,因为在fread 之后,您知道将读取多少条记录,因此您可以使用该信息。

此外,我不会迭代 buffer-&gt;head,而是使用局部变量,使用 for 循环。

我还将为*RecNum 使用局部变量,并且仅在最后分配给*RecNum。如果您的目标是并行写入*RecNum,那么您的代码无论如何都会有错误,因为您的增量和读取都没有使用原子操作。

直到那时您才应该开始考虑 SSE 或 AVX。如果*channel 中的大部分为零,则可以使用 SSE/AVX 一次检查 16 个或更多字节是否大于或等于零。

更新:
现在,在您提供 parse 函数的代码后,我可以看到情况有所不同。那里有很多分店...

更新:
这是我的意思是next_photon 的优化实现。如果输入next_photon时保证buffer-&gt;head == 0,可以简化。而且我假设您不会故意检查fread 的返回值,因为您只想使用StopRecord 来处理它。所以我就这样离开了,即使它不安全。

static inline bool next_photon(FILE* filehandle, uint64_t *RecNum,
                            uint64_t StopRecord, record_buf_t *buffer,
                            uint64_t *oflcorrection, uint64_t *timetag,
                            int *channel)
{
    int recNum = *RecNum;
    int i = buffer->head;

    while (true) {
        int records;
        bool quit;

        if (StopRecord - recNum <= RECORD_CHUNK - i) {
            records = i + StopRecord - recNum;
            quit = true;
        } else {
            records = RECORD_CHUNK;
            quit = false;
        }

        const int i0 = i;

        for (; i < records; i++) { // still have records on buffer
            ParseHHT2_HH2(buffer->records[i], channel, timetag, oflcorrection);

            if (*channel >= 0) { // found a photon
                *RecNum = recNum + i - i0 + 1;
                buffer->head = i + 1;
                return true;
            }
        }

        recNum += records - i0;

        if (quit) {
            break;
        }

        // run out of buffer
        i = 0;
        fread(buffer->records, RECORD_CHUNK, sizeof(uint32_t), filehandle);    
    }

    *RecNum = recNum;
    buffer->head = i;
}

【讨论】:

  • 除了看起来很明显 OP 的代码 I/O 绑定,因为他看到并行化分析显着加速,即使那样他也不是(他认为) 使他的 I/O 带宽饱和。
  • 您怎么能 100% 确定这一点?优化 SSD 访问并非易事。我们不知道RECORD_CHUNK的选择。
  • 并行化分析将吞吐量提高了 3 倍,这表明分析成本与 I/O 成本处于同一数量级。如果程序严格 I/O 限制,那么加快分析部分的速度不会显着提高整体吞吐量。
  • 对于 1,5 GB 文件,单线程程序在 3 秒内解析整个内容。如果我只是从解析函数返回并跳过实际的解析,大约需要 0.9 秒。用于分析数据的更简单的算法确实需要 3 秒。 RECORD 块是 1024,每条记录是 4 个字节,所以我正在读取 4kB 块。 1024 的任何倍数都会给我类似的性能。
  • @PedramAzad 我知道有很多分支......我做了一个几乎没有分支的解析器。请参阅上面的编辑。令我惊讶的是,这不会产生任何加速。唯一似乎有所作为的是摆脱了 next_photon 函数中的 ifs。这就是为什么我想就如何重新安排计算以可能使用 SIMD 加分块有一个更抽象的答案。
【解决方案2】:

您正在循环访问磁盘,我认为 SIMD 在那里不会有太大帮助,您可以使用 mmap。

检查这些答案:

When should I use mmap for file access?

Fastest file reading in C

但您也可以将 SIMD (SSE/AVX/NEON) 用于其他部分,例如解析代码

【讨论】:

    【解决方案3】:

    通过并行化加速数据分析对程序的吞吐量有如此显着的影响,这表明数据分析成本与 I/O 成本处于同一数量级。因此,如果您想将其吞吐量提高到更接近可用 I/O 带宽所施加的限制,最好的做法可能是并行执行分析和 I/O。

    您可以通过维护两个独立的 I/O 缓冲区来做到这一点,在读入另一个缓冲区的同时处理一个缓冲区,然后翻转。

    【讨论】:

    • 作为一个更复杂的文件分析示例,例如计算光子在两个通道中的到达时间之间的相关性。这相当于计算到达时间之间的增量并将其放入直方图中。该算法的 4 线程版本需要 1.4 秒(同样是 1.5 GB 文件),而计数光子功能(有 4 个线程)需要 1 秒。我对消除 if(*channel>=0) 将 count_photon 函数的速度提高到 0.6 秒这一事实感到非常惊讶。
    猜你喜欢
    • 2013-10-31
    • 2019-03-11
    • 1970-01-01
    • 2018-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-20
    • 1970-01-01
    相关资源
    最近更新 更多