使用内存映射文件所消耗的时间难以理解。

10
我正在编写一个例程,使用内存映射文件来比较两个文件。如果文件太大而无法一次性映射,则会将文件分成几部分进行映射。例如,要映射一个1049MB的文件,我将其分为512MB + 512MB + 25MB。
除了一件事情外,一切都正常:无论先比较哪一个,主要部分(512MB * N)还是余数(在这个例子中为25MB),结果都是相同的。三个观察结果:
1. 先比较哪一个并不重要,结果都是相同的,不管是先比较主要部分还是先比较余数。 2. 余数部分似乎花费了更多时间在用户模式下。 3. 在VS2010 beta 1中进行分析显示,时间花费在std::_Equal()内部,但该函数大多数时间(分析器说100%)在等待I/O和其他线程。
我尝试了以下操作:
1. 将VIEW_SIZE_FACTOR更改为其他值。 2. 用成员函数替换Lambda函数。 3. 更改要测试的文件大小。 4. 更改余数执行的顺序为循环之前或之后。
结果非常一致:在余数部分和用户模式下需要更长的时间。
我怀疑这与映射大小不是映射对齐的倍数(在我的系统上为64K)有关,但不确定如何解决。
下面是该例程的完整代码和对3G文件进行测量的时间。请问有人能解释这个问题吗?谢谢。
// using memory-mapped file
template <size_t VIEW_SIZE_FACTOR>
struct is_equal_by_mmapT
{
public:
    bool operator()(const path_type& p1, const path_type& p2)
    {
        using boost::filesystem::exists;
        using boost::filesystem::file_size;

        try
        {
            if(!(exists(p1) && exists(p2))) return false;

            const size_t segment_size = mapped_file_source::alignment() * VIEW_SIZE_FACTOR;  

            // lanmbda 
            boost::function<bool(size_t, size_t)> segment_compare = 
            [&](size_t seg_size, size_t offset)->bool 
            {
                using boost::iostreams::mapped_file_source;
                boost::chrono::run_timer t;     

                mapped_file_source mf1, mf2;  

                mf1.open(p1, seg_size, offset);
                mf2.open(p2, seg_size, offset);

                if(! (mf1.is_open() && mf2.is_open())) return false;

                if(!equal (mf1.begin(), mf1.end(), mf2.begin())) return false;  

                return true;
            };

            boost::uintmax_t size = file_size(p1);
            size_t round     = size / segment_size;
            size_t remainder = size & ( segment_size - 1 );

            // compare the remainder
            if(remainder > 0)
            {
                cout << "segment size = " 
                     << remainder 
                     << " bytes for the remaining round";
                if(!segment_compare(remainder, segment_size * round)) return false;    
            }   

            //compare the main part.  take much less time, even 
            for(size_t i = 0; i < round; ++i)
            {
                cout << "segment size = " 
                     << segment_size 
                     << " bytes, round #" << i;
                if(!segment_compare(segment_size, segment_size * i))  return false;
            }
        }
        catch(std::exception& e)
        {
            cout << e.what();
            return false;
        }

        return true;                                      
    }
};

typedef is_equal_by_mmapT<(8<<10)> is_equal_by_mmap;  // 512MB  

输出:

剩余轮次的段大小为354410496字节

实际时间116.892秒,CPU时间56.201秒(48.1%),用户时间54.548秒,系统时间1.652秒

第0轮的段大小为536870912字节

实际时间72.258秒,CPU时间2.273秒(3.1%),用户时间0.320秒,系统时间1.953秒

第1轮的段大小为536870912字节

实际时间75.304秒,CPU时间1.943秒(2.6%),用户时间0.240秒,系统时间1.702秒

第2轮的段大小为536870912字节

实际时间84.328秒,CPU时间1.783秒(2.1%),用户时间0.320秒,系统时间1.462秒

第3轮的段大小为536870912字节

实际时间73.901秒,CPU时间1.702秒(2.3%),用户时间0.330秒,系统时间1.372秒


回应者建议后的更多观察

进一步将剩余部分分为主体和尾部(remainder = body + tail),其中

  • body = N * alignment(),tail < 1 * alignment()
  • body = m * alignment(),tail < 1 * alignment() + n * alignment(),其中m为偶数。
  • body = m * alignment(),tail < 1 * alignment() + n * alignment(),其中m为2的指数。
  • body = N * alignment(),tail = remainder - body。N是随机的。

