使用 PMR 分配器以获取更好性能

标题:Basic usage of PMRs for better performance

日期:2023/02/17

作者:Marek Krajewski

链接:https://www.youtube.com/watch?v=I6nDF9IEsRE

注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。


大家好,下午好,欢迎来到我关于PMR和性能的演讲。那么,这里都有谁呢?我是谁?我是一名自由职业者,主要从事C++编程。我从网络协议开始,然后做了很多客户端服务器、REST相关的工作,现在转到了Qt和用户界面。我一直在Linux、Windows上工作,最近几年更多地转向嵌入式领域,而我似乎总是关心性能。那么,这次演讲讲些什么?首先是介绍,我们现在在做什么,然后我们会快速看一下分配器,再看STL中的分配器支持,然后看这些神秘的PMR,没人知道它是什么,接着是如何使用它们,然后我会稍微离题讲一下系统分配器的主题,之后是提问时间和简短总结。

什么是分配器?嗯,当今计算机硬件中最慢的部分不是处理器,而是内存总线。因此,我们必须关心CPU缓存,关心“热度”,关心数据局部性。此外,分配内存是昂贵的,因为涉及到系统调用,我们必须在线程之间同步,我们必须关注碎片化,避免碎片化。而分配器就是试图为我们解决这些问题的软件片段。

有两种内存分配器:系统内存分配器和自定义内存分配器。系统内存分配器由标准库、C库提供给你。它们也可以在程序启动时在运行时注入到程序中。它们通过mallocfree函数调用,并且一旦设置,就对整个程序全局有效。嗯,我必须说,它们非常好,但它们也必须满足非常广泛的应用需求。因此,默认设置通常有点保守和次优,所以自定义内存分配器还有一些调整空间。

那么自定义内存分配器,我们必须在程序中显式地包含它们,以编程方式,但它们只能应用于程序的某些特定部分。它们易于定制,不仅可以通过提供更快的分配/释放来提升性能,还可以简化代码。它们可以提供数据的特殊放置,可以提供内存分配的分析等等。

那么,内存分配器的调整空间在哪里?有两个可能性可以提高性能:

  1. 通过更快的分配、分配、分配、分配和释放调用。

  2. 通过改善内存访问和所分配数据的内存布局。

哪一个占主导地位,取决于程序。对于短时间运行的程序,更快的分配调用将占主导;对于长时间运行的程序,当然是内存布局。

那么C++如何支持自定义分配器?我们可以覆盖整个程序的全局newdelete分配器,这很粗糙。我们可以覆盖单个类的分配器。那么C++是怎么做的呢?然后我们可以将分配器支持与STL容器一起使用。

那么STL中的分配器API是什么?首先,我们有一些需要的类型定义。然后是rebind函数,这对我们来说不那么有趣。所以分配器定义了allocatedeallocate给你使用。我们可以在新的中使用并保护实际的硬件,三种不同的分配器调用……。然后这个实现。但事实上,这项工作会有一点……为你的对象提供内存,然后构造和销毁。construct会获取一块内存并调用构造函数,destroy会获取一块内存并在那块内存上调用析构函数——这有点像placement new。我们还有必需的比较运算符。

分配器和容器之间是如何交互的?STL容器在容器类型中包含一个模板参数。然后容器会接受分配器的指针和引用定义。这类东西,并在内部使用它。然后容器必须将分配器实例作为成员保存。这是个问题吗?当我们有很多……时,这是浪费空间吗?我们可以看一下从Visual Studio实现中拿来的vector代码,然后我们的分配器,最后一个参数,将被转发给一个压缩对。压缩对。有人知道那是什么吗?压缩对?Boost压缩对?ABO?是的?空基类优化。空基类优化。所以如果分配器是空的,它会被优化掉。不是空的。无状态的。无状态的。无状态的意思是它没有状态,它没有成员。而在C++98中,所有分配器都是无状态的。所以如果我们有很多数据,我们有很多数据……。

