【问题标题】:Do Standard Library (STL) Containers support a form of nothrow allocation?标准库 (STL) 容器是否支持一种 nothrow 分配形式?
【发布时间】:2011-06-17 03:28:02
【问题描述】:

new 运算符(或对于 POD,malloc/calloc)在分配大块内存时支持一种简单而有效的失败形式。

假设我们有这个:

const size_t sz = GetPotentiallyLargeBufferSize(); // 1M - 1000M
T* p = new (nothrow) T[sz];
if(!p) {
  return sorry_not_enough_mem_would_you_like_to_try_again;
}
...

std::containers 是否有任何这样的构造,或者我是否总是必须处理 std::vector 和朋友的(预期的!!)异常?


可能有一种方法可以编写一个自定义分配器来预分配内存,然后将此自定义分配器传递给向量,这样只要向量不要求比您预先放入分配器中更多的内存,它不会失败?


事后考虑:除了正常的储备函数之外,真正需要的是成员函数bool std::vector::reserve(std::nothrow) {...}。但是,由于只有在分配器也被扩展以允许 nothrow 分配时,这才有意义,所以它不会发生。似乎 (nothrow) new 毕竟对某些东西有好处:-)


编辑:至于为什么我什至在问这个:

我在调试时想到了这个问题(调试器的第一次机会/第二次机会异常处理):如果我将调试器设置为第一次机会捕获任何 bad_alloc,因为我正在测试低内存条件,它会如果它还捕获了那些已经在代码中很好预期和处理的 bad_alloc 异常,那会很烦人。这不是/不是一个真正的大问题,但我只是突然想到,异常是针对特殊情况的,而我已经预料到代码中每个奇怪的调用都会发生的事情并不例外。

如果new (nothrow) 有它的合法用途,那么向量-nothrow-reserve 也将有。

【问题讨论】:

  • 写一个try...catch 结构没什么大不了的。如果你有很多,你可以写一个包装函数template<typename T> T* MyAlloc (size_t),里面有异常处理。
  • 有时这很重要。
  • 即使您分配千兆字节并接近 OOM?我对此表示怀疑。
  • @Martin:你能解释一下为什么你不希望抛出任何异常吗?这对我来说毫无意义。
  • @TonyK:你问我为什么要这个是完全正确的。我故意不考虑这个问题,因为那样我会得到质疑我的动机的答案,而不是专注于实际问题的东西。 :-)(我将添加一个编辑解释更多内容。)

标签: c++ stl standard-library


【解决方案1】:

默认情况下,标准 STL 容器类在底层使用 std::allocator 类进行分配,这就是为什么如果没有可用内存,它们可以抛出 std::bad_alloc。有趣的是,关于分配器的 C++ ISO 规范声明任何分配器类型的返回值必须是指向能够容纳一定数量元素的内存块的指针,这会自动阻止您构建自定义分配器这可能会使用newnothrow 版本来发生这些静默分配失败。但是,您可以构建一个自定义分配器,如果没有可用内存则终止程序,因为那时返回的内存在没有剩余内存时是有效的,这是空洞的。 :-)

简而言之,标准容器默认抛出异常,而您尝试使用自定义分配器自定义它们以防止抛出异常的任何方式都不符合规范。

【讨论】:

  • 大多数调用分配器的方法除了通过异常之外没有其他方法来报告错误。
【解决方案2】:

我们经常听到“我不想使用异常,因为它们效率低下”。

除非您指的是“嵌入式”环境,您希望关闭所有运行时类型信息,否则如果以适当的方式抛出异常,您不应该过分担心异常的效率低下。内存不足是这些适当的方法之一。

vector 的部分约定是如果它不能分配就会抛出。如果您编写一个返回 NULL 的自定义分配器,情况会更糟,因为它会导致未定义的行为。

如果你必须使用一个分配器,它将首先尝试一个失败的分配回调,如果一个可用,只有当你仍然无法分配到 throw,但你仍然必须以异常结束时。

我可以给你一个提示:如果你真的要分配如此大量的数据,那么 vector 可能是错误的类,你应该使用 std::deque 代替。为什么?因为双端队列不需要连续的内存块,但仍然是恒定时间查找。优点有两个:

    • 分配失败的频率会降低。因为您不需要连续的块,所以您很可能拥有可用的内存,尽管不在单个块中。
    • 没有重新分配,只是更多的分配。重新分配很昂贵,因为它需要移动所有对象。当您处于高容量模式时,这可能是一个非常及时的操作。

当我过去在这样的系统上工作时,我们发现由于上述原因 1,我们实际上可以使用 deque 存储的数据量是使用 vector 的 4 倍以上,并且由于原因 2 它更快。