总时间不变,但我可以看到时间与尾部无关,而与主体和尾部的大小有关。更大的部分需要更多的时间。时间是用户时间,对我来说最难理解。

我还查看了Procexp.exe中的页面错误。剩余部分并没有比主循环占用更多的页面错误。


更新2

我在其他工作站上进行了一些测试,似乎问题与硬件配置有关。

测试代码

// compare the remainder, alternative way
if(remainder > 0)
{
    //boost::chrono::run_timer t;       
    cout << "Remainder size = " 
         << remainder 
         << " bytes \n";

    size_t tail = (alignment_size - 1) & remainder;
    size_t body = remainder - tail;

{
    boost::chrono::run_timer t;                               
    cout << "Remainder_tail size = " << tail << " bytes";
    if(!segment_compare(tail, segment_size * round + body)) return false;
}                        
{
    boost::chrono::run_timer t;                               
    cout << "Remainder_body size = " << body << " bytes";
    if(!segment_compare(body, segment_size * round)) return false; 
}                        

}

观察:

在另外两台与我的硬件配置相同的电脑上,结果如下:

------VS2010Beta1ENU_VSTS.iso [1319909376 字节] ------

剩余大小 = 44840960 字节

剩余尾部大小 = 14336 字节

实际时间为 0.060 秒,CPU 时间为 0.040 秒(66.7%),用户时间为 0.000 秒,系统时间为 0.040 秒

剩余主体大小 = 44826624 字节

实际时间为 13.601 秒,CPU 时间为 7.731 秒(56.8%),用户时间为 7.481 秒,系统时间为 0.250 秒

段大小 = 67108864 字节,总循环次数 = 19

实际时间为 172.476 秒,CPU 时间为 4.356 秒(2.5%),用户时间为 0.731 秒,系统时间为 3.625 秒

然而,在一台不同硬件配置的电脑上运行同样的代码产生了以下结果:

------VS2010Beta1ENU_VSTS.iso [1319909376 字节] ------

剩余大小 = 44840960 字节

剩余尾部大小 = 14336 字节

实际时间为 0.013 秒,CPU 时间为 0.000 秒(0.0%),用户时间为 0.000 秒,系统时间为 0.000 秒

剩余主体大小 = 44826624 字节

实际时间为 2.468 秒,CPU 时间为 0.188 秒(7.6%),用户时间为 0.047 秒,系统时间为 0.141 秒

段大小 = 67108864 字节,总循环次数 = 19

实际时间为 65.587 秒,CPU 时间为 4.578 秒(7.0%),用户时间为 0.844 秒,系统时间为 3.734 秒

系统信息

我的工作站产生了无法理解的计时:

操作系统名称:Microsoft Windows XP Professional

操作系统版本号:5.1.2600 Service Pack 3 Build 2600

操作系统制造商:Microsoft Corporation

操作系统配置:成员工作站

操作系统类型:单处理器 Free

最初安装日期:2004-01-27, 23:08

系统运行时间:3 天,2 小时,15 分钟,46 秒

系统制造商:Dell Inc.

系统型号:OptiPlex GX520

系统类型:基于 X86 的 PC

处理器数量:已安装 1 个处理器。

                       [01]: x86 Family 15 Model 4 Stepping 3 GenuineIntel ~2992 Mhz

BIOS版本:DELL-7

Windows目录:C:\WINDOWS

系统目录:C:\WINDOWS\system32

引导设备:\Device\HarddiskVolume2

系统语言环境:zh-cn;中文(中国)

输入语言环境:zh-cn;中文(中国)

时区:(GMT+08:00)北京,重庆,香港,乌鲁木齐

物理内存总量:3,574 MB

可用物理内存:1,986 MB

虚拟内存最大值:2,048 MB

可用虚拟内存:1,916 MB

正在使用的虚拟内存:132 MB

页面文件位置:C:\pagefile.sys

网络适配器:已安装 3 张网卡。

       [01]: VMware Virtual Ethernet Adapter for VMnet1

             Connection Name: VMware Network Adapter VMnet1

             DHCP Enabled:    No

             IP address(es)

             [01]: 192.168.75.1

       [02]: VMware Virtual Ethernet Adapter for VMnet8

             Connection Name: VMware Network Adapter VMnet8

             DHCP Enabled:    No

             IP address(es)

             [01]: 192.168.230.1

       [03]: Broadcom NetXtreme Gigabit Ethernet

             Connection Name: Local Area Connection 4

             DHCP Enabled:    Yes

             DHCP Server:     10.8.0.31

             IP address(es)

             [01]: 10.8.8.154

