mmap 是直接访问页面缓存,还是页面缓存的副本?
Does mmap directly access the page cache, or a copy of the page cache?
换个方式问这个问题,你能确认当你 mmap() 一个文件时,你确实访问了页面缓存中已经存在的确切物理页面吗?
我问是因为我正在一台 1TB RAM 的 192 核机器上进行测试,在测试前预先缓存到页面缓存中的 400GB 数据文件上(通过删除缓存,然后执行文件上的 md5sum)。
最初,我有所有 192 个线程,每个线程分别映射文件,假设它们都将(基本上)返回相同的内存区域(或者可能是相同的内存区域,但以某种方式映射多次)。因此,我假设使用两个不同映射到同一个文件的两个线程都可以直接访问相同的页面。 (让我们忽略此示例的 NUMA,但显然它在更高的线程数下很重要。)
但是,在实践中,我发现当每个线程单独映射文件时,在更高的线程数下性能会变得 糟糕 。当我们删除它而只是执行一个传递到线程的 mmap(这样所有线程都直接访问同一内存区域)时,性能会显着提高 .
一切都很好,但我想找出原因。如果实际上映射一个文件只是授予对现有页面缓存的直接访问权限,那么我认为映射它多少次并不重要——它应该都去到完全相同的地方。
但鉴于存在这样的性能成本,在我看来,实际上每个 mmap 都是独立且冗余填充的(可能通过从页面缓存复制,或者可能通过再次从磁盘读取)。
您能评论一下为什么我看到共享访问同一内存与映射同一文件的性能如此不同吗?
谢谢,感谢您的帮助!
我想我找到了答案,它处理页面目录。答案是肯定的,同一文件的两个映射区域将访问相同的底层页面缓存数据。但是,每个映射都需要将每个虚拟页面独立映射到物理页面——这意味着页面目录中的条目数是访问相同 RAM 的 2 倍。
基本上,每个 mmap() 都会在虚拟内存中创建一个新范围。该范围内的每一页都对应于物理内存的一页,并且该映射存储在分层页目录中——每 4KB 页有一个条目。所以一个大区域的每个mmap()都会在页目录中生成大量的条目。
我的猜测是它实际上并没有预先定义它们,这就是为什么即使对于一个巨大的文件,mmap() 也可以立即调用。但随着时间的推移,它可能必须建立这些条目,因为映射范围存在错误,这意味着随着时间的推移它会被填满。填充页面目录的额外工作可能是使用不同 mmap 的线程比共享相同 mmap 的线程慢的原因。我敢打赌内核在取消映射范围时需要擦除所有这些条目——这就是 unmmap() 如此缓慢的原因。
(还有翻译后备缓冲区,但那是每个 CPU,而且太小了,我认为这在这里并不重要。)
无论如何,这听起来像是重新映射同一区域只会增加额外的开销,因为在我看来没有任何收获。
换个方式问这个问题,你能确认当你 mmap() 一个文件时,你确实访问了页面缓存中已经存在的确切物理页面吗?
我问是因为我正在一台 1TB RAM 的 192 核机器上进行测试,在测试前预先缓存到页面缓存中的 400GB 数据文件上(通过删除缓存,然后执行文件上的 md5sum)。
最初,我有所有 192 个线程,每个线程分别映射文件,假设它们都将(基本上)返回相同的内存区域(或者可能是相同的内存区域,但以某种方式映射多次)。因此,我假设使用两个不同映射到同一个文件的两个线程都可以直接访问相同的页面。 (让我们忽略此示例的 NUMA,但显然它在更高的线程数下很重要。)
但是,在实践中,我发现当每个线程单独映射文件时,在更高的线程数下性能会变得 糟糕 。当我们删除它而只是执行一个传递到线程的 mmap(这样所有线程都直接访问同一内存区域)时,性能会显着提高 .
一切都很好,但我想找出原因。如果实际上映射一个文件只是授予对现有页面缓存的直接访问权限,那么我认为映射它多少次并不重要——它应该都去到完全相同的地方。
但鉴于存在这样的性能成本,在我看来,实际上每个 mmap 都是独立且冗余填充的(可能通过从页面缓存复制,或者可能通过再次从磁盘读取)。
您能评论一下为什么我看到共享访问同一内存与映射同一文件的性能如此不同吗?
谢谢,感谢您的帮助!
我想我找到了答案,它处理页面目录。答案是肯定的,同一文件的两个映射区域将访问相同的底层页面缓存数据。但是,每个映射都需要将每个虚拟页面独立映射到物理页面——这意味着页面目录中的条目数是访问相同 RAM 的 2 倍。
基本上,每个 mmap() 都会在虚拟内存中创建一个新范围。该范围内的每一页都对应于物理内存的一页,并且该映射存储在分层页目录中——每 4KB 页有一个条目。所以一个大区域的每个mmap()都会在页目录中生成大量的条目。
我的猜测是它实际上并没有预先定义它们,这就是为什么即使对于一个巨大的文件,mmap() 也可以立即调用。但随着时间的推移,它可能必须建立这些条目,因为映射范围存在错误,这意味着随着时间的推移它会被填满。填充页面目录的额外工作可能是使用不同 mmap 的线程比共享相同 mmap 的线程慢的原因。我敢打赌内核在取消映射范围时需要擦除所有这些条目——这就是 unmmap() 如此缓慢的原因。
(还有翻译后备缓冲区,但那是每个 CPU,而且太小了,我认为这在这里并不重要。)
无论如何,这听起来像是重新映射同一区域只会增加额外的开销,因为在我看来没有任何收获。