那么在C++11中改变了什么?嗯,有几件事。首先,我们得到了对有状态分配器的支持,然后我们得到了对花式指针和作用域分配器的支持。这意味着分配器转发被修复了。所以我们看到在C++98中有一大堆东西是错误的。设计还没准备好。但我们不会讨论这些修复,因为现在每个人都用C++20了。谁在工作中用C++20?一个,两个,三个,四个,五个人。17呢?啊,是的。14呢?14?谁用Qt容器?没人。好的。很好。因为只有一个人。因为Qt容器不支持分配器。所以,好吧。但,所以我们将跳过这个C++11的内容,转到C++17,那里有一个未完成的事项。这就是这个分配器类型依赖。容器类型依赖于分配器。它是容器类型的一部分。这是个问题还是个麻烦?问题。所以,如果我们定义一个函数,这个函数接受一个vector作为参数。然后我们有两个vector。一个是正常的标准vector,另一个使用不同的分配器。那么编译器会接受其中一个vector,而拒绝另一个。因为类型不匹配。所以,理论上,我们应该做的是,在分配器类上为每个使用容器的函数都模板化。然后我们有两个vector。嗯,没人会那么做。因为这太麻烦了,而且不可扩展。我们有很多函数,而且不应该所有东西都模板化。这只是语法噪音。所以,是的。是的,在C++11中曾有一些尝试来解决这个问题。像functionpromiseshared_ptr这样的新函数、新类获得了对类型擦除分配器的支持。但之后就停止了。STL容器保持不变。

那么下一个问题是,PMR解决了什么问题?你现在可能已经知道了。或者至少感觉到了。是的,就是这个类型签名问题。我们不想修复它。我们不想修复它。我们在C++17修复了它。解决方案是,我们将一个新分配器的基类包装在符合STL规范的分配器包装器中。这样我们就可以使用未更改的容器。然后我们总是在容器中使用这个包装器。而单个包装器可以在内部使用不同的PMR。多态地。问题是,这是什么设计模式?不是吗?桥接。不是?桥接。桥接模式。好的。

那么什么是PMR?Victor说它是“穷人的Rust”。法国人会说这是“person, it’s person mobilité réduite”。(译者注:这是一个法语双关语,person mobilité réduite意思是行动不便人士,其缩写PMR恰好和polymorphic memory resource缩写相同)。但它是一个缩写,代表多态内存资源。嗯,为什么不叫多态分配器?所以我们必须看看这些类型的设计。我们有我们旧的STD容器。然后我们,我们用它新的多态分配器对它进行模板化。这是我们的包装器类。这个包装器类将包含一个指向PMR内存资源基类的指针。如果你想提供一个新的内存分配器,我们只需实现一个新类型的内存资源。新的子类。所以,因为多态分配器本身变化不大。它就保持原样。所以整个库是以多态内存资源命名的。简称PMR。

所以,这个PMR在,这是右边,对吗?在右边展示了PMR的接口。所以它是分配器中处理内存分配的部分。allocatedeallocate。加上is_equal,我们需要这个来区分无状态与有状态分配器。

那么我们如何使用它?那个构造。好的。我们只需在这里定义一些,一些内存缓冲区。我们把它交给我写的my_buffer_resource。然后我们将把这个资源交给我们的vector。怎么给?通过获取它的地址。这是可行的,因为在多态分配器的接口中,有一个隐式转换。我们可以,我们将获取一个指向内存资源的指针,并将其包装在这个包装器类中。那么PMR vector of string是什么?它只是一个类型定义,就像我们在下一行代码中看到的这个。它是vector的一个模板类型定义。接受一个PMR多态分配器类型。就是这样。

你立刻就能在这里看到两个问题。首先,容器不应该比它的分配器寿命长。因为我们使用的是裸指针。第二个问题。PMR不会转发给标准string实例。这个接受string的PMR vector不会将其PMR转发给内部的string。这个接受string的PMR vector不会将其PMR转发给内部的string。所以,这是一个标准string。这是一个标准string。这个接受string的PMR vector不会将其PMR转发给内部的string。所以,哎呀,让我看看。例如,我们这里有一个这样的PMR vector of string,然后我们push_back三个小的vector字符串,它们会使用小字符串优化,并且会保持在PMR内。但当小字符串优化无法使用时,那么就会调用new运算符。

