【问题标题】:How to use dynamic data structures like std::vector and prevent paging ?如何使用像 std::vector 这样的动态数据结构并防止分页?
【发布时间】:2014-07-04 08:56:23
【问题描述】:

跟进这个问题Vector push_back only if enough memory is available,我试过了 在更一般的意义上重新表述这个问题。

考虑这个片段:

vector<double> v1;
cout << "pushing back ..." << endl;
while (true) {
    try {
        v1.push_back(0.0);
    } catch (bad_alloc& ba){
        cout << "bad_alloc caught: " << ba.what() << endl;
        break;
    }
}

关于上述代码片段,下列哪些说法是正确的?

1) 最终会到达 catch 块

2) 你无法事先确定是否有足够的内存让push_back 不抛出bad_alloc

3) catch 块中涉及内存分配的每个操作都可能失败,因为没有剩余内存了

我做的第一件事是在 Windows 上运行这个程序,这导致观察到在任何分页发生之前,bad_alloc 被抛出,因为显然已经超出了每个进程的内存量。这一观察导致了下一个陈述:

4) 在大多数操作系统上,bad_alloc 会在分页发生之前被抛出,但没有事先确定的方法。

经过一番研究,我对上述陈述提出了以下想法:

A1) 没错,catch 块将会到达,但可能不会在操作系统由于分页而执行密集的 I/O 操作之前。

A2) 没错,至少不是以独立于操作系统的方式

A3) 没错,您必须预先分配内存,以便对目前收集的向量中的数据做一些有用的事情(例如,如果您觉得这很有用,请自行进行一些分页)

A4) 没错,这取决于多个特定于操作系统的参数,例如每个进程的最大 RAM 量、进程优先级、操作系统进程调度程序的策略等...

我不确定 A1-A4 是否正确,因此是我的问题,但如果是,下面是下一个陈述:

5) 如果你需要编写一些算法并确保不会出现分页,请不要使用像std::vector 这样的动态数据结构。而是使用 Array 并确保它将使用特定于操作系统的函数(例如 mlockall (Unix))留在内存中

如果 5) 为真,它会导致最后一条语句:

6) 没有独立于操作系统的方法来编写不会导致分页的程序。

提前感谢大家分享您对上述声明的看法。

