使用C++和Python进行随机访问时,Linux内存映射文件性能差劲

11
尝试使用内存映射文件创建一个多GB(约13GB)文件时,我遇到了mmap()的问题。最初在Windows上使用boost::iostreams::mapped_file_sink进行了实现,并且一切顺利。然后在Linux上运行代码,Windows上花费几分钟的时间在Linux上变成了数小时。这两台机器是相同硬件的克隆:Dell R510 2.4GHz 8M Cache 16GB Ram 1TB Disk PERC H200 Controller。担心可能存在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看起来稍微快一些)时具有类似的性能。看起来,在Linux上,无论是C ++还是Python,在大约3GB(400 million * 8 bytes per int64 = 3.2GB)左右的位置性能下降。我知道在32位Linux上,3GB是一个魔法边界,但不知道64位Linux是否存在类似的行为。结果的要点是:在400 million int64s时,Windows需要1.4分钟,而Linux需要1.7小时。我实际上正在尝试映射近13亿个int64。有人能解释一下Windows和Linux之间性能差异如此之大的原因吗?任何帮助或建议都将不胜感激!

LoadTest.cpp

Makefile

LoadTest.vcxproj

更新的 mmap_test.py

原始的 mmap_test.py

更新结果:使用更新的 Python 代码后,Python 的速度现在与 C++ 相当。

原始结果 注意:Python 的结果已经过时。


1
你的Linux机器有多少内存? - Mats Petersson
@MatsPetersson 16GB内存 - shao.lo
1
你可以尝试使用 madvise() 函数并查看是否有所改变。你可能需要尝试不同的参数:http://man7.org/linux/man-pages/man2/madvise.2.html - John Zwinck
@Mine 安装的 Linux 是 64 位的。 - shao.lo
1
FYI,numpy数组中dtype为uint32的整数占用4个字节。每次使用标量索引访问数组时,都会创建一个新的Python整数(无限精度,超过4个字节)(装箱/拆箱很昂贵,这就是为什么应该使用向量操作的原因:out[fpr[::2]]=fpr[::2])。 - jfs
显示剩余8条评论
1个回答

8

编辑:升级为“正式答案”。问题在于Linux处理“dirty pages”的方式。我仍然希望系统时不时地刷新脏页,所以我不允许它拥有太多未完成的页面。但是同时,我可以展示这就是问题所在。

我用以下命令(使用“sudo-i”):

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

这些设置会导致虚拟机污染设置:

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

与“before”进行比较:
$ ./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

我不确定应该使用什么设置来实现最佳性能,同时又不让所有脏页面永远停留在内存中(这意味着如果系统崩溃,写入磁盘的时间会更长)。

历史记录:以下是我最初作为“非答案”的回复的内容-此处的一些评论仍然适用...

并不是真正的答案,但我觉得有趣的是,如果我改变代码,首先读取整个数组,然后再将其写出,它比在同一个循环中完成两个操作要快得多。我明白,如果你需要处理真正庞大的数据集(比内存还大),这种做法毫无意义。在发布的原始代码中,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,我正在看到每秒IO速率为50-70MB/s,这非常好,因为我并不真正期望我的相对简单的设置提供更多。在更大的尺寸下改进不是那么好,但仍然有一个相当不错的改进:总共用时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 不幸的是,我也非常疲倦,需要睡觉。我明天会试着尝试这些方法,但不要抱太大希望。正如我所说,这不是真正的答案,而是一篇不适合写在评论中的长篇评论(其中包含代码,读起来完全无用)。

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