那么正确的做法是什么?好的,我们必须给它不是裸的string,而是PMR string类型。然后这个机制将提供将PMR转发给string实例的功能。有问题吗?

如果我们给PMR的内存耗尽会发生什么?我们可以为我们的基础PMR设置一个上游分配器,上游PMR,如果这个内存也耗尽了,那么将使用默认内存资源,它就是new_delete_resourcenew_delete_resource将回退到全局的newdelete运算符。当然我们可以改变这一点。我们可以设置一个新的全局默认资源。

那么关于我们自己实现PMR类呢?嗯,对于内存资源来说,这并不复杂,因为我们必须实现处理内存管理的三个方法。但对于像PMR string这样的分配器感知类,它们会从其容器接受PMR,这就更复杂了。在这次演讲中我们不会讨论它。因此,我们将看看标准库提供给我们的PMR。在C++17的标准库中,我们有一些预打包的PMR类型。所以这里的情况不像协程那样糟糕。

首先,我们有两个基本的PMR,它们将是我们的主力。我们将在后面讨论它们。然后是两个特殊的PMR,即我们已经见过的new_delete_resource。以及一个什么都不分配的null_memory_resource。它有什么用?我们也会讨论它。作为支持的第三部分,在std::pmr命名空间中有许多容器。比如pmr::vectorpmr::listpmr::string,所以我们可以直接拿来用。这些是常规的、普通STL容器的非常廉价的类型定义,只是设置了默认类型为这种多态分配器类型。好的,这就是我们库支持的这三个部分。

那么让我们看看我们的第一个主力。这个monotonic_buffer_resource。这是个奇怪的名字,不是吗?但单调缓冲区的实现非常简单。我们只有一大块内存,一块内存,单调缓冲区被设计用于非常快速的内存分配。所以它基本上只是……只会递增一个内部指针。哦,整个分配就完成了。然而,内存只有在单调缓冲区在其析构函数中超出作用域时才会被释放。如果我们只释放一个由单调缓冲区分配的对象,deallocate操作什么也不做。它什么都不做。所以,我们在这里看到三个由单调缓冲区分配的对象,中间的对象被释放了,它的析构函数被调用了,但内存仍然被浪费着。清楚了吗?清楚了吗?好的。它被浪费着。“单调”在数学中意味着总是增长。所以,这是个贴切的名字。

构造函数很简单。我们可以指定上游内存、初始大小,并明确地提供一些我们自己分配的缓冲区供它使用。

那么,我们如何使用它?就用标准方式,我们可以定义一个PMR字符串的vector,并将这个PMR的地址给它。然而,有一个问题,因为它不是很直观。那就是在循环中使用这种单调缓冲区。让我们假设这里的buffer_memory没有release。。所以,我们创建一个字符串的vector,PMR字符串的PMR vector,使用某个缓冲区内存资源,然后我们使用它,然后它超出作用域,被销毁,但单调缓冲区永远不会收缩。所以当我们开始新的迭代时,下一个对象将在空闲指针的当前位置分配。它不会收缩。是吗?只有在……。但是,为此,我们有这个release方法。这样我们就可以将指针倒回到开头。清楚了吗?第一次迭代,对象,对象,对象,对象,对象。现在,PMR vector超出作用域。析构函数,析构函数,析构函数被调用,然后分配器的第二部分是释放内存。但是,这个家伙在这里什么也不做。明白了吗?它有其用途。它不仅仅是一个设计糟糕的分配器。因为,我必须说,这些分配器不是委员会设计的。它们是从现有的代码库中借鉴来的,这些代码在彭博社使用了20年左右。所以它是设计良好、经过充分测试的。所以,好吧。我们可能会认为它是新东西。非常闪亮。不。人们一直在使用它。比如,我第一次看到它的使用是在一个XML解析程序中,该程序在内部使用了链表。当然,这是性能灾难。然后,当切换到这种由简单缓冲区支持的递增分配器时,它变成了高性能解析器。在游戏编程中,他们一直在使用它。他们用了很久了。所以,我没有时间讨论它。也许在提问环节。