另一台工作站提供“正确”的定时: 操作系统名称:Microsoft Windows XP Professional 操作系统版本:5.1.2600 Service Pack 3 Build 2600 操作系统制造商:Microsoft Corporation 操作系统配置:成员工作站 操作系统构建类型:多处理器免费版 原始安装日期:2009年5月18日下午2:28:18 系统运行时间:21天5小时0分钟49秒 系统制造商:Dell Inc. 系统型号:OptiPlex 755 系统类型:基于X86的个人电脑 处理器:已安装1个处理器。
        [01]: x86 Family 6 Model 15 Stepping 13 GenuineIntel ~2194 Mhz

BIOS版本:DELL - 15

Windows目录:C:\WINDOWS

系统目录:C:\WINDOWS\system32

启动设备:\Device\HarddiskVolume1

系统区域设置:zh-cn;中文(中国)

输入区域设置:en-us;英语(美国)

时区:(GMT+08:00)北京,重庆,香港,乌鲁木齐

物理内存总量:3,317 MB

可用物理内存:1,682 MB

虚拟内存:最大大小:2,048 MB

虚拟内存:可用:2,007 MB

虚拟内存:正在使用:41 MB

页面文件位置:C:\pagefile.sys

网络适配器:已安装 3 张 NIC。

       [01]: Intel(R) 82566DM-2 Gigabit Network Connection

             Connection Name: Local Area Connection

             DHCP Enabled:    Yes

             DHCP Server:     10.8.0.31

             IP address(es)

             [01]: 10.8.0.137

       [02]: VMware Virtual Ethernet Adapter for VMnet1

             Connection Name: VMware Network Adapter VMnet1

             DHCP Enabled:    Yes

             DHCP Server:     192.168.154.254

             IP address(es)

             [01]: 192.168.154.1

       [03]: VMware Virtual Ethernet Adapter for VMnet8

             Connection Name: VMware Network Adapter VMnet8

             DHCP Enabled:    Yes

             DHCP Server:     192.168.2.254

             IP address(es)

             [01]: 192.168.2.1

有任何解释理论吗?谢谢。

你尝试过使用分析器来查看你在哪里浪费时间了吗? - Ionut Anghelcovici
也许这有些离题,但如果你只是想测试字节级别的相等性,为什么要使用内存映射呢?为什么不在循环中直接读取每个文件的字符并进行比较呢?默认的缓冲机制会使IO更高效。你不需要消耗大量的内存。 - j_random_hacker
1
我想测试一下mmap在处理大文件时是否比fstream更快。实际上,随着文件大小的增加,时间节省得越多,即使包括了讨论中额外的时间。它仍然要快得多。我只是不明白关于余数部分的额外时间到底去哪里了。 - t.g.
我明白了。这两个文件的字节数大小完全相同吗? - j_random_hacker
如果你比较最后的余数会发生什么? - MSN
显示剩余7条评论
6个回答

4

这种行为看起来相当不合逻辑。我想知道如果我们尝试一些愚蠢的东西会发生什么。只要整个文件大于512MB,你可以将最后一部分与完整的512MB进行比较,而不是剩余的大小。

类似这样:

        if(remainder > 0)
        {
            cout << "segment size = " 
                 << remainder 
                 << " bytes for the remaining round";
                if (size > segment_size){
                    block_size = segment_size;
                    offset = size - segment_size;
                }
                else{
                    block_size = remainder;
                    offset = segment_size * i
                }
            if(!segment_compare(block_size, offset)) return false;    
        }   

这似乎是一个非常愚蠢的做法,因为我们会比较文件的一部分两次,但如果您的性能分析数据准确无误,它应该更快。

这不会给我们答案(至少现在不会),但如果确实更快,这意味着我们要寻找的响应在于您的程序对小数据块的处理方式。


嗨嗨,太棒了!超越内存映射的思维。 - Jonas Byström
它不起作用,因为这样违反了对齐规则“偏移量必须是分配粒度的倍数”,从而导致映射失败。我做过类似的事情,比如进一步分割余数。请查看我的更新。 - t.g.

