C++ weak_ptr创建性能

 8prye孙瑞D 发布于 2023-02-13 18:47
  • php
  • 我已经读过创建或复制std :: shared_ptr涉及一些开销(参考计数器的原子增量等).

    但是如何从它创建一个std :: weak_ptr:

    Obj * obj = new Obj();
    // fast
    Obj * o = obj;
    // slow
    std::shared_ptr a(o);
    // slow
    std::shared_ptr b(a);
    // slow ?
    std::weak_ptr c(b);
    

    我希望在一些更快的性能,但我知道共享指针仍然必须递增弱引用计数器..所以这仍然像将shared_ptr复制到另一个慢?

    2 个回答
    • 除了Alec对他之前项目中使用的shared/weak_ptr系统的非常有趣的描述之外,我还想详细介绍一下典型std::shared_ptr/weak_ptr实现可能发生的事情:

      // slow
      std::shared_ptr<Obj> a(o);
      

      上述结构的主要费用是分配一块内存来保存两个引用计数.这里不需要进行原子操作(除了实现可能会或可能不会执行的操作operator new).

      // slow
      std::shared_ptr<Obj> b(a);
      

      复制构造中的主要开销通常是单个原子增量.

      // slow ?
      std::weak_ptr<Obj> c(b);
      

      weak_ptr构造函数中的主要开销通常是单个原子增量.我希望这个构造函数的性能几乎与shared_ptr复制构造函数的性能相同.

      另外两个要注意的重要构造函数是:

      std::shared_ptr<Obj> d(std::move(a));  // shared_ptr(shared_ptr&&);
      std::weak_ptr<Obj> e(std::move( c ));  // weak_ptr(weak_ptr&&);
      

      (以及匹配的移动赋值运算符)

      移动构造函数根本不需要任何原子操作.他们只是将引用计数从rhs复制到lhs,并使rhs == nullptr.

      仅当赋值之前的lhs!= nullptr时,移动赋值运算符才需要原子递减.在vector<shared_ptr<T>>移动分配之前的大部分时间(例如在a内)lhs == nullptr,因此根本没有原子操作.

      后者(weak_ptr移动成员)实际上不是C++ 11,而是由LWG 2315处理.但是我希望它已经被大多数实现实现(我知道它已经在libc ++中实现).

      vector<shared_ptr<T>>::insert/erase使用智能指针复制成员时,这些移动成员将用于在容器中搜索智能指针,例如在容器周围,并且可以具有可测量的积极影响.

      我指出它,以便你知道如果你有机会移动而不是复制a shared_ptr/weak_ptr,那么输入一些额外的字符是值得的.

      2023-02-13 18:49 回答
    • 这是我与游戏引擎的日子

      故事如下:

      我们需要一个快速的共享指针实现,不会破坏缓存(缓存现在更聪明btw)

      正常指针:

      XXXXXXXXXXXX....
      ^--pointer to data
      

      我们的共享指针:

      iiiiXXXXXXXXXXXXXXXXX...
      ^   ^---pointer stored in shared pointer
      |
      +---the start of the allocation, the allocation is sizeof(unsigned int)+sizeof(T)
      

      unsigned int*用于计数在((unsigned int*)ptr)-1

      这样一个"共享指针"是指针大小,它包含的数据是指向实际数据的指针.所以(因为模板=>内联和任何编译器都会内联一个运算符返回一个数据成员)它与普通指针的访问是相同的"开销".

      创建指针需要比正常情况多3个CPU指令(访问位置-4正在运行,添加1和写入位置-4)

      现在我们在调试时只使用弱指针(因此我们使用DEBUG定义(宏定义)进行编译)因为那时我们希望看到所有的分配和最新情况等等.这很有用.

      弱指针必须知道他们指向的东西何时消失,不要保持他们指向的东西(在我的情况下,如果弱指针保持分配活着,引擎永远不会回收或释放任何记忆,那么它基本上是无论如何共享指针)

      所以每个弱指针都有一个bool,alive或者什么东西,并且是它的朋友shared_pointer

      调试我们的分配时看起来像这样:

      vvvvvvvviiiiXXXXXXXXXXXXX.....
      ^       ^   ^ the pointer we stored (to the data)
      |       +that pointer -4 bytes = ref counter
      +Initial allocation now 
          sizeof(linked_list<weak_pointer<T>*>)+sizeof(unsigned int)+sizeof(T)
      

      您使用的链表结构取决于您关注的内容,我们希望保持尽可能接近sizeof(T)(我们使用伙伴算法管理内存),因此我们存储了指向weak_pointer的指针并使用了xor技巧. ... 美好时光.

      无论如何:指向shared_pointers指向的东西的弱指针放在一个列表中,以某种方式存储在上面的"v"中.

      当引用计数达到零时,您将浏览该列表(这是指向实际weak_pointers的指针列表,当它们被删除时它们将自行删除)并且您将alive = false(或其他内容)设置为每个weak_pointer.

      weak_pointers现在知道他们指向的东西不再存在(所以在被引用时扔掉了)

      在这个例子中

      没有开销(系统的对齐是4个字节.64位系统往往喜欢8个字节的对齐....在那里将ref-counter与int [2]结合在一起以填充它.记住这个涉及到现场新闻(没有人投票,因为我提到它们:P)等等.你需要确保struct你对分配匹配你所分配和制作的东西.编译器可以自己对齐东西(因此int [2]不是int,int ).

      您可以完全取消引用shared_pointer,而不需要任何开销.

      正在制作的新共享指针根本不会破坏缓存并且需要3个CPU指令,它们不是很容易管道但是编译器总是会内联getter和setter(如果不是总是:P)那么'将成为可以填充管道的呼叫站点周围的东西.

      共享指针的析构函数也很少(递减,就是这样),所以很棒!

      高性能笔记

      如果你有这样的情况:

      f() {
         shared_pointer<T> ptr;
         g(ptr);
      }
      

      无法保证优化器不敢通过将"按值"传递给shared_pointer来进行加法和减法.

      这是您使用普通引用(实现为指针)的地方

      所以你要g(ptr.extract_reference());改为 - 编译器会再次内联简单的getter.

      现在你有一个T&,因为ptr的范围完全包围g(假设g没有副作用等等),该引用在g的持续时间内有效.

      删除引用是非常难看的,你可能不会偶然做到(我们依赖这个事实).

      事后来看

      我本应该创建一个名为"extracted_pointer"的类型,或者很难为类成员输入错误的类型.

      stdlib ++使用的弱/共享指针

      http://gcc.gnu.org/onlinedocs/libstdc++/manual/shared_ptr.html

      不是那么快......

      但是不要担心奇怪的缓存未命中,除非你制作一个游戏引擎没有运行不错的工作量> 120fps:P仍然比Java更好.

      stdlib方式更好.每个对象都有自己的分配和工作.我们shared_pointer这是一个真实的案例"相信我它的工作,尽量不要担心如何"(不是很难),因为代码看起来非常混乱.

      如果你解除了......他们对他们实现中的变量名称做了什么,它会更容易阅读.参见Boost的实现,正如文档中所述.

      除了变量名之外,GCC stdlib实现很可爱.你可以很容易地阅读它,它可以正常工作(遵循OO原则)但速度稍慢,并且可能会在最近蹩脚的芯片上破坏缓存.

      UBER高性能笔记

      你可能在想,为什么没有XXXX...XXXXiiii(最后的引用计数)然后你会得到最好的分配器对齐!

      回答:

      因为必须做的pointer+sizeof(T)可能不是一个CPU指令!(减去4或8是CPU可以轻松完成的事情,因为它有意义,它会做很多事情)

      2023-02-13 18:50 回答
    撰写答案
    今天,你开发时遇到什么问题呢?
    立即提问
    热门标签
    PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有