好的,现在第二个主力。这是我们的pool_resource。我们可以有同步和非同步版本。它做什么?它由针对不同大小的池的集合组成。分配也很快,因为我们只需要找到大小合适的池。此外,碎片化将减少,局部性将最大化,因为对象将被放置在连续的内存中。我们稍后会看到。但当然,它针对大小相等的块,或大致相同大小的对象进行了优化。

构造函数,我们只有带有上游的普通构造函数,但也有选项,可以稍微定制一下这个池资源。好的。池资源就是这样实现的。所以,我们有几个针对不同大小的池。每个池管理一组块。这些是块。这些块被划分为区块。所以你看,它是连续的。它提供了良好的局部性和没有内存扩散。(译者注:原文“no memory diffusion”可能指减少碎片化或提高缓存效率)。

那么我们如何定制它?首先,通过required_poolable_size。这是旧名字。largest_required_pool_block。它简单地告诉你,我们支持的最大池大小是4000左右。对于更大的池,对于过大的池,你必须向上游分配器请求。然后,块的长度也可以更改。这里有一些权衡。什么时候,什么时候,什么时候应该更改块的长度。没时间,没时间讲那个了。内容太多了。我很抱歉。但是,这也不是什么新东西。我们在游戏编程中将其用于池分配器,用于粒子、抛射物、宇宙飞船。我第一次见到它的使用是在网络编程中处理网络数据包。所以,为数据包预分配大小相等的块。Apache HTTP服务器在80年代就使用了内存池。他们甚至为操作分离和局部化使用了内存池层次结构。为了局部性。这都是非常古老的技术。

特殊的PMR。null_memory_resource。我们到底为什么需要它?嗯,它不分配内存。但它必须做点什么。在allocate时,它抛出bad_alloc。所以我们可以将它作为一种分配守卫使用。我们将其作为某些20,000字节的单调缓冲区资源的上游资源。当这20,000字节耗尽时,我们将抛出bad_alloc。所以它是有用的。在测试中。

那么,我们如何使用PMR?因为,所有这些都是非常、非常理论化的。理论上的。但是首先,看看一些我们可能都知道的著名内存技巧。

技巧一。我们需要一个局部变量,局部字符串或vector,它将在函数中使用然后被释放。所以,技巧是,不是在堆上分配它,而是使用一个叫做alloca的malloc扩展。这将给你栈上的内存。这很危险。我知道。但人们使用它。所以,单调缓冲区可以做到这一点。总是可以做到,而无需使用malloc扩展。

第二,我们用malloc分配许多小的内存块。我们只是想削减系统调用的开销,只分配一个大的块然后自己分割。所以,这也是单调缓冲区所做的。

第三个著名技巧。我们分配和释放许多相同类型或相似大小的对象。所以人们只是缓存用于此的内存块,而不是归还它们,然后重用。这正是我们的池PMR内部所做的。所以,瞧。

现在,让我们具体一点。我有几个场景。

场景一。我们有一个短命的动态对象。在局部作用域。某个字符串。某个vector。我们必须构建它。然后,以某种方式,在上面累积值。类似这里的东西。然后,我们不再需要它了。所以,我们可以只使用单调缓冲区资源,它由栈上的本地内存缓冲区支持。我们只需定义unsigned char buffer[something]。长度是某个值。不调用alloca或任何东西。然后,把它给这个单调缓冲区资源。这个资源有一块内存,会在每次分配时递增指针。最后,它只会将指针倒回。哦。开销是最小的。这通常会完全绕过全局堆。在,什么情况下,它会在全局堆上分配?谁知道呢?上游是什么?默认的上游分配器?new_deletenew_delete。是的。如果我们,如果我们,如果我们,如果我们溢出这个缓冲区,new_delete将被触发。所以,是的。明白了吗?场景一。

