首页 > 解决方案 > 什么是在 C 中解析二进制文件的高效且干净的方法?

问题描述

我正在解析一个我知道格式的自定义二进制文件结构。

一般的想法是,每个文件都被分解成连续字节块,我想并行地对其进行分离和解码。

我正在寻找一种可读的、高性能的替代方案decode_block()

这是我目前正在使用的内容:

#include <stdio.h>

int decode_block(uint8_t buffer[]);

int main(){
  FILE *ptr;

  ptr = fopen("example_file.bin", "rb");
  if (!ptr){
    printf("can't open.\n");
    return 1;
  }

  int block1_size = 2404;
  uint8_t block1_buffer[block1_size];
  fread(block1_buffer, sizeof(char), block1_size, ptr);

  int block2_size = 3422;
  uint8_t block2_buffer[block2_size];
  fread(block2_buffer, sizeof(char), block2_size, ptr);

  fclose(ptr);

  //Do these in parallel
  decode_block(block1_buffer);
  decode_block(block2_buffer);
  return 0;
}

int decode_block(uint8_t buffer[]){
  unsigned int size_of_block = (buffer[3] << 24) + (buffer[2] << 16) + (buffer[1] << 8) + buffer[0];
  unsigned int format_version = buffer[4];
  unsigned int number_of_values = (buffer[8] << 24) + (buffer[7] << 16) + (buffer[6] << 8) + buffer[5];
  unsigned int first_value = (buffer[10] << 8) + buffer[9];

  // On and on and on

  int ptr = first_value
  int values[number_of_values];

  for(int i = 0; i < number_of_values; i++){
    values[i] = (buffer[ptr + 3] << 24) + (buffer[ptr + 2] << 16) + (buffer[ptr + 1] << 8) + buffer[ptr];
    ptr += 4
  }

  // On and on and on

  return 0
}

将整个文件读入字节数组,然后逐字节解释数组,感觉有点多余。此外,它还产生了非常庞大的代码。

但是由于我需要并行操作文件的多个部分,所以我想不出另一种方法来做到这一点。此外,是否有更简单或更快的方法将早期字节转换buffer为他们尊重的元数据值?

标签: cparsinggcc

解决方案


ID:

  • 使用“内存映射文件”来避免加载原始数据(例如mmap()在 POSIX 系统中)。请注意,这不是可移植的“普通 C”,但几乎每个操作系统都支持执行此操作的方法。

  • 确保文件格式规范要求这些值与文件中的 4 字节边界对齐,并且(如果您确实需要支持有符号整数)这些值以“2 的补码”格式(而不是“符号和幅度”或其他任何东西)

  • 尽可能检查文件是否符合规范(不仅是对齐要求,还包括“数据不能在标题中间开始”、“数据开始 + 条目 * entry_size 不能超过文件大小”等内容, “版本无法识别”等)。

  • 对于 little-endian 机器有不同的代码(例如,可以在编译时使用 选择使用哪个代码#ifdef),您可以将内存映射文件的数据转换为int32_t(或uint32_t)。请注意,您显示的代码(例如(buffer[ptr + 3] << 24) + (buffer[ptr + 2] << 16) + (buffer[ptr + 1] << 8) + buffer[ptr]),负数被破坏(即使在“2 的恭维”机器上);因此替代代码(对于“非小端”情况)将比您的更复杂(并且更慢) . 当然,如果您不需要支持负数,则不应使用任何有符号整数类型(例如int),坦率地说,int无论如何您都不应该将“可能是 16 位”用于 32 位值。

  • 确定您应该使用多少线程(可能是命令行参数;可能是通过询问操作系统计算机实际有多少 CPU)。启动线程并告诉它们它们是哪个“线程号”(现有线程是 0 号,第一个产生的线程是 1 号等)。

  • 让线程根据它们的“线程号”、全局“总线程”、全局“总条目”和全局“第一个条目的偏移量”计算它们的开始和结束偏移量(在内存映射文件中)。这主要是对舍入特别注意的除法。请注意(为避免全局变量),您可以将包含详细信息的结构传递给每个线程。因为线程只读取它,所以不需要对此数据采取任何保护措施(例如锁、临界区)。

  • 让每个线程并行解析其数据部分;然后等待它们全部完成(例如,如果您不想保留线程供以后使用,也许“线程号 0”会执行“pthread_join()”)。

您可能还需要检查所有值(由所有线程解析)是否在允许的范围内(以符合文件格式规范);并在不处理的情况下进行某种错误处理(例如,当文件损坏或被恶意篡改时)。这可以像(全局的,原子递增的)“到目前为止发现的可疑值的数量”计数器一样简单;这可以让您在解析所有值后显示“找到 N 个狡猾的值”错误消息。

注1:如果您不想使用内存映射文件(或不能);您可以拥有一个“文件读取器线程”和多个“文件解析器线程”。这需要更多的同步(它会演变为具有流控制的 FIFO 队列 - 例如,提供者线程执行某种“队列满时 {wait}”,而消费者线程执行某种“队列空时 {wait}”)。与使用内存映射文件相比,这种额外的同步会增加开销并使其变慢(除了更复杂之外)。

注意 2:如果文件的数据没有被操作系统的文件数据缓存缓存,那么无论您做什么,您都可能会遇到文件 IO 的瓶颈,并且在这种情况下使用多线程可能无助于提高性能。


推荐阅读