【问题讨论】:

    标签: c++ memory memory-management data-structures out-of-memory


    【解决方案1】:

    如果您的程序必须在 Windows/Unix/OS X 上运行,请制作包装函数:

    bool lockMemoryRegion( void *addr, size_t size )
    {
    #ifdef WIN32
        return VirtualLock( addr, size ) != 0;
    #else
        return mlock( addr, size ) == 0;
    #endif
    }
    
    bool unlockMemoryRegion( void *addr, size_t size )
    {
    #ifdef WIN32
        return VirtualUnlock( addr, size ) != 0;
    #else
        return munlock( addr, size ) == 0;
    #endif
    }
    

    那么如果你需要锁定std::vector使用的内存:

    std::vector<int> v( 1000 );
    lockMemoryRegion( v.data(), v.capacity() * sizeof (int) );
    

    仅在您确实应该使用时才使用内存锁。将页面锁定在内存中可能会减少可用 RAM 并强制系统将其他关键页面换出到页面文件,从而降低系统性能。

    【讨论】:

      【解决方案2】:

      一个乱七八糟的问题。您仍然需要了解您真正感兴趣的操作系统上的现代内存分配。我建议您进行一些系统的背景阅读,因为对您的大杂烩问题的答案不一定会给您正确的大图。

      1) 最终会到达 catch 块

      2) 无法事先确定是否有足够的内存让 push_back 不抛出 bad_alloc

      3) catch 块中涉及内存分配的每个操作都可能失败,因为没有剩余内存了

      这些都不一定是真的......操作系统可能会分配虚拟地址空间,然后在进程被访问并且操作系统找不到物理内存来支持它时终止进程。此外,内存不足的进程杀手可能会认为您推得太远并终止您或任何其他非关键进程。

      对于 3),标准明确指出,实现可以使用单独的内存区域将抛出的对象传递给将处理它的 catch 语句 - 毕竟,放在同一个堆栈上是没有意义的您在异常处理期间正在展开。因此,与动态内存分配(使用 new 或 malloc)相比,该内存分配的问题要少得多,但在极少数情况下仍可能分页并因此导致进程终止。内部抛出的对象进行动态内存分配仍然很危险(例如,将描述存储在字符串或 istringstream 数据成员中)。类似地,catch 语句可能会为变量、表达式求值、函数调用等分配堆栈空间——它们也可能导致失败,但没有 new/malloc 危险。

      4) 在大多数操作系统上,bad_alloc 会在分页发生之前被抛出,但没有预先确定的方法。

      当然不是——那么分页的意义何在?

      A1) 没错,catch 块将会到达,但可能不会在操作系统由于分页而执行密集的 I/O 操作之前。

      如果碰巧正在使用交换磁盘,那么是的,您应该在内存不足情况之前进行分页,但这可能不会表现为异常。

      A2) 没错,至少不是以独立于操作系统的方式

      不……一开始就不是真的。

      A3) 没错,您必须预先分配内存,以便对目前收集的向量中的数据做一些有用的事情(例如,如果您觉得这很有用,请自行进行一些分页)

      您不必预先分配任何东西...这将通过构造函数参数或调整大小来完成...这是可选的,但可能允许您处理更多数据而不会遇到内存不足的情况,因为需要较少用于在数据移动到更大的内存块时暂时增加内存使用量。所有这一切都与您是否“做一些有用的事情”无关,我不知道您通过“自己做一些分页”来想象什么。如果您访问矢量元素,它们可能必须被分页。如果您有一段时间没有使用它们,它们可能会被分页。操作系统缓存算法决定了这一点。您可能至少想了解这种类型的简单算法,例如最近最少使用 (LRU)。

      A4) 没错,这取决于多个特定于操作系统的参数,例如每个进程的最大 RAM 量、进程优先级、操作系统进程调度程序的策略等...

      您可以设置每个进程的内存分配限制,但是您认为在超出该限制之前不会发生分页的概念是错误的。分页可能发生在进程的任何部分——动态分配、堆栈、可执行映像、静态数据、线程特定数据等——只要操作系统看到它有一段时间没有被使用并想要更多的物理内存紧迫的目的。

      您的问题清楚地表明,以下假设是以早期假设的真实性为条件的,但我会尽快解决它们,因为它们无论如何都具有真实性和/或相关性......

      5) 如果您需要编写一些算法并确保不会出现分页,请不要使用像 std::vector 这样的动态数据结构。而是使用一个数组,并确保它会使用特定于操作系统的函数(例如 mlockall (Unix))留在内存中

      您使用哪种类型的数据类型/容器无关紧要 - 操作系统甚至不知道或关心您将其授予进程的内存的不同部分用于什么用途。因此,这样的函数可以应用于数组或动态分配的内存——例如——如果你填充了一个向量,那么你可以使用 .data() 来获取一个指向存储数据的实际内存区域的指针,然后将其锁定到物理内存。当然,如果你做了一些事情来强制向量找到不同的内存区域(例如添加超出容量()的元素),那么它仍然会寻找更多的内存并且将一些已删除的内存区域锁定在物理内存中可能会对你的进程产生不利影响,并且系统性能。

      如果 5) 为真,它会导致最后一条语句:

      6) 没有独立于操作系统的方法来编写不会导致分页的程序。

      不,没有。分页意味着对经历它的进程是透明的,进程很少需要避免它。

      【讨论】:

        【解决方案3】:

        1、2、3都是正确的,假设2指的是可移植的方式。您可以根据特定于操作系统的进程内存使用报告功能做出一个不错的猜测。它们不是那么准确,也不是便携式的,但它们确实提供了一个相当好的猜测。

        至于 4,那不是真的。它是物理内存量与进程的虚拟地址空间大小相比的函数。 x64 的地址空间比物理内存大得多。 x86 现在要小得多,但几年前使用 2GB 或 1GB RAM 的旧机器,它会更大。

        如果你需要写一些算法并确保不会有 分页,不要使用像 std::vector 这样的动态数据结构。反而 使用 Array 并确保它将使用特定于操作系统的内存保留在内存中 函数,例如 mlockall (Unix)

        废话。你可以保留向量来分配你需要的所有内存,然后调用mlock

        但是绝对没有独立于操作系统的方法来编写不会导致分页的程序。分页是 C++ 使用的平面内存模型的一个实现细节,当然没有与这个实现细节相关的标准功能,也永远不会有。

        【讨论】:

        • 我同意,操作系统会为您处理内存分配,并决定在不告诉您的情况下进行分页,这完全是操作系统特定的。
        • 1) 是“最终,将到达 catch 块”,而您说“1、2 和 3 都是正确的”......正如我在回答中解释的那样,这是错误的。跨度>
        • 这样的实现是不合格的。
        • @Puppy:可能,但 Angle.Bracket 显然感兴趣的是可以采取哪些措施(如果有的话)以实际的方式处理这种情况,而不是标准是否掩盖了它。
        • 实现细节和直接不符合项之间存在很大差异。
        【解决方案4】:

        1) 最终会到达 catch 块

        这个“最终”并不意味着“当你分配最多字节时”,而是更多(虚拟内存映射 - 如果存在 - 也必须耗尽)。

        大约十年前,我看到一个 linux 进程调度程序有杀死行为不端的应用程序的习惯。我认为这个应用程序符合条件(即它可能在到达 catch 块之前被操作系统终止)。

        3) catch 块中涉及内存分配的每个操作都可能失败,因为没有剩余内存了

        理论上是正确的,实际上可能是错误的。向量将继续分配越来越大的连续块。这样一来,它可能不再能够分配 LARGE 块,但之前的较小分配已被释放。您可能会在 catch 块中获得一些可用内存。

        4) 在大多数操作系统上,bad_alloc 会在分页发生之前被抛出,但没有特定的方法可以提前告知。

        既然没有办法事先知道,唯一现实的办法就是测量它。

        5) 如果您需要编写一些算法并确保不会出现分页,请不要使用像 std::vector 这样的动态数据结构。而是使用一个数组,并确保它会使用特定于操作系统的函数(例如 mlockall (Unix))留在内存中

        这是不正确的。向量是分配的连续内存块上的安全包装器。您也可以使用向量和内存锁定函数。

        对于 (6):分页取决于硬件、操作系统和应用程序(您可以在两个不同的系统上运行相同的应用程序并对其进行不同的分页)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-01-23
          • 2015-09-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-09-21
          相关资源
          最近更新 更多