场景二。我们有一个大的数据结构。它不经常改变。它首先是以单调方式构建的。就像在单调缓冲区中一样。意思是元素被添加、添加、添加。偶尔,可能会被移除。但通常不会。所以,我们的建议是,就用我们的老朋友单调缓冲区资源,它是为这种情况设计的。特别是为这种情况。例如,我们正在解析一些配置文件。并将其写入配置映射。所以它通常不经常改变。嗯,这里我们没有指定任何,任何本地内存缓冲区。所以,会发生什么?这是什么?默认的上游。new_delete。是的。所以一开始,unordered_map会获得一些内存。所以我们可以说,我们可以为预期的条目数预留内存。这样我们就不必每次都重新分配它。如果可能的话,在容器上调用reserve以避免重新分配。明白了吗?好的。场景二。

场景三。我们有一个数据结构。我们构建了它。但我们不断地更新它。所以这个缓冲区家伙不太合适。因为它会无限增长。所以,在这种情况下,池资源将是一个很好的选择。为什么?我们看到它针对高效的内存重用和局部性进行了优化。所以,如果我们非常频繁地更改数据结构,我们需要一些东西,一些PMR来提供它。这样我们就不会损害性能。所以,这是池资源的一个用例。当然,我们必须参数化它使其合适。但总的来说,这是内存资源的一个用例。它是为其设计的用例。

场景四。现在,我们有一个深的调用链。函数调用链。甚至可能是递归的。在这些函数中的每一个,我们都需要这些局部的动态对象,比如字符串的vector,它们将被构建等等。是吗?那么我们能在这里做什么?这也是池资源的一个用例。我们只需定义一个池资源,并将其向下传递到调用链中。效果是什么?为什么它应该是好的?缓存?重用?每次我们退出一个函数,栈帧从栈中弹出,那么被销毁对象所支持的内存将返回到我们的池中。但是,然后我们会再次调用一个类似的函数。然后,它将使用,它将分配类似的对象。所以,我们可以立即重用在另一个函数中释放的内存,在新的函数中使用。此外,如果返回到池中的块可能仍然在缓存中。所以,我们现在可以占用一点池资源的时间……。池资源将被准备好。池资源将被准备好并准备好立即重用。当然,理论上,是的?但我们必须测量,我们必须实验,但这也会是池资源的一个很好的选择。清楚了吗?清楚了吗?是的?是的?好的。

一些高级问题。我们想看更高级的东西吗?或者,大家已经都糊涂了?好的,我们可以跳过那个。不?是的。高级的?是的。你想讲高级的吗?好的。这是一个称为“wink out”的高级场景。嗯,谷歌协议缓冲区的竞技场使用它。他们说,对象将一次性释放,通过丢弃整个竞技场,理想情况下不需要运行所包含对象的析构函数。没人知道这是什么意思,是吧?所以,让我们看看。我们有一个单调缓冲区作为容器的PMR。然后……啊,是的,是的。我们想实现的是,不会为容器的元素调用析构函数。例如,字符串的析构函数等等。因为如果我们退出函数,就不再需要这些对象了。所以,技巧是我们可以为对象的容器使用单调缓冲区。好吗?这是C++20的,但是……单调缓冲区资源。现在,我直接构造一个分配器。这通常在容器的构造函数中完成。但这里我不需要容器。我需要分配器。然后,同样是C++20,我在这个分配器上调用new_object。它将做两件事:分配内存并调用对象的构造函数。好吗?这是constructallocate的快捷方式。现在我们可以使用它,使用它,使用它。哦,是的。然后……我们得到一个指向对象的指针。然后我们使用这个对象。在作用域结束时,它不再需要了。但是数据泄露了吗?会调用析构函数吗?是的,是的,是的,是的。你应该非常、非常清楚,如果你推入的数据不是真正可析构的,你就有大麻烦了。当然,析构没有副作用。是的,抱歉,我漏掉了那一点。当然。因为这是一个高级的、仅限专家的技术。所以,在这里我们泄露了这个对象。所以不会为它调用任何析构函数。在这里,在多态分配器中,底层内存将在分配器超出作用域时被释放。所以我们释放了内存,但没有调用任何析构函数。哟嚯。这就是protobuf竞技场所提供的。他们甚至还额外提供了析构时必须调用的注册函数。所以,好的。明白了吗?或者这一切都是令人困惑的混乱?不,没那么难。这不是一个高级演讲。

