使用内存映射文件在C++中解析二进制文件速度过慢

8

我正在尝试整数方式解析二进制文件以检查整数值是否满足某个条件,但循环非常缓慢。

此外,我发现内存映射文件是将文件快速读入内存的最快方法,因此我正在使用以下基于Boost的代码:

unsigned long long int get_file_size(const char *file_path) {
    const filesystem::path file{file_path};
    const auto generic_path = file.generic_path();
    return filesystem::file_size(generic_path);
}

boost::iostreams::mapped_file_source read_bytes(const char *file_path,
                                         const unsigned long long int offset,
                                         const unsigned long long int length) {
    boost::iostreams::mapped_file_params parameters;
    parameters.path = file_path;
    parameters.length = static_cast<size_t>(length);
    parameters.flags = boost::iostreams::mapped_file::mapmode::readonly;
    parameters.offset = static_cast<boost::iostreams::stream_offset>(offset);

    boost::iostreams::mapped_file_source file;

    file.open(parameters);
    return file;
}

boost::iostreams::mapped_file_source read_bytes(const char *file_path) {
    const auto file_size = get_file_size(file_path);
    const auto mapped_file_source = read_bytes(file_path, 0, file_size);
    return mapped_file_source;
}

我的测试用例大致如下:

inline auto test_parsing_binary_file_performance() {
    const auto start_time = get_time();
    const std::filesystem::path input_file_path = "...";
    const auto mapped_file_source = read_bytes(input_file_path.string().c_str());
    const auto file_buffer = mapped_file_source.data();
    const auto file_buffer_size = mapped_file_source.size();
    LOG_S(INFO) << "File buffer size: " << file_buffer_size;
    auto printed_lap = (long) (file_buffer_size / (double) 1000);
    printed_lap = round_to_nearest_multiple(printed_lap, sizeof(int));
    LOG_S(INFO) << "Printed lap: " << printed_lap;
    std::vector<int> values;
    values.reserve(file_buffer_size / sizeof(int)); // Pre-allocate a large enough vector
    // Iterate over every integer
    for (auto file_buffer_index = 0; file_buffer_index < file_buffer_size; file_buffer_index += sizeof(int)) {
        const auto value = *(int *) &file_buffer[file_buffer_index];
        if (value >= 0x30000000 && value < 0x49000000 - sizeof(int) + 1) {
            values.push_back(value);
        }

        if (file_buffer_index % printed_lap == 0) {
            LOG_S(INFO) << std::setprecision(4) << file_buffer_index / (double) file_buffer_size * 100 << "%";
        }
    }

    LOG_S(INFO) << "Values found count: " << values.size();

    print_time_taken(start_time, false, "Parsing binary file");
}
内存映射文件的读取速度如预期般快,但在我的设备上逐个整数地迭代速度过慢,尽管硬件条件良好(SSD等)。
2020-12-20 13:04:35.124 (   0.019s) [main thread     ]Tests.hpp:387   INFO| File buffer size: 419430400
2020-12-20 13:04:35.124 (   0.019s) [main thread     ]Tests.hpp:390   INFO| Printed lap: 419432
2020-12-20 13:04:35.135 (   0.029s) [main thread     ]Tests.hpp:405   INFO| 0%
2020-12-20 13:04:35.171 (   0.065s) [main thread     ]Tests.hpp:405   INFO| 0.1%
2020-12-20 13:04:35.196 (   0.091s) [main thread     ]Tests.hpp:405   INFO| 0.2%
2020-12-20 13:04:35.216 (   0.111s) [main thread     ]Tests.hpp:405   INFO| 0.3%
2020-12-20 13:04:35.241 (   0.136s) [main thread     ]Tests.hpp:405   INFO| 0.4%
2020-12-20 13:04:35.272 (   0.167s) [main thread     ]Tests.hpp:405   INFO| 0.5%
2020-12-20 13:04:35.293 (   0.188s) [main thread     ]Tests.hpp:405   INFO| 0.6%
2020-12-20 13:04:35.314 (   0.209s) [main thread     ]Tests.hpp:405   INFO| 0.7%
2020-12-20 13:04:35.343 (   0.237s) [main thread     ]Tests.hpp:405   INFO| 0.8%
2020-12-20 13:04:35.366 (   0.261s) [main thread     ]Tests.hpp:405   INFO| 0.9%
2020-12-20 13:04:35.399 (   0.293s) [main thread     ]Tests.hpp:405   INFO| 1%
2020-12-20 13:04:35.421 (   0.315s) [main thread     ]Tests.hpp:405   INFO| 1.1%
2020-12-20 13:04:35.447 (   0.341s) [main thread     ]Tests.hpp:405   INFO| 1.2%
2020-12-20 13:04:35.468 (   0.362s) [main thread     ]Tests.hpp:405   INFO| 1.3%
2020-12-20 13:04:35.487 (   0.382s) [main thread     ]Tests.hpp:405   INFO| 1.4%
2020-12-20 13:04:35.520 (   0.414s) [main thread     ]Tests.hpp:405   INFO| 1.5%
2020-12-20 13:04:35.540 (   0.435s) [main thread     ]Tests.hpp:405   INFO| 1.6%
2020-12-20 13:04:35.564 (   0.458s) [main thread     ]Tests.hpp:405   INFO| 1.7%
2020-12-20 13:04:35.586 (   0.480s) [main thread     ]Tests.hpp:405   INFO| 1.8%
2020-12-20 13:04:35.608 (   0.503s) [main thread     ]Tests.hpp:405   INFO| 1.9%
2020-12-20 13:04:35.636 (   0.531s) [main thread     ]Tests.hpp:405   INFO| 2%
2020-12-20 13:04:35.658 (   0.552s) [main thread     ]Tests.hpp:405   INFO| 2.1%
2020-12-20 13:04:35.679 (   0.574s) [main thread     ]Tests.hpp:405   INFO| 2.2%
2020-12-20 13:04:35.702 (   0.597s) [main thread     ]Tests.hpp:405   INFO| 2.3%
2020-12-20 13:04:35.727 (   0.622s) [main thread     ]Tests.hpp:405   INFO| 2.4%
2020-12-20 13:04:35.769 (   0.664s) [main thread     ]Tests.hpp:405   INFO| 2.5%
2020-12-20 13:04:35.802 (   0.697s) [main thread     ]Tests.hpp:405   INFO| 2.6%
2020-12-20 13:04:35.831 (   0.726s) [main thread     ]Tests.hpp:405   INFO| 2.7%
2020-12-20 13:04:35.860 (   0.754s) [main thread     ]Tests.hpp:405   INFO| 2.8%
2020-12-20 13:04:35.887 (   0.781s) [main thread     ]Tests.hpp:405   INFO| 2.9%
2020-12-20 13:04:35.924 (   0.818s) [main thread     ]Tests.hpp:405   INFO| 3%
2020-12-20 13:04:35.956 (   0.850s) [main thread     ]Tests.hpp:405   INFO| 3.1%
2020-12-20 13:04:35.998 (   0.893s) [main thread     ]Tests.hpp:405   INFO| 3.2%
2020-12-20 13:04:36.033 (   0.928s) [main thread     ]Tests.hpp:405   INFO| 3.3%
2020-12-20 13:04:36.060 (   0.955s) [main thread     ]Tests.hpp:405   INFO| 3.4%
2020-12-20 13:04:36.102 (   0.997s) [main thread     ]Tests.hpp:405   INFO| 3.5%
2020-12-20 13:04:36.132 (   1.026s) [main thread     ]Tests.hpp:405   INFO| 3.6%
...
2020-12-20 13:05:03.456 (  28.351s) [main thread     ]Tests.hpp:410   INFO| Values found count: 10650389
2020-12-20 13:05:03.456 (  28.351s) [main thread     ]          benchmark.cpp:31    INFO| Parsing binary file took 28.341 second(s)

