【问题标题】:Bad Linux Memory Mapped File Performance with Random Access C++ & Python使用随机访问 C++ 和 Python 时 Linux 内存映射文件性能不佳
【发布时间】:2014-12-08 13:15:37
【问题描述】:

在尝试使用内存映射文件创建数 GB 的文件(大约 13gb)时,我遇到了 mmap() 的问题。最初的实现是在 Windows 上的 c++ 中使用 boost::iostreams::mapped_file_sink 完成的,一切都很好。然后代码在 Linux 上运行,在 Windows 上花费几分钟的时间在 Linux 上变成了几个小时。

这两台机器是相同硬件的克隆:Dell R510 2.4GHz 8M Cache 16GB Ram 1TB Disk PERC H200 Controller。

Linux 是使用 3.8 内核和 g++ 4.83 的 Oracle Enterprise Linux 6.5。

有人担心 boost 库可能存在问题,因此使用 boost::interprocess::file_mapping 和本机 mmap() 完成了实现。这三个都表现出相同的行为。当 Linux 性能严重下降时,Windows 和 Linux 性能达到一定水平。

完整的源代码和性能数据链接如下。

// C++ code using boost::iostreams
void IostreamsMapping(size_t rowCount)
{
   std::string outputFileName = "IoStreamsMapping.out";
   boost::iostreams::mapped_file_params params(outputFileName);
   params.new_file_size = static_cast<boost::iostreams::stream_offset>(sizeof(uint64_t) * rowCount);
   boost::iostreams::mapped_file_sink fileSink(params); // NOTE: using this form of the constructor will take care of creating and sizing the file.
   uint64_t* dest = reinterpret_cast<uint64_t*>(fileSink.data());
   DoMapping(dest, rowCount);
}

void DoMapping(uint64_t* dest, size_t rowCount)
{
   inputStream->seekg(0, std::ios::beg);
   uint32_t index, value;
   for (size_t i = 0; i<rowCount; ++i)
   {
      inputStream->read(reinterpret_cast<char*>(&index), static_cast<std::streamsize>(sizeof(uint32_t)));
      inputStream->read(reinterpret_cast<char*>(&value), static_cast<std::streamsize>(sizeof(uint32_t)));
      dest[index] = value;
   }
}

最后一次测试是用 Python 完成的,以便用另一种语言重现。脱落发生在同一个地方,所以看起来是同一个问题。

# Python code using numpy
import numpy as np
fpr = np.memmap(inputFile, dtype='uint32', mode='r', shape=(count*2))
out = np.memmap(outputFile, dtype='uint64', mode='w+', shape=(count))
print("writing output")
out[fpr[::2]]=fpr[::2]

对于 c++ 测试,Windows 和 Linux 具有相似的性能,最高可达 3 亿个 int64(Linux 看起来稍快一些)。对于 C++ 和 Python,Linux 上的性能似乎下降了大约 3Gb(4 亿 * 每个 int64 8 字节 = 3.2Gb)。

我知道在 32 位 Linux 上 3Gb 是一个神奇的边界,但我不知道 64 位 Linux 的类似行为。

结果的要点是,在 4 亿个 int64 上,Windows 需要 1.4 分钟,而 Linux 需要 1.7 小时。我实际上正在尝试映射接近 13 亿个 int64。

谁能解释一下为什么 Windows 和 Linux 之间的性能如此不同?

任何帮助或建议将不胜感激!

LoadTest.cpp

Makefile

LoadTest.vcxproj

updated mmap_test.py

original mmap_test.py

Updated Results 更新了 Python 代码...Python 速度现在可与 C++ 媲美

Original Results 注意:Python 结果已过时