现在我想稍微离题一下,看看jemalloc。什么是jemalloc?什么是jemallocjemalloc是一个系统内存分配器。它旨在减少碎片化并提供良好的并发可扩展性。所以,它有许多超越普通分配器的特性。它的主要用户是FreeBSD、Mozilla Firefox。他们在使用它减少Firefox中的碎片化方面取得了相当大的成功。然后它的开发者去了Facebook。数据库也喜欢它,因为它在并发环境中非常具有可扩展性。Android也切换到了它。所以,我认为这是件好事。

那么,它是如何设计的?它通过创建多个竞技场来减少线程程序的锁争用。竞技场是彼此完全独立工作的子分配器。所以,默认情况下,它为每个CPU或核心创建四个竞技场,我想。以前是这样。然后,在内部,它区分三种大小类别:小、大和巨大对象。以应用不同的优化。这些类别进一步细分为不同的尺寸等级。见过这个吗?池分配器,有吗?是的?好的。此外,这个分配器支持线程特定缓存。就像tcmalloc。这意味着每个线程都有一个线程本地内存,里面存放着最常访问的对象。所以。我们可以调整它。我们可以用运行时选项调整它。我们可以用一些……比如我们可以开启后台线程来清理脏页。我们可以更改未使用页面的衰减和衰减时间。我们可以更改竞技场数量。我们不喜欢太多的内存浪费。我们可以开启亲和性。线程到竞技场的亲和性。我们可以获得广泛的跟踪和广泛的统计信息。所以,如果你正在寻找内存问题,那么jemalloctcmalloctcmalloc也不错。但jemalloc也有更好的性能。所以,它适合你。

文档中的例子。看起来不是很酷。它只是为高资源应用进行参数化,优先考虑CPU利用率而不是内存。然后是内存消耗较低且完全不用内存的应用。所以这没那么有趣。这更像是给系统管理员或SRE工程师用的。但我们也有编程API作为malloc扩展。它叫做mallocx。我们可以在那里做几件事。我们可以显式创建新的竞技场。新的线程缓存。然后我们可以通过mallocx访问它们,给它一个ID。因此,应用程序可以将其拥有的频繁访问的对象分配在一个预先创建的专用竞技场中。并改善局部性,减少争用等等。而且,这样创建的竞技场,你可以单独调整。例如,如果预期会频繁重用,就设置较长的衰减时间。这样我们在将内存归还给操作系统之前会多等一会儿。

让我喝点水。。我们还可以通过编程方式设置线程和某个竞技场之间的显式绑定。所以手动设置线程-竞技场亲和性。不是全局的。是的,以减少争用和分配级别,好的。然后我们有扩展钩子,这太高级了,但这些扩展钩子允许你实现自己的内存处理。一个很酷的使用例子是,利用大页来减少TLB未命中,我之前在这里的某个演讲中讲过,是由Facebook的HHVM完成的,他们编写了自己的扩展钩子来管理1GB的大页用于频繁访问的数据,这很酷,而且是开源的,你只需谷歌一下。

哦,我们还有时间,还有时间。那么PMR的情况如何,因为这里有一些相似之处,机制并没有那么不同。但首先,jemalloc开发者怎么说,作为一个极端例子,我们可以使用它,我们可以分配一个竞技场并将其用作池分配器,因为在内部它会有针对不同对象大小的分级,然后我们将在其上执行通用目的分配器分配,然后整个竞技场在单一操作中被销毁,等等。是的。这看起来像什么?有点类似于Apache内存池,然而是的,Apache,然而我们可以将其作为一个整体使用。所以我们的机制,我们的PMR,将复制这种机制的是同步池资源,因为竞技场是通过锁同步的。。