解析那些“419 MB”的数据通常需要大约28-70秒,即使在“Release”模式下编译也没有真正帮助。是否有任何方法可以缩短这段时间?看起来我执行的操作不应该如此低效。请注意,我正在使用“GCC 10”为“Linux 64位”编译。根据评论建议,使用具有“advise()”的“内存映射文件”也不能提高性能。
boost::interprocess::file_mapping file_mapping(input_file_path.string().data(), boost::interprocess::read_only);
boost::interprocess::mapped_region mapped_region(file_mapping, boost::interprocess::read_only);
mapped_region.advise(boost::interprocess::mapped_region::advice_sequential);
const auto file_buffer = (char *) mapped_region.get_address();
const auto file_buffer_size = mapped_region.get_size();
...

根据评论和答案得出的一些结论:

  • 使用advise(boost::interprocess::mapped_region::advice_sequential)没有帮助
  • 不调用reserve()或者调用时大小刚好为正确值,可以将性能提高一倍
  • 直接迭代int *比迭代char *稍慢一些
  • 使用std::vector收集结果比使用std::set稍快一些
  • 性能对于进度日志记录来说微不足道

1
*(int *) 是UB(严格别名违规)。还有%的日志行在哪里?请包含前几个。添加 file.advise(boost::interprocess::mapped_region::advice_sequential); 会有帮助吗? - rustyx
@rustyx:我尝试了使用advise()memory-mapped file方法,但它并没有提高性能。此外,我添加了一些缺失的日志行。 - BullyWiiPlaza
2
只是为了确认一下...将文件复制到/dev/null需要多长时间?啊...而且,“预期的内存映射文件读取几乎瞬间完成”并没有太多意义。如果Linux内存映射的工作方式与其他操作系统相同,那么在打开文件时不会读取文件...每次尝试读取4kb块(一个内存页)时,都会发生由操作系统捕获的故障,并从磁盘填充该页。 - xanatos
1
@xanatos:复制文件大约需要 4 秒,谢谢您指出 内存映射文件 会导致多次磁盘访问的提示。我用将整个文件读入内存后再处理它的方式替换了那段代码,这解决了性能问题,大大缩短了处理时间。 - BullyWiiPlaza
另一个例子表明,在I/O绑定问题中,I/O是限制性能的因素。我想知道如果你使用未缓冲的read而不是使用iostreams,性能会提高多少。通过直接写入和读取专门用于此目的的专用分区(如从/dev/sda2读取)进行原始磁盘I/O可能存在一些潜力。这是否在工作流程中有意义取决于您如何获取数据。如果它是在本地生成的,则根本不需要通过磁盘。同样,如果是通过局域网获取的。 - Peter - Reinstate Monica
2个回答

