42

我听说 C++14 在 C++ 标准库本身中引入了垃圾收集器。此功能背后的基本原理是什么?这不是 RAII 存在于 C++ 中的原因吗?

  • 标准库垃圾收集器的存在将如何影响 RAII 语义?
  • 这对我(程序员)或我编写 C++ 程序的方式有什么影响?
4

7 回答 7

32

垃圾收集和 RAII 在不同的上下文中很有用。GC 的存在不应影响您对 RAII 的使用。由于 RAII 是众所周知的,我举两个例子说明 GC 很方便。


垃圾收集对实现无锁数据结构有很大帮助。

[...] 事实证明,确定性内存释放是无锁数据结构中相当基本的问题。(来自Andrei Alexandrescu的无锁数据结构)

基本上问题是您必须确保在线程读取内存时不会释放内存。这就是 GC 派上用场的地方:它可以查看线程并仅在安全时进行释放。请阅读文章了解详情。

在这里要明确一点:这并不意味着整个世界都应该像在 Java 中那样被垃圾收集。只有相关数据应该被准确地垃圾收集。


在他的一个演讲中,Bjarne Stroustrup 还给出了一个很好的、有效的例子,GC 变得很方便。想象一个用 C/C++ 编写的应用程序,大小为 10M SLOC。该应用程序运行良好(相当无错误),但它会泄漏。您既没有资源(工时)也没有解决此问题的功能知识。源代码是一个有点凌乱的遗留代码。你做什么工作?我同意这可能是用 GC 解决问题的最简单和最便宜的方法。


正如sasha.sochka所指出的,垃圾收集器是可选的。

我个人担心的是人们会开始像在 Java 中一样使用 GC,并且会编写草率的代码和垃圾收集所有东西。(我的印象shared_ptr已经成为默认的“转到”,即使在unique_ptr或者,地狱,堆栈分配会这样做的情况下。)

于 2013-06-24T08:43:39.753 回答
11

我同意@DeadMG 的观点,即当前 C++ 标准中没有 GC,但我想添加 B. Stroustrup 的以下引用:

当(不是如果)自动垃圾收集成为 C++ 的一部分时,它将是可选的

所以 Bjarne 确信它会在未来被添加。至少 EWG(进化工作组)主席和最重要的委员会成员之一(更重要的是语言创建者)想要添加它。

除非他改变他的观点,否则我们可以期待它在未来被添加和实施。

于 2013-06-23T15:53:31.160 回答
11

有一些算法在没有 GC 的情况下是复杂/低效/不可能编写的。我怀疑这是 C++ 中 GC 的主要卖点,并且永远不会看到它被用作通用分配器。

为什么不是通用分配器?

首先,我们有 RAII,而且大多数人(包括我)似乎都相信这是一种更好的资源管理方法。我们喜欢确定性,因为它使编写健壮、无泄漏的代码变得更加简单,并使性能可预测。

其次,您需要对如何使用内存进行一些非常非 C++ 的限制。例如,您至少需要一个可访问的、未混淆的指针。混淆指针,在常见的树容器库中很流行(使用对齐保证的低位作为颜色标志),GC 无法识别。

与此相关的是,如果您支持任意数量的混淆指针,那么使现代 GC 如此可用的东西将很难应用于 C++ 。分代碎片整理 GC 真的很酷,因为分配非常便宜(本质上只是增加一个指针)并且最终你的分配被压缩成更小的东西并改善了局部性。为此,对象需要是可移动的。

为了使对象安全地移动,GC 需要能够更新所有指向它的指针。它将无法找到混淆的那些。这可以适应,但不会很漂亮(可能是一种gc_pin类型或类似的类型,像 current 一样std::lock_guard使用,在您需要原始指针时使用)。可用性将不复存在。

如果不使东西可移动,GC 将比您在其他地方习惯的要慢得多且可扩展性更低。

可用性原因(资源管理)和效率原因(快速、可移动的分配)不碍事,GC 还有什么好处?当然不是通用的。输入无锁算法。

为什么要无锁?

无锁算法的工作原理是让争用中的操作暂时与数据结构“不同步”,并在稍后的步骤中检测/纠正这一点。这样做的一个影响是,在竞争中的内存可能在被删除后被访问。例如,如果您有多个线程竞争从 LIFO 中弹出一个节点,则一个线程可能会在另一个线程意识到该节点已被占用之前弹出并删除该节点:

线程 A:

  • 获取指向根节点的指针。
  • 从根节点获取指向下一个节点的指针。
  • 暂停

线程 B:

  • 获取指向根节点的指针。
  • 暂停

线程 A:

  • 弹出节点。(如果根节点指针自读取后未更改,则将根节点指针替换为下一个节点指针。)
  • 删除节点。
  • 暂停

线程 B:

  • 从我们的根节点指针获取指向下一个节点的指针,它现在“不同步”并且刚刚被删除,所以我们崩溃了。