2
你要比较的文件有多乱?你可以使用FSCTL_GET_RETRIEVAL_POINTERS获取文件在磁盘上映射的范围。我怀疑最后的25MB会有很多小的范围,这也解释了你测得的性能问题。

1
谢谢你的提示。但是,这不像是由于片段引起的,因为我已经在多个大小从几兆到几十亿字节的文件上进行了测试。 - t.g.

2
我想知道当一个段的大小不是页数的整数倍时,mmap是否会表现出奇怪的行为?也许你可以尝试通过逐步将段大小减半直到小于mapped_file_source::alignment()来处理文件的最后部分,并特殊处理那最后一点。
另外,你说你正在使用512MB块,但你的代码将大小设置为8<<10。然后它将其乘以mapped_file_source::alignment()。mapped_file_source::alignment()真的是65536吗?
我建议为了更具可移植性并减少混淆,你在代码中只需按照模板参数给定的大小并要求其是mapped_file_source::alignment()的偶数倍即可。或者让人们传入2的幂作为块大小的起始值之类的东西。将块大小作为模板参数传递,然后乘以某个奇怪的实现定义常量似乎有点奇怪。

1
关于alignment(),mapped_file::alignment()是一个静态成员函数,返回操作系统的虚拟内存分配粒度,这不是实现定义而是平台定义的。当使用mmap时,“偏移量必须是分配粒度的倍数”(http://msdn.microsoft.com/en-us/library/aa366763%28VS.85%29.aspx)。使用FACTOR(VIEW_SIZE_FACTOR)而不是SIZE(VIEW_SIZE)作为模板参数可以保证不违反此规则,并减轻用户查找粒度的工作。 - t.g.
1
此外,(8<<10) 只是为了方便人类阅读,因为通常以 KB 为单位,而 (1<<10) 表示 1K。观察前面的数字(比如 8),可以大致判断它在 MB 中有多大。并且由于它是 const 整数,所以会在编译时计算,而不是运行时。 - t.g.
我会将因子更改为1来测试您的建议。谢谢。 - t.g.
我的观点是关于大小的,我认为最好将您的段的实际大小作为模板参数,并在其不是平台特定值的倍数时出错。 - Omnifarious

2
我知道这不是你问题的确切答案,但你是否尝试绕过整个问题 - 即一次性映射整个文件?我对Win32内存管理了解不多;但在Linux上,您可以使用mmap()MAP_NORESERVE标志,因此您不需要为整个文件大小保留RAM。考虑到您只是从两个文件中读取,如果操作系统缺少RAM,它应该能够随时丢弃页面...

当文件太大时,它将无法映射。这就是为什么我将文件分成几个部分,并逐个视图进行映射的原因,这是我所知道的Win32中推荐的方法。当文件足够小时,可以一次性映射,没有性能问题。 - t.g.

1

我会在Linux或BSD上尝试它,只是出于好奇心想看看它的表现。

我对这个问题有一个非常粗略的猜测:我敢打赌Windows正在进行大量额外的检查,以确保它不会映射到文件末尾之后的区域。过去,在某些操作系统中存在安全问题,允许mmap用户查看文件系统私有数据或其他文件中的数据,因此在这里小心谨慎是OS设计人员的好习惯。所以Windows可能使用更加谨慎的“从磁盘向内核复制数据,清零未映射数据,再将数据复制到用户”而不是更快的“从磁盘向用户复制数据”。

尝试映射到文件末尾的前一点位置,不包括最后几个字节,这几个字节无法适应64K块。


我刚刚在另一台Windows电脑上尝试了一下。请查看我的帖子中的更新。 - t.g.

0

可能是病毒扫描器导致了这些奇怪的结果吗?您尝试过没有病毒扫描器吗?

敬礼,

塞巴斯蒂安


不太可能,因为我在几台安装了同样病毒扫描程序的电脑上尝试过。当结果似乎只与硬件配置有关时。我猜这是某种CPU或RAM缓存的影响,但无法确定具体原因,这就是为什么我发布了硬件配置的原因。 - t.g.
我同意,的确看起来不太可能。但在这些奇怪的情况下,我想检查所有的假设,所以我总是尝试禁用病毒扫描程序(以及防火墙,当从网络读取文件时)来确定问题是否仍然存在。问候,Sebastiaan - Sebastiaan M

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