1

正如xanatos所暗示的那样,内存映射文件在性能上是具有欺骗性的,因为它们并不会立即将整个文件读入内存。在处理过程中,由于页面未命中而导致了多次磁盘访问,从而严重影响了性能。

在这种情况下,更有效的方法是先将整个文件读入内存,然后再通过内存进行迭代:

inline std::vector<std::byte> load_file_into_memory(const std::filesystem::path &file_path) {
    std::ifstream input_stream(file_path, std::ios::binary | std::ios::ate);

    if (input_stream.fail()) {
        const auto error_message = "Opening " + file_path.string() + " failed";
        throw std::runtime_error(error_message);
    }

    auto current_read_position = input_stream.tellg();
    input_stream.seekg(0, std::ios::beg);

    auto file_size = std::size_t(current_read_position - input_stream.tellg());
    if (file_size == 0) {
        return {};
    }

    std::vector<std::byte> buffer(file_size);

    if (!input_stream.read((char *) buffer.data(), buffer.size())) {
        const auto error_message = "Reading from " + file_path.string() + " failed";
        throw std::runtime_error(error_message);
    }

    return buffer;
}

现在的性能已经大大提高,总共大约需要 3 - 15 秒

我认为将一个大小不确定的文件全部读入内存是个坏主意……这不是我会建议的方法。很遗憾,我这里没有 Linux 机器来调试你的读取速度为什么很慢。但你并不需要真的读取整个文件。你可以按照 4kb(或更多……通常有递减收益……唯一重要的是使用 4kb 的倍数:8kb、16kb、32kb、64kb 等)的块读取文件。 - xanatos
1
通常情况下,您会缓冲文件,这正是std::ifstream所做的。您尝试过只是迭代它吗? - Passer By
@xanatos:是的,通常我也会为软件的最终用户提供缓冲版本,以防需要(例如当文件不能完全适应RAM时)。然而,这很可能比一次性读取所有内容要慢。 - BullyWiiPlaza
我不明白为什么使用足够大的缓冲区(例如1MB)流式处理文件应该比一次性将其全部读入RAM,然后迭代它显着慢。在这两种情况下,磁盘I/O都是最慢的瓶颈。 - Thomas

0

这让我想起了40年前我第一次遇到的缓慢问题,那是由于一个百分比进度条导致的。将其注释掉并重新测量。 还要测量容量储备,并检查实际所需容量 - 如果只需要1%,那么你就浪费了空间,因此也浪费了时间。

  • unsigned long long 可能会很昂贵。 unsignedlong 不足以满足吗?
  • 取模、除法可能会额外增加成本。
  • 进度日志记录可能会很慢,最好使用单独的线程,然后检查是否刷新(与直觉相反)可能更快。

所以:

const auto pct_factor = file_buffer_size == 0 ? 0.0 : 100 / (double)file_buffer_size;
values.reserve(file_buffer_size / sizeof(int));
for (auto file_buffer_index = 0, long pct_countdown = 0; file_buffer_index < file_buffer_size; file_buffer_index += sizeof(int)) {
    const auto value = *(int *) &file_buffer[file_buffer_index];
    if (value >= 0x30000000 && value < 0x49000000 - sizeof(int) + 1) {
        values.push_back(value);
    }

    if (pct_countdown-- < 0) {
        pct_countdown = printed_lap;
        const auto pct = file_buffer_index * pct_factor;
        LOG_S(INFO) << std::setprecision(4) << pct << "%";
    }
}
  • 整数百分比可能会更好。稍微舍弃精确度。
  • 批量数据values - 是否真的需要如此。一个集合可能足够了。

我承认我对于*(int *)有些疑虑。使用一个int*指针并增加它似乎更直接。


1
进度日志记录不会显著影响性能。提前准确地保留向量大小实际上可以将时间缩短约一半,但是省略reserve()也可以获得大致相同的效益。直接使用int *进行迭代并没有帮助(事实上,速度甚至稍微慢了一些)。使用std::set似乎也更慢。尽管如此,我最终还是需要大约24秒的处理时间,这似乎还不够优化。 - BullyWiiPlaza
1
long long和取模运算几乎可以确定不是问题。 - Tumbleweed53
@Tumbleweed53 谢谢,不过我希望这确实不是内存映射输入的使用方式。 - Joop Eggen
@BullyWiiPlaza 的 values.add 是有条件地发生的,所以 reserve 可能会浪费大量资源。进度日志记录(以及其周围的代码)不是罪魁祸首,这意味着可能存在内存问题。 - Joop Eggen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接