【问题讨论】:

  • 你的 Linux 机器有多少内存?
  • @MatsPetersson 16GB 内存
  • 你可以尝试使用 madvise() 看看它是否改变了什么?您可能需要尝试各种建议参数:man7.org/linux/man-pages/man2/madvise.2.html
  • 我建议检查 64 位 Linux 上的行为并查看结果。
  • fyi,numpy 数组中的一个整数,dtype uint32 正好占用 4 个字节。每次使用标量索引访问数组时都会创建一个新的 Python 整数(无限精度,超过 4 个字节)(装箱/拆箱很昂贵,这就是为什么应该使用向量操作:out[fpr[::2]]=fpr[::2]

标签: python c++ linux mmap


【解决方案1】:

编辑:升级到“正确答案”。问题在于 Linux 处理“脏页”的方式。我仍然希望我的系统不时刷新脏页,所以我不允许它有太多未完成的页面。但与此同时,我可以证明这就是正在发生的事情。

我这样做了(使用“sudo -i”):

# echo 80 > /proc/sys/vm/dirty_ratio
# echo 60 > /proc/sys/vm/dirty_background_ratio

这给出了这些设置 VM 脏设置:

grep ^ /proc/sys/vm/dirty*
/proc/sys/vm/dirty_background_bytes:0
/proc/sys/vm/dirty_background_ratio:60
/proc/sys/vm/dirty_bytes:0
/proc/sys/vm/dirty_expire_centisecs:3000
/proc/sys/vm/dirty_ratio:80
/proc/sys/vm/dirty_writeback_centisecs:500

这使我的基准测试运行如下:

$ ./a.out m64 200000000
Setup Duration 33.1042 seconds
Linux: mmap64
size=1525 MB
Mapping Duration 30.6785 seconds
Overall Duration 91.7038 seconds

与“之前”比较:

$ ./a.out m64 200000000
Setup Duration 33.7436 seconds
Linux: mmap64
size=1525
Mapping Duration 1467.49 seconds
Overall Duration 1501.89 seconds

有这些虚拟机脏设置:

grep ^ /proc/sys/vm/dirty*
/proc/sys/vm/dirty_background_bytes:0
/proc/sys/vm/dirty_background_ratio:10
/proc/sys/vm/dirty_bytes:0
/proc/sys/vm/dirty_expire_centisecs:3000
/proc/sys/vm/dirty_ratio:20
/proc/sys/vm/dirty_writeback_centisecs:500

我不确定我应该使用什么设置来获得理想的性能,同时仍然不会将所有脏页永远留在内存中(这意味着如果系统崩溃,写入磁盘需要更长的时间)。

对于历史:这是我最初写的“未回答” - 这里的一些 cmets 仍然适用......

不是一个真正的答案,但我觉得很有趣的是,如果我将代码更改为首先读取整个数组,然后将其写出,这比在同一个循环中执行这两个操作要快得多。我很欣赏如果您需要处理非常庞大的数据集(比内存大),这完全没用。使用发布的原始代码,100M uint64 值的时间为 134 秒。当我拆分读取和写入周期时,它是 43 秒。

这是修改后的DoMapping函数【只有我改过的代码】:

struct VI
{
    uint32_t value;
    uint32_t index;
};


void DoMapping(uint64_t* dest, size_t rowCount)
{
   inputStream->seekg(0, std::ios::beg);
   std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
   uint32_t index, value;
   std::vector<VI> data;
   for(size_t i = 0; i < rowCount; i++)
   {
       inputStream->read(reinterpret_cast<char*>(&index), static_cast<std::streamsize>(sizeof(uint32_t)));
       inputStream->read(reinterpret_cast<char*>(&value), static_cast<std::streamsize>(sizeof(uint32_t)));
       VI d = {index, value};
       data.push_back(d);
   }
   for (size_t i = 0; i<rowCount; ++i)
   {
       value = data[i].value;
       index = data[i].index;
       dest[index] = value;
   }
   std::chrono::duration<double> mappingTime = std::chrono::system_clock::now() - startTime;
   std::cout << "Mapping Duration " << mappingTime.count() << " seconds" << std::endl;
   inputStream.reset();
}

我目前正在运行一个包含 200M 记录的测试,这在我的机器上需要大量时间(2000 多秒没有代码更改)。很明显,所花费的时间来自磁盘 I/O,而且我看到 50-70MB/s 的 IO 速率非常好,因为我真的不希望我相当简单的设置能够提供很多比那更多的。较大尺寸的改进效果不那么好,但仍然是一个不错的改进:总时间为 1502 秒,而“在同一循环中读写”为 2021 秒。

另外,我想指出,这对任何系统来说都是一个相当糟糕的测试——Linux 明显比 Windows 差的事实是不切实际的——你真的不想映射一个大文件并写 8字节 [意味着必须读入 4KB 页面] 到每个页面随机。如果这反映了您的真实应用程序,那么您应该认真地以某种方式重新考虑您的方法。当您有足够的可用内存以使整个内存映射区域适合 RAM 时,它将运行良好。

我的系统中有大量 RAM,所以我认为问题在于 Linux 不喜欢太多“脏”的映射页面。

我有一种感觉,这可能与它有关: https://serverfault.com/questions/126413/limit-linux-background-flush-dirty-pages 更多解释: http://www.westnet.com/~gsmith/content/linux-pdflush.htm

不幸的是,我也很累,需要睡觉。我明天看看能不能用这些做实验——但不要屏住呼吸。就像我说的,这不是一个真正的答案,而是一个不适合评论的长评论(并且包含代码,在评论中阅读完全是垃圾)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-15
    • 1970-01-01
    • 1970-01-01
    • 2015-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多