使用 GC,您可以避免从未提交的内存中读取的可能性,因为当线程 B 引用它时,该节点永远不会被删除。有一些方法可以解决这个问题,例如危险指针或在 Windows 上捕获 SEH 异常,但这些方法会严重影响性能。GC 往往是这里的最佳解决方案。

于 2013-06-23T17:20:30.990 回答
6

没有,因为没有。C++ 唯一的 GC 功能是在 C++11 中引入的,它们只是标记内存,不需要收集器。在 C++14 中也不会有。

在地狱里,收藏家不可能通过委员会,这是我的看法。

于 2013-06-23T15:01:38.067 回答
4

GC具有以下优点:

  1. 它可以在没有程序员帮助的情况下处理循环引用(使用 RAII 样式,您必须使用 weak_ptr 来打破循环)。因此,如果使用不当,RAII 风格的应用程序仍然会“泄漏”。
  2. 为给定对象创建/销毁大量 shared_ptr 可能会很昂贵,因为引用计数递增/递减是原子操作。在多线程应用程序中,包含引用计数的内存位置将是“热点”位置,给内存子系统带来很大压力。GC 不容易出现这个特定问题,因为它使用可达集而不是引用计数。

我并不是说 GC 是最好/好的选择。我只是说它有不同的特点。在某些情况下,这可能是一个优势。

于 2013-06-24T10:38:37.373 回答
4

到目前为止,没有一个答案涉及向语言添加垃圾收集的最重要的好处:在没有语言支持的垃圾收集的情况下,几乎不可能保证在对它的引用存在时不会破坏任何对象。更糟糕的是,如果确实发生了这样的事情,几乎不可能保证以后尝试使用该引用不会最终操纵其他一些随机对象。

尽管 RAII 可以比垃圾收集器更好地管理多种对象的生命周期,但让 GC 管理几乎所有对象具有相当大的价值,包括那些生命周期由 RAII 控制的对象。对象的析构函数应该杀死对象并使其无用,但将尸体留在 GC 后面。因此,对对象的任何引用都将成为对尸体的引用,并且在它(引用)完全不存在之前将保持为一个。只有当所有对尸体的提及都不再存在时,尸体本身才会这样做。

虽然有一些方法可以在没有固有语言支持的情况下实现垃圾收集器,但这种实现要么要求在创建或销毁引用时通知 GC(增加相当大的麻烦和开销),要么冒着 GC 不知道引用的风险about 可能存在于未引用的对象中。对 GC 的编译器支持消除了这两个问题。

于 2013-07-23T20:27:05.663 回答
1

定义:

RCB GC:基于引用计数的 GC。

MSB GC:基于标记扫描的 GC。

快速回答:

MSB GC 应该添加到 C++ 标准中,因为在某些情况下它比 RCB GC 更方便。

两个说明性示例:

考虑一个初始大小很小的全局缓冲区,任何线程都可以动态扩大其大小并保持旧内容可供其他线程访问。

实施 1(MSB GC 版本):

int*   g_buf = 0;
size_t g_current_buf_size = 1024;

void InitializeGlobalBuffer()
{
    g_buf = gcnew int[g_current_buf_size];
}

int GetValueFromGlobalBuffer(size_t index)
{
    return g_buf[index];
}

void EnlargeGlobalBufferSize(size_t new_size)
{
    if (new_size > g_current_buf_size)
    {
        auto tmp_buf = gcnew int[new_size];
        memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));       
        std::swap(tmp_buf, g_buf); 
    }   
}

实施 2(RCB GC 版本):

std::shared_ptr<int> g_buf;
size_t g_current_buf_size = 1024;

std::shared_ptr<int> NewBuffer(size_t size)
{
    return std::shared_ptr<int>(new int[size], []( int *p ) { delete[] p; });
}

void InitializeGlobalBuffer()
{
    g_buf = NewBuffer(g_current_buf_size);
}

int GetValueFromGlobalBuffer(size_t index)
{
    return g_buf[index];
}

void EnlargeGlobalBufferSize(size_t new_size)
{
    if (new_size > g_current_buf_size)
    {
        auto tmp_buf = NewBuffer(new_size);
        memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));       
        std::swap(tmp_buf, g_buf); 

        //
        // Now tmp_buf owns the old g_buf, when tmp_buf is destructed,
        // the old g_buf will also be deleted. 
        //      
    }   
}

请注意:

打完电话std::swap(tmp_buf, g_buf);tmp_buf自己老了g_buf。当tmp_buf被销毁时,旧的g_buf也会被删除。

如果另一个线程正在调用GetValueFromGlobalBuffer(index);从 old 获取值g_buf,那么将发生种族危险!!!

因此,虽然实现 2 看起来和实现 1 一样优雅,但它不起作用!

如果我们想让实现 2 正常工作,我们必须添加某种锁机制;那么它不仅会比实现 1 更慢,而且更不优雅。

结论:

最好将 MSB GC 作为可选功能纳入 C++ 标准。

于 2013-09-15T06:47:07.700 回答