我们做的其他事情是分配一个 2MB 的备用缓冲区,当我们捕获到 bad_alloc 时,我们释放了缓冲区,然后无论如何都扔了,以表明我们已经达到了容量。但是现在有了 2MB 的备用空间,我们至少知道我们有足够的内存来执行小型操作,将数据从内存移动到临时磁盘存储。

因此我们有时可以捕获 bad_alloc 并采取适当的措施来保持一致的状态,这是异常的目的,而不是假设内存耗尽总是致命的,除了终止程序(甚至更糟糕的是,调用未定义的行为)。

【讨论】:

  • 感谢有关deque 的提示——有趣的观察,可能值得一试。 (不过,正如我的原始代码所暗示的,如果有人需要 >500MB 的缓冲区,那么无论您如何对其进行分段,大部分时间都可能会失败。)
  • @Martin:为什么 500MG deque 可能会失败? deque 的重点在于它不是一个单独的缓冲区,而是多个分配。例如,如果您在一台甚至模糊不清的现代 PC 上运行,那么 500MB 的小块应该可以轻松分配。在 32 位操作系统/进程上,同时进行超过 3 个这样的分配可能会开始变得棘手。
  • @Steve - 是的,32 位进程。不是物理 RAM 耗尽,而是进程的地址空间变得“满”。我在deque 中看到的一个问题是,您真的不能确定它将如何拆分您的缓冲区。因此,您可能会遇到它运行良好的情况和其他(碎片)情况,它根本不起作用。本身可能是一个有趣的问题。
  • @Martin:它依赖于实现,但deque 的实现我见过使用非常小的块,不到 1kb。它应该能够使用几乎任何可用的地址空间。
  • 异常问题IMO与低效率无关:问题是在项目的压力下,为了避免一直阅读文档,从正常的程序员代码中落下的第一件事就是异常管理。因此,您的应用程序 100% 正在开发中,并在客户端随机崩溃。 IMO 错误机制应与参数一样强制:因此程序员知道并将对其进行管理。所以我使用 nothrow,因为检查指针 "if(!ptr)" 比编写完整的 try-catch 块更简单。
【解决方案3】:

标准容器为此使用异常,您无法绕过它,只能在知道它会成功时才尝试分配。你不能以便携的方式做到这一点,因为实现通常会过度分配一个未指定的数量。如果您必须在编译器中禁用异常,那么您对容器的操作将受到限制。

关于“简单高效”,我认为std 容器相当简单且相当高效:

T* p = new (nothrow) T[sz];
if(!p) {
    return sorry_not_enough_mem_would_you_like_to_try_again;
}
... more code that doesn't throw ...
delete[] p;

try {
    std::vector<T> p(sz);
    ... more code that doesn't throw ...
} catch (std::bad_alloc) {
    return sorry_not_enough_mem_would_you_like_to_try_again;
}

代码行数相同。如果它在故障情况下出现效率问题,那么您的程序必须每秒失败数十万次,在这种情况下,我稍微质疑程序设计。我还想知道在什么情况下,与new 可能确定它不能满足请求的系统调用的成本相比,抛出和捕获异常的成本是显着的。

但更好的是,编写 API 来使用异常如何:

std::vector<T> p(sz);
... more code that doesn't throw ...

比您的原始代码短四行,当前必须处理“sorry_not_enough_mem_would_you_like_to_try_again”的调用者可以改为处理异常。如果此错误代码通过几层调用者向上传递,则您可以在每一层保存四行。 C++ 也有例外,对于几乎所有目的,您不妨接受这一点并相应地编写代码。

关于“(预期!!)” - 有时您知道如何处理错误情况。在这种情况下要做的是捕获异常。这就是异常应该如何工作的方式。如果抛出异常的代码以某种方式知道没有任何人可以捕获它,那么它可以终止程序。

【讨论】:

  • 关于您的建议只是让 API 传播异常:我认为当您已经知道关键大小的哪个分配可能失败时传播 bad_alloc 是一个非常糟糕的主意。无论代码将要捕获它,它在哪里或哪个分配失败都不会更明智,如果您已经知道您正在执行临界大小分配,那么我认为捕获 bad_alloc 并重新抛出具有更多信息/可以的东西真的更好以更具体的方式被抓住和处理。
  • @Martin:嗯,你知道你的具体用例,而我不知道。如果代码需要内存,那么我看不出它对 1GB 内存的需求与需要 1 字节(动态分配)内存的代码有何本质不同。通常,“重试”不是对内存不足的良好响应,但是如果在您的特定情况下,有一些分配可能只是重试可能有效,而另一些分配则不会,那么一定要区分两个。
猜你喜欢
  • 1970-01-01
  • 2020-03-08
  • 2023-01-24
  • 2016-10-01
  • 1970-01-01
  • 2012-11-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多