【问题标题】:What's a performant and clean way to parse a binary file in C?什么是在 C 中解析二进制文件的高效且干净的方法?
【发布时间】:2020-06-05 18:56:59
【问题描述】:

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

一般的想法是,每个文件都被分解成连续字节块,我想将它们分开并并行解码。

我正在寻找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 中的早期字节转换为其受尊重的元数据值?

【问题讨论】:

  • 你知道文件中的块边界在哪里,而不必扫描它吗?
  • 打开文件两次 - 2 个文件指针。让 1 个线程从头开始读取/解码,另一个线程在中间开始读取/解码。请参阅:fseek。
  • @jwdonahue 是的,我提前知道了区块边界。
  • @Johnny Mopp 使用两个不同的指针同时读取同一个文件的文件权限没有问题吗?

标签: c parsing gcc


【解决方案1】:

我会:

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

  • 确保文件格式规范要求值与文件中的 4 字节边界对齐,并且(如果您确实需要支持有符号整数)将值存储在“2 的补码”中格式(而不是“符号和大小”或其他任何内容)

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

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

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

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

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

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

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

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

【讨论】:

  • 这是一个很棒的答案,非常感谢!我不确定您检查“数据不能在标题中间开始”是什么意思。这意味着什么?我该如何检查?
  • @CSSStudent7782:我猜first_value = (buffer[10] &lt;&lt; 8) + buffer[9]; 用于确定文件中第一个值的偏移量(例如,这样您将来可以在不破坏向后兼容性的情况下增加标头的大小)。如果我的猜测是正确的,那么您可能会收到类似 if(first_value &lt; 11) 的支票(因为该版本的标头是 11 个字节)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多