第二点,线程缓存,你也可以为你分配线程缓存,所以对我来说,它看起来就像一个本地分配的内存块,所以某种程度上类似于单调缓冲区资源,但没有所有的机制……。线程缓存的大小必须配置,因为有一个默认的最大大小,你必须更改它等等。那么你认为你会使用它吗?因为它很酷很好,但我思考过,因为我不知道你可以以这种方式定制全局内存系统内存分配器,因为使用GNU的内存分配器或标准malloc从来不可能,tcmalloc没有那么多可以更改的地方,比如线程数量或线程缓存大小之类的,但jemalloc对定制有广泛的支持。但后来我想,好吧,我会回到我的PMR,也许会把它用作上游分配器来提供基本内存。例如,Facebook的Folly,大家都知道吗?那个由Alexandrescu等人开发的Folly库。他们有fbvector,一个标准vector的替代品。他们所做的,是试图检测我们是否在使用jemalloc,如果是,我们知道它在内部使用什么重新分配策略,然后这个vector在增长大小时不会以指数方式增长两倍,而是只使用jemalloc内部使用的相同增长策略。这是一个很酷的优化案例,也利用了jemalloc中对可重定位类型的支持,这在C++目前还没有。

另一个可能的用法是标签堆分配器。标签堆就像你可以指定在哪个堆上分配某些对象。游戏行业使用这个。顽皮狗我想用过。顽皮狗,你有几个堆,然后程序可以选择它使用哪个堆,当不再需要时,它将被释放。但不是程序的所有对象都会被释放,而只是那些分配在给定堆上的对象。它被称为标签堆,因为你在从堆分配东西时给出一个标签。我们可以用jemalloc竞技场来实现它。但不幸的是,它没有被包含在STL标准库中。

所以总结一下。所以,我认为,哦,五分钟了。我的结论是,编写自定义数据结构、分配器等代价高昂,但现在我们有了PMR,它可以用于大多数用例,所以我们很高兴能开发。所以,系统分配器调整很有趣,但嗯,我个人会坚持使用PMR。当然,永远不要把我的场景当作经验法则。所以,只需测量、实验、更新、再次测量。然后你就看到是否有意义。

关于分配器的另一件事是,它们不仅能提高性能,还能做其他事情,比如放置对象。例如,在栈上、在文件映射内存上、在共享内存上,然后可以测量和报告内存使用情况,测试程序的正确性。所以我们必须做这些,在C++23中,但我们会讲到那里。那里有什么问题吗?嗯,C++如果没有问题就不是C++了。关于库模块的问题。所以,我们没有的是,例如,共享内存资源。我们想要它。我想要它。但,好吧,我们有boost inter-process分配器。好的,它们可以插入到STL容器中。它是可行的。诚然。我不知道。我没试过,但Boost说它会没事的。

然后,在彭博社使用了多年的PMR测试资源没有被包含在C++11中。也没有在C++20中,也许它会在C++23中进来,但我不知道。没听说任何消息。没听说任何关于它的事情。所以它不完整。支持不完整。

其次,我们有一些陷阱。例如,shared_ptr不是分配器感知的。但它有一个分配器参数。但它不会传递给它的内容。所以你只需知道这一点。而且这不直观。所以,在C++14中,我已经说过了,这个标准函数std::function有接受分配器参数的构造函数,然后它们在内部实现了类型擦除分配器。但在C++17中被移除了,因为它没有被很好地指定,而且没人知道如何实现它。每个实现都不同。然后在C++17中,std::any从中吸取了教训,完全不允许分配器定制。因为,为什么不呢?它只是,它有问题。我们不想做它。

我们的未来是什么?好吧,让我们坦率地说。这一切都非常无聊。好吗?我们是懒惰的程序员。为什么不能让编译器做这项工作?是的,有些人正在考虑这个问题。例如,这将是C++未来的语法。我们有一些容器。就这样。我们不考虑分配任何东西。我们简单地对编译器说,嗯,为此使用这个分配器,并完成所有的连接工作。我们不考虑它。甚至有人,我认为有些人试图提出一个提案。但我不知道。真的不知道。像,Lakos。彭博社的Lakos。是的,就是Lakos分配器模型。是的。Pablo Halper和……不,他们试图提出别的东西。但在这个……。以这种方式。比如,在类中使用using something,那么在该类中的每个人都会使用那个。我会喜欢它。你呢?分配器的事情。哇哦。甚至更好。为什么我们不能忘记所有这些?编译器在优化东西。让编译器优化程序的堆布局。这样它就高效了。我们可以像LTO、PGO或基于注解的那样做。相关工作正在进行中。但只在研究界。所以还有很长的路要走。

就这些。提问时间。谢谢。或者,你们想学更多吗?我还有很多幻灯片。你们没有问题。你们将被轰炸。是的。

所以,在演讲早期,你提到将分配器包含在模板类型中的一个缺点是,你不能把它传递给那些已经接受vector的所有函数,对吧?所以,如果我理解正确的话,用新东西这个问题仍然存在,有点。有点。除非你已经承诺改变所有东西。因为我们有一个包装器,并且这个包装器将被独占使用。所以这个类型没有变化。所以我们固定了字符串的PMR类型,仅此而已。然后我们可以插入不同的内存分配。来,来。它在哪里?这里。哦,不。这是Victor。什么?啊。是的。这个。是的,这个。这是我们的占位符。我们需要它在这里,但只需一次。当我们固定了它,所有这个STD容器将在std::pmr命名空间中。不,也许不是,如果它是这样定义的。但我们有这个占位符,每个容器只会使用这个占位符,如果我们改变内存分配,我们不改变这个类型。对的。但这假设你已经将整个代码库从std::vector移植到了现在的新东西上,这可能困难也可能不困难……也许不难。但这些都是优化。你不需要在所有地方都应用它。你只在需要它的代码片段应用它。因为过早优化等等。谢谢。谢谢你的演讲。

简短的问题,如果我理解正确,你能回到池的那张幻灯片吗,在那里你看到这些不同的块?如果我理解正确,配置只是最大所需池,而不是最小。不。好的。所以如果我知道,例如,我知道的一个用例,我计算FFT。我总是需要相同大小的FFT数组。我知道它就像三个不同的尺寸。所以我也知道最小值,不只是我想要分配的最大值。这里不可能做到。是的,但如果我们给出最大尺寸,池分配器会计算这个分级。好的。谢谢。所以也许还有另一种标准库PMR的情况。

一切都清楚了吗?我没有问题。我有一个评论。所以这是这个专家级的可winkable的东西。是的。我无法更强调这对于任何做图、树或类似事情的人有多重要。在图或树上进行大量分配的重计算。所以基于John Lakos在这里的演讲,我想是在17年,我们像这样使用:分配所有东西,不销毁任何东西。是的。最后一次性丢弃内存,性能提升了300%。我去年就这个话题做过演讲。但那是高级水平。但我可以展示给你看。这种图垃圾回收。是的。我们在2017年就已经实现了我们的东西。但我只是想评论一下,不知道这个的人应该去了解一下。是的。但好吧,这本来不应该是一个高级演讲。它是,那是基础。两个PMR,它们如何插入,我如何使用它。如果你想了解更多,就在Twitter上找我。

我还有一个问题。如果我,也许我错过了。但你说过问题之一是PMR资源,不管是什么,不应该比……不,使用它的vector不应该活得比它长。但这没有被修复,对吧?这仍然。没有,它是,它是,它是设计如此。没有被修复。是的。好的。所以你必须自己注意。是的。这就像,就像,我们使用Lambda捕获时一样的情况。你必须注意。是的。好的。要知道你在做什么。我们是C程序员,C++。是的。谢谢。所以我们作为C++程序员知道我们在做什么。C++程序员。Python程序员不知道那个。但我想传达给你们的是清晰的。这些分配器类,如何使用,以及它们也有仅供专家使用的进阶用法。因为问题来了。这是未定义行为吗?不是。但告诉我为什么。仅限专家。这是另一个演讲。一个高级演讲。谢谢。