惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Hugging Face - Blog
Hugging Face - Blog
IT之家
IT之家
Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
The GitHub Blog
The GitHub Blog
腾讯CDC
L
LangChain Blog
P
Proofpoint News Feed
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
量子位
Apple Machine Learning Research
Apple Machine Learning Research
Cloudbric
Cloudbric
B
Blog RSS Feed
B
Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
Vercel News
Vercel News
S
Schneier on Security
Project Zero
Project Zero
宝玉的分享
宝玉的分享
美团技术团队
MyScale Blog
MyScale Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Spread Privacy
Spread Privacy
T
Threatpost
A
Arctic Wolf
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
爱范儿
爱范儿
Recorded Future
Recorded Future
C
CERT Recently Published Vulnerability Notes
T
Threat Research - Cisco Blogs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
SecWiki News
SecWiki News
H
Hacker News: Front Page
AWS News Blog
AWS News Blog
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tor Project blog
C
Check Point Blog
L
Lohrmann on Cybersecurity
F
Full Disclosure
TaoSecurity Blog
TaoSecurity Blog
Google Online Security Blog
Google Online Security Blog
云风的 BLOG
云风的 BLOG
The Last Watchdog
The Last Watchdog
Martin Fowler
Martin Fowler

I'm OWenT

国产大模型(GLM 5.1、Kimi K2.6)真实场景效果和 Coding Plan 额度测试 新版本libatapp的连接管理——从etcd服务发现到拓扑驱动的自动重连 新版本libatbus的设计变更——从树形路由到拓扑驱动 Protobuf又一坑 - C++标准和ABI兼容性 AI真好用-给Blog主题统一加mermaid,chart.js,excalidraw,draw.io的多种引入方式支持 给内网部署Squid-通用HTTP下载缓存 UE使用CodeChecker和clang-tidy生成静态分析报告 找出UE的循环依赖 C++小协程栈和临时变量及作用域的栈溢出问题分析 游戏服务的可观测性能力建设(C++生态) 指标上报的多线程优化和多拉取源点优化 协程(libcopp)的Channel功能和CPU命中率优化 通用RPC代码生成器 手夯一个STL allocator和对象内存分析组件 std::condition_variable 的信号丢失问题 踩坑一处(GCC)STL std::async 实现BUG导致的crash问题 GCC 14的一个warning to error BUG 给xresloader(Excel导表工具)增强UE读表支持(包含蓝图,Blueprint) Opentelemetry社区在gRPC的几个链接问题(静态库和动态库混用,musl工具链,符号裁剪) Excel转表工具(xresloader)的新验证器(验证外部Excel和文本数据,唯一性和自定义规则) protobuf v22和gRPC v1.55版本升级的依赖变化和upb适配 关于protobuf近期版本(v20/v3.20+)和 gRPC v1.54版本在某些编译环境下的一些链接和编译问题 xresloader-Excel导表工具链的近期变更汇总 打通游戏服务端框架的C++20协程改造的最后一环 Opentelemetry-cpp的Logs模块标准更新(涉及近期版本:1.8-1.9的BREAK CHANGES) 给cmake-toolset和工具链(curl等)加HTTP/2和HTTP/3支持 又开新坑之 coredns 插件: nftables和filter 关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记 填个转表工具 xresloader 去年的坑(数组尾部裁剪) 集成 upb 和 lua binding 的踩坑小记 libcopp对C++20协程的接入和接口设计 再度优化GCC、LLVM、Clang、libc++、libc++abi等套件的构建脚本 游戏服务的分布式事务优化(二)- 事务管理 游戏服务的分布式事务优化(一)- Write Ahead Log(WAL) 模块 记录一些bazel适配用编译选项 测试现代化硬件C++浮点数性能和一致性 适配Boringssl和OpenSSL 3.0 近期cmake-toolset的一些适配问题 C++20 Text Formatting/fmtlib 适配问题小记 再次重构LLVM+Clang+libcxx+libc++abi+其他相关工具的构建流程 重构基于CMake的构建工具链 新版GCC和LLVM+Clang终于Release啦 折腾一下nftables下的双拨 [C++20] Module partitions和符号交叉引用(声明和实现分离) [Rust] 实现一个线程安全且迭代器可以保存的链表 基于protobuf的代码生成 几个使用protobuf中C++接口的Arena的坑 Amazon Aurora DB存储引擎论文阅读小记 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等) xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等) 在游戏服务器中使用分布式事务 libcopp接入C++20 Coroutine和一些过渡期的设计 libatbus 的大幅优化 nftables初体验 容器配置开发环境小计 PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记 跨平台协程库 - libcopp 简介 C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比) 尝鲜Github Action 一些xresloader(转表工具)的改进 protobuf、flatbuffer、msgpack 针对小数据包的简单对比 协程框架(libcopp) 小幅优化 Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持 Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 C++20 Coroutine libcopp merge boost.context 1.69.0 Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感 Rust玩具-企业微信机器人通用服务 使用ELK辅助监控开发测试环境服务质量和问题定位 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现 Webpack+vue+boostrap+ejs构建Web版GM工具 Rust的第二次接触-写个小服务器程序 理解和适配AEAD加密套件 atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比 可执行文件压缩 初识Rust 使用restructedtext编写xresloader文档 atframework的etcd模块化重构 C++的backtrace ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装 protobuf-net的动态Message实现 pbc的proto3接入 atgateway内置协议流程优化-加密、算法协商和ECDH 整理一波软件源镜像同步工具+DevOps工具 Blog切换到Hugo libcopp v2的第一波优化完成 libcopp(v2) vs goroutine性能测试 libcopp的线程安全、栈池和merge boost.context 1.64.0 GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本 libatbus的几个藏得很深的bug 用cmake交叉编译到iOS和Android 开源项目得一些小维护 atapp的c binding和c#适配 对象路由系统设计 2016年总结 近期的一个协程流程BUG 重写了llvm+clang+libc++和libc++abi的构建脚本 atsf4g完整游戏工程示例|I'm OWenT atframework基本框架已经完成|I'm OWenT
实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针)
owent · 2024-10-09 · via I'm OWenT

blog-website

背景

我们的新项目有个比较复杂的全区全服交易行系统,其中搜索和推荐是高实时性全区服多维度排序的,并且要支持比较复杂的标签交集查询和属性范围查询的自由组合。 当有订单发生变化时,它不仅仅会影响全服状态下搜索和推荐条件的结果变化,也会同时影响商品维度的聚合,交易行层面的数据聚合。

为了降低搜索开销和提供弹性伸缩的能力,我们在多个不同类型的服务上设计了基于视图的动态索引支持,并且针对常用的一些搜索推荐模型,实现了大量的静态索引。

这也同时带来的影响是每当订单有变化时它都有可能会去刷新大量的索引。我们早期第一版直接用 std::shared_ptr 来维护订单信息。每次变更索引时都是重新入删除和插入一个 std::shared_ptrstd::shared_ptr 底层的实现是使用 std::atomic 来维护了引用计数。每次变更操作都会导致 std::atomic 增减,同时带来 CPU Cache Line 失效。

在大多数场景下,这也没太大问题,因为大多数场景下参数传递 std::shared_ptr 也好,内存存放 std::shared_ptr 也好,通常上下文会存在更多逻辑操作,导致这个CPU Cache Line 失效后重新prefetch的开销占比很小。但是在我们这个场景下情况又有所不同。 我们采用了BTree来管理有序索引,这样比常规的红黑树来说内存结构会更紧凑,缓存命中率更高。然而在连续大量索引发生变化时,一直要发生Cache失效->重新prefetch,Cache失效->重新prefetch的过程。 这反而带来了额外的浪费。我们在后来用valgrind分析的过程中也确实验证到这部分的Cache Miss率明显高于其他操作。于是实现一个非线程安全版本的 shared_ptr 就被提上了日程。思路类似 Rust的 std::rc::Rc

其实GCC的 STL本身自带费线程安全的内部版本的,可以通过使用

template<class T>
using strong_rc_ptr<T> = std::__shared_ptr<T, std::_S_single>;

然后使用 strong_rc_ptr<T> 就是单线程模式。然而我们这里考虑到跨编译器跨平台,所以自己实现了一个。

实现

基础功能

引用计数型的智能指针的基本原理比较简单。就是有一个存储区去存放引用计数。当计数清零后释放。

为了加快解引用的性能,原始指针并没有放在引用计数的存储区里,而是直接放在智能指针对象上。这样大多数场景访问指针内容的时候不需要多一次跳转去查询实际地址。

接下来更多的代码其实是在适配和优化各种使用场景。 首先是针对各类构造场景,我的实现分成了5种。( std::shared_ptrboost::shared_ptr 分别按各自的实现有一些实现合并和分离,和我这边实现稍有差异,但主体Feature相同 )

  • 默认构造函数:接收指针传入,通过 new/delete 管理计数对象。
  • 默认inplace构造:通过 new/delete 管理计数对象和实际对象的内存,通过placement new构造对象。(碎片更少, make_shared/make_strong_rc 的构造方式)
  • 默认带自定义Deletor的构造:比上面的构造多存储一个Deletor对象,通过 new/delete 管理计数对象。
  • 指定Allocator的inplace构造:通过自定义Allocator管理计数对象和实际对象的内存,通过placement new构造对象。( 自定义分配器, allocate_shared/allocate_strong_rc 的构造方式)

    boost::shared_ptr 的实现有问题,某些地方显示使用 new/delete 操作符了,导致对自定义Allocator没有完整的支持。

  • 指定Allocator的带自定义Deletor的构造:比上面的构造多存储一个Deletor对象,自定义Allocator管理计数对象。

有些实现里带Deletor和带Allocator的实现是合并的,我这里分离出来了。是因为没有Deletor的时候可以少一哥对象的维护操作,理论上会更利于编译优化。

另外 boost::shared_ptr 的某些分支实现显示使用 new/delete 操作符来分配管理对象的内存结构了,没有使用 allocator_traits<Alloc>::rebind_alloc<T> 这种形式。实际上也是不对的。 所以这里我采用和 std::shared_ptr 一样的方式保持对自定义Allocator的完全兼容。具体详情也可以参考我之前写的 《手夯一个STL allocator和对象内存分析组件》的Allocator rebind章节

enable_shared_from_this 的两种实现

接下来是 enable_shared_from_this 的实现。要实现继承 enable_shared_from_this 的对象自动带 share_from_this() 接口,首先基类需要记录一个 weak_ptr/weak_rc_ptr 。 然后构造 shared_ptr 的时候检测目标指针可否转换成 enable_shared_from_this 指针。这个检测目前新版本的编译器和STL实现大多数采用 std::void_t<T> 转换的detection idiom来实现。 比如GCC实现:


private:
  template<typename _Tp1>
  void _M_weak_assign(_Tp1* __p, const __shared_count<_Lp>& __n) const noexcept {
    _M_weak_this._M_assign(__p, __n);
  }

  friend const __enable_shared_from_this*  __enable_shared_from_this_base(
    const __shared_count<_Lp>&, const __enable_shared_from_this* __p) {
    return __p;
  }

  // ...
  template<typename _Yp>
  using __esft_base_t = decltype(__enable_shared_from_this_base(
      std::declval<const __shared_count<_Lp>&>(),
      std::declval<_Yp*>()));

  // Detect an accessible and unambiguous enable_shared_from_this base.
  template<typename _Yp, typename = void>
  struct __has_esft_base : false_type { };

  template<typename _Yp>
  struct __has_esft_base<_Yp, __void_t<__esft_base_t<_Yp>>> : __not_<is_array<_Tp>> { }; // No enable shared_from_this for arrays

  template<typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
  typename enable_if<__has_esft_base<_Yp2>::value>::type
  _M_enable_shared_from_this_with(_Yp* __p) noexcept {
    if (auto __base = __enable_shared_from_this_base(_M_refcount, __p))
      __base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
  }

  template<typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
  typename enable_if<!__has_esft_base<_Yp2>::value>::type
  _M_enable_shared_from_this_with(_Yp*) noexcept { }

MSVC的STL也是这个实现,但是这个idiom在C++17之后一些行为才被标准化,老的编译器可能会不支持。所以我这里还是采用了更传统的实现方法,即采用模板匹配比动态参数函数更优先匹配的机制。

template <class T1, class T2, class T3>
inline void __enable_shared_from_this_with(const __strong_rc_counter<T1>* __n,
                                                                          const T2* __py,
                                                                          const enable_shared_rc_from_this<T3>* __p) {
  if (nullptr != __p) {
    __p->__internal_weak_assign(const_cast<T2*>(__py), *__n);
  }
}

template <class T1, class T2, class T3, size_t T3SIZE>
inline void __enable_shared_from_this_with(
    const __strong_rc_counter<T1>* __n, const T2* __py, const enable_shared_rc_from_this<T3[T3SIZE]>* __p) {
  if (nullptr != __p) {
    for (auto& p : *__p) {
      p->__internal_weak_assign(const_cast<T2*>(__py), *__n);
    }
  }
}

#ifdef _MANAGED

// Avoid C4793, ... causes native code generation

struct __sp_any_pointer {
  template <class T>
  __sp_any_pointer(T*) {}  // NOLINT: runtime/explicit
};

inline void __enable_shared_from_this_with(__sp_any_pointer, __sp_any_pointer,
                                                                          __sp_any_pointer) {}

#else  // _MANAGED

inline void __enable_shared_from_this_with(...) {}

#endif  // _MANAGED

const类型比较操作符

在实现操作符重载的时候有个小tips需要注意。比如如果比较 shared_ptr<T>shared_ptr<const T> 的时候。 如果实现的不好,容易在某些SFINAE流程里推断成 T* ,然后由于 const T* 不能转成 T* 导致推断失败。 这个需要稍微注意一下。

std::shared_ptrboost::shared_ptr 的差异

在写单元测试的时候,我发现 std::shared_ptrboost::shared_ptr 的实现上还有一些行为上的差异。虽然说这部分可能是未定义行为,但是我觉得 std::shared_ptr 的行为大部分场景下更符合直觉。

空指针引用计数

// boost::shared_ptr
class shared_count
{
private:
  sp_counted_base * pi_;

public:
  BOOST_CONSTEXPR shared_count() BOOST_SP_NOEXCEPT: pi_(0)
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
      , id_(shared_count_id)
#endif
  {
  }

  long use_count() const BOOST_SP_NOEXCEPT {
    return pi_ != 0? pi_->use_count(): 0;
  }
};

// GCC: std::shared_ptr
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp> {
public:
  _Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { }

  long _M_get_use_count() const noexcept {
    // No memory barrier is used here so there is no synchronization
    // with other threads.
    return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
  }
};

// 单元测试
util::memory::strong_rc_ptr<int> pi;
// boost::shared_ptr 行为(strong_rc_ptr采用此行为)
CASE_EXPECT_TRUE(pi.use_count() == 0);
// std::shared_ptr 行为
CASE_EXPECT_TRUE(pi.use_count() == 1);

pi.reset(static_cast<int *>(nullptr));
CASE_EXPECT_TRUE(pi.use_count() == 1);

这里空指针 boost::shared_ptr 引用计数为0, std::shared_ptr 引用计数为1。我觉得0更符合直觉就按0处理了。

操作符的怪异行为

/// GCC: libstdc++
template<typename _Tp1, typename _Tp2, _Lock_policy _Lp>
inline bool operator==(const __shared_ptr<_Tp1, _Lp>& __a, const __shared_ptr<_Tp2, _Lp>& __b) noexcept {
  return __a.get() == __b.get();
}
#ifdef __cpp_lib_three_way_comparison
template<typename _Tp, typename _Up, _Lock_policy _Lp>
inline strong_ordering operator<=>(const __shared_ptr<_Tp, _Lp>& __a, const __shared_ptr<_Up, _Lp>& __b) noexcept {
  return compare_three_way()(__a.get(), __b.get());
}
// 其他相似的重载不再展示 ...
#else
template<typename _Tp, typename _Up, _Lock_policy _Lp>
inline bool operator<(const __shared_ptr<_Tp, _Lp>& __a, const __shared_ptr<_Up, _Lp>& __b) noexcept {
  using _Tp_elt = typename __shared_ptr<_Tp, _Lp>::element_type;
  using _Up_elt = typename __shared_ptr<_Up, _Lp>::element_type;
  using _Vp = typename common_type<_Tp_elt*, _Up_elt*>::type;
  return less<_Vp>()(__a.get(), __b.get());
}
// 其他相似的重载不再展示 ...
#endif

// boost::shared_ptr
template<class T, class U>
inline bool operator==(shared_ptr<T> const & a, shared_ptr<U> const & b) BOOST_SP_NOEXCEPT {
  return a.get() == b.get();
}

template<class T, class U>
inline bool operator<(shared_ptr<T> const & a, shared_ptr<U> const & b) BOOST_SP_NOEXCEPT {
    return a.owner_before(b);
}

// 单元测试
{
  util::memory::strong_rc_ptr<int> p1;
  util::memory::strong_rc_ptr<int> p2;
  p2.reset(nullptr);

  CASE_EXPECT_TRUE(p1 == p2);

  // std::shared_ptr 行为(strong_rc_ptr采用此行为)
  CASE_EXPECT_FALSE((p1 < p2 || p2 < p1));
  // boost::shared_ptr 行为
  CASE_EXPECT_TRUE((p1 < p2 || p2 < p1));
}

这里可以看到 ,当 p1 == p2 的时候直观上显然 p1 < p2 || p2 < p1 应该是 false 的。但是因为boost的实现 operator== 用了原始指针,但是 operator< 缺用的管理数据块的指针。两个不一致。 这里 strong_rc_ptr 保持和 std::shared_ptr 的行为一致。

继承和父子转换和比较操作符

struct X {
  int dummy;
};

struct Y {
  int dummy2;
};

struct Z : public X, public virtual Y {};

// 单元测试
util::memory::strong_rc_ptr<Z> pz(new Z);
util::memory::strong_rc_ptr<X> px(pz);

CASE_EXPECT_TRUE(px.get() == pz.get());
CASE_EXPECT_TRUE(px == pz);

util::memory::strong_rc_ptr<Y> py(pz);

CASE_EXPECT_TRUE(py.get() == pz.get());
CASE_EXPECT_TRUE(py == pz);
CASE_EXPECT_FALSE(py < pz || pz < py);

// strong_rc_ptr 行为, std::shared_ptr 不允许编译通过
CASE_EXPECT_TRUE(px < py || py < px);
// boost::shared_ptr 行为,和下面操作符实现相关
CASE_EXPECT_FALSE(px < py || py < px);

util::memory::strong_rc_ptr<void> pvx(px);
util::memory::strong_rc_ptr<void> pvy(py);
util::memory::strong_rc_ptr<void> pvz(pz);

CASE_EXPECT_TRUE(pvx.get() != pvy.get());
CASE_EXPECT_TRUE(pvx != pvy);

// std::shared_ptr 行为(strong_rc_ptr采用此行为)
CASE_EXPECT_TRUE(pvx < pvy || pvy < pvx);
CASE_EXPECT_TRUE(pvy < pvz || pvz < pvy);

// boost::shared_ptr 行为,和下面操作符实现相关
CASE_EXPECT_FALSE(pvx < pvy || pvy < pvx);
CASE_EXPECT_FALSE(pvy < pvz || pvz < pvy);

对于父子类转换后再类型擦除后的比较操作。我也是觉得 std::shared_ptr 的行为更符合直觉,所以按 std::shared_ptr 的行为为准。

单元测试

单元测试我直接就扒了 boost.shared_ptr 的了。部分和 std::shared_ptr 差异的部分按 std::shared_ptr 的行为做了调整。 这样覆盖场景应该是比较完善了。

周边组件迁移

一键切换组件

因为我这个 strong_rc_ptr 实现了 shared_ptr 的所有接口,所以替换起来非常简单。也提供了一套 traits 接口用于一键切换。这样也方便比较性能差异。 代码大致上这样:

LIBATFRAME_UTILS_NAMESPACE_BEGIN
namespace memory {
enum class compat_strong_ptr_mode : int8_t {
  kStrongRc = 0,
  kStl = 1,
};

template <compat_strong_ptr_mode>
struct LIBATFRAME_UTILS_API_HEAD_ONLY compat_strong_ptr_function_trait;

template <>
struct LIBATFRAME_UTILS_API_HEAD_ONLY compat_strong_ptr_function_trait<compat_strong_ptr_mode::kStrongRc> {
  template <class Y>
  using shared_ptr = memory::strong_rc_ptr<Y>;

  template <class Y>
  using weak_ptr = memory::weak_rc_ptr<Y>;

  template <class Y>
  using enable_shared_from_this = memory::enable_shared_rc_from_this<Y>;

  template <class Y, class... ArgsT>
  static inline memory::strong_rc_ptr<Y> make_shared(ArgsT&&... args) {
    return memory::make_strong_rc<Y>(std::forward<ArgsT>(args)...);
  }

  template <class Y, class Alloc, class... TArgs>
  static inline memory::strong_rc_ptr<Y> allocate_shared(const Alloc& alloc, TArgs&&... args) {
    return memory::allocate_strong_rc<Y>(alloc, std::forward<TArgs>(args)...);
  }

  template <class Y, class F>
  static inline memory::strong_rc_ptr<Y> static_pointer_cast(F&& f) {
    return memory::static_pointer_cast<Y>(std::forward<F>(f));
  }

  template <class Y, class F>
  static inline memory::strong_rc_ptr<Y> const_pointer_cast(F&& f) {
    return memory::const_pointer_cast<Y>(std::forward<F>(f));
  }

#if defined(LIBATFRAME_UTILS_ENABLE_RTTI) && LIBATFRAME_UTILS_ENABLE_RTTI
  template <class Y, class F>
  static inline memory::strong_rc_ptr<Y> dynamic_pointer_cast(F&& f) {
    return memory::dynamic_pointer_cast<Y>(std::forward<F>(f));
  }
#endif
};

template <>
struct LIBATFRAME_UTILS_API_HEAD_ONLY compat_strong_ptr_function_trait<compat_strong_ptr_mode::kStl> {
  template <class Y>
  using shared_ptr = std::shared_ptr<Y>;

  template <class Y>
  using weak_ptr = std::weak_ptr<Y>;

  template <class Y>
  using enable_shared_from_this = std::enable_shared_from_this<Y>;

  template <class Y, class... ArgsT>
  static inline std::shared_ptr<Y> make_shared(ArgsT&&... args) {
    return std::make_shared<Y>(std::forward<ArgsT>(args)...);
  }

  template <class Y, class Alloc, class... TArgs>
  static inline std::shared_ptr<Y> allocate_shared(const Alloc& alloc, TArgs&&... args) {
    return std::allocate_shared<Y>(alloc, std::forward<TArgs>(args)...);
  }

  template <class Y, class F>
  static inline std::shared_ptr<Y> static_pointer_cast(F&& f) {
    return std::static_pointer_cast<Y>(std::forward<F>(f));
  }

  template <class Y, class F>
  static inline std::shared_ptr<Y> const_pointer_cast(F&& f) {
    return std::const_pointer_cast<Y>(std::forward<F>(f));
  }

#if defined(LIBATFRAME_UTILS_ENABLE_RTTI) && LIBATFRAME_UTILS_ENABLE_RTTI
  template <class Y, class F>
  static inline std::shared_ptr<Y> dynamic_pointer_cast(F&& f) {
    return std::dynamic_pointer_cast<Y>(std::forward<F>(f));
  }
#endif
};

template <class T, compat_strong_ptr_mode PtrMode>
struct LIBATFRAME_UTILS_API_HEAD_ONLY compat_strong_ptr_type_trait {
  using shared_ptr = typename compat_strong_ptr_function_trait<PtrMode>::template shared_ptr<T>;
  using weak_ptr = typename compat_strong_ptr_function_trait<PtrMode>::template weak_ptr<T>;
};

}  // namespace memory
LIBATFRAME_UTILS_NAMESPACE_END

算法和容器基础组件适配

有了上面的一键切换traits,其他的组件切换也就比较简单了。

首先在 atframe_utils 内部分组件接入了一件切换。

时间轮定时器实现 jeffies_timer 使用了 shared_ptrweak_ptr 来保证递归调用和移除时生命周期的有效性。 这里通过增加模板参数实现切换支持。

template <time_t LVL_BITS = 6, time_t LVL_CLK_SHIFT = 3, size_t LVL_DEPTH = 8,
          memory::compat_strong_ptr_mode PTR_MODE = memory::compat_strong_ptr_mode::kStl>
class jiffies_timer {
  //...
};

LRU算法容器 lru_map 里为了方便管理也使用了 shared_ptr 管理引用关系。 这个容器为了简化使用,除了在最后面加模板参数外,也加了个简化一点的别名。这样可以不用显示指定hash函数,等于判定,内存分配器的情况下定制智能指针选项。

template <class TKEY, class TVALUE, class THasher = std::hash<TKEY>, class TKeyEQ = std::equal_to<TKEY>,
          class TOption = lru_map_option<compat_strong_ptr_mode::kStl>,
          class TAlloc = std::allocator<std::pair<const TKEY, typename lru_map_type_traits<TKEY, TVALUE, TOption>::iterator>>>
class lru_map {
  // ...
};

template <class TKEY, class TVALUE, class TOption, class THasher = std::hash<TKEY>, class TKeyEQ = std::equal_to<TKEY>,
          class TAlloc = std::allocator<std::pair<const TKEY, typename lru_map_type_traits<TKEY, TVALUE, TOption>::iterator>>>
using lru_map_with_option = lru_map<TKEY, TVALUE, THasher, TOption, TAlloc>;

分布式系统和分布式事务的 WAL模块 内涉及大量的Event Log的管理和订阅关系管理,也采用 shared_ptr 管理。 这个模块结构比较复杂切涉及多个组件保持一致,所以另外加了一层traits来实现。


enum class wal_mt_mode : int8_t {
  kSingleThread = 0,
  kMultiThread = 1,
};

template <class T, wal_mt_mode MTMode>
struct LIBATFRAME_UTILS_API_HEAD_ONLY wal_mt_mode_data_trait;

template <wal_mt_mode MTMode>
struct LIBATFRAME_UTILS_API_HEAD_ONLY wal_mt_mode_func_trait;

template <class T>
struct LIBATFRAME_UTILS_API_HEAD_ONLY wal_mt_mode_data_trait<T, wal_mt_mode::kSingleThread> {
  using strong_ptr = memory::strong_rc_ptr<T>;
  using weak_ptr = memory::weak_rc_ptr<T>;
};

template <class T>
struct LIBATFRAME_UTILS_API_HEAD_ONLY wal_mt_mode_data_trait<T, wal_mt_mode::kMultiThread> {
  using strong_ptr = std::shared_ptr<T>;
  using weak_ptr = std::weak_ptr<T>;
};

template <>
struct LIBATFRAME_UTILS_API_HEAD_ONLY wal_mt_mode_func_trait<wal_mt_mode::kSingleThread> {
  template <class Y>
  using enable_shared_from_this = memory::enable_shared_rc_from_this<Y>;

  template <class Y, class Alloc, class... ArgsT>
  static inline memory::strong_rc_ptr<Y> allocate_strong(const Alloc& alloc, ArgsT&&... args) {
    return memory::allocate_strong_rc<Y>(alloc, std::forward<ArgsT>(args)...);
  }

  template <class Y, class F>
  static inline memory::strong_rc_ptr<Y> static_pointer_cast(F&& f) {
    return memory::static_pointer_cast<Y>(std::forward<F>(f));
  }

  template <class Y, class F>
  static inline memory::strong_rc_ptr<Y> const_pointer_cast(F&& f) {
    return memory::const_pointer_cast<Y>(std::forward<F>(f));
  }

#if defined(LIBATFRAME_UTILS_ENABLE_RTTI) && LIBATFRAME_UTILS_ENABLE_RTTI
  template <class Y, class F>
  static inline memory::strong_rc_ptr<Y> dynamic_pointer_cast(F&& f) {
    return memory::dynamic_pointer_cast<Y>(std::forward<F>(f));
  }
#endif
};

template <>
struct LIBATFRAME_UTILS_API_HEAD_ONLY wal_mt_mode_func_trait<wal_mt_mode::kMultiThread> {
  template <class Y>
  using enable_shared_from_this = std::enable_shared_from_this<Y>;

  template <class Y, class Alloc, class... ArgsT>
  static inline std::shared_ptr<Y> allocate_strong(const Alloc& alloc, ArgsT&&... args) {
    return std::allocate_shared<Y>(alloc, std::forward<ArgsT>(args)...);
  }

  template <class Y, class F>
  static inline std::shared_ptr<Y> static_pointer_cast(F&& f) {
    return std::static_pointer_cast<Y>(std::forward<F>(f));
  }

  template <class Y, class F>
  static inline std::shared_ptr<Y> const_pointer_cast(F&& f) {
    return std::const_pointer_cast<Y>(std::forward<F>(f));
  }

#if defined(LIBATFRAME_UTILS_ENABLE_RTTI) && LIBATFRAME_UTILS_ENABLE_RTTI
  template <class Y, class F>
  static inline std::shared_ptr<Y> dynamic_pointer_cast(F&& f) {
    return std::dynamic_pointer_cast<Y>(std::forward<F>(f));
  }
#endif
};

然后把这些类型放进用于统一管理的 wal_log_operator 里。

template <class LogKeyT, class LogT, class ActionGetter, class CompareLogKeyT = std::less<LogKeyT>,
          class HashActionCaseT = std::hash<typename wal_log_action_getter_trait<LogT, ActionGetter>::type>,
          class EqualActionCaseT = std::equal_to<typename wal_log_action_getter_trait<LogT, ActionGetter>::type>,
          class Allocator = std::allocator<LogT>, wal_mt_mode MTMode = wal_mt_mode::kMultiThread>
class wal_log_operator {
  // ...
  static constexpr const wal_mt_mode mt_mode = MTMode;

template <class Y, class... ArgsT>
  static inline typename wal_mt_mode_data_trait<Y, mt_mode>::strong_ptr make_strong(ArgsT&&... args) {
    using alloc_type = typename std::allocator_traits<log_allocator>::template rebind_alloc<Y>;
    return wal_mt_mode_func_trait<mt_mode>::template allocate_strong<Y>(alloc_type(), std::forward<ArgsT>(args)...);
  }

  template <class Y, class Alloc, class... ArgsT>
  static inline typename wal_mt_mode_data_trait<Y, mt_mode>::strong_ptr allocate_strong(const Alloc& alloc,
                                                                                        ArgsT&&... args) {
    return wal_mt_mode_func_trait<mt_mode>::template allocate_strong<Y>(alloc, std::forward<ArgsT>(args)...);
  }

  template <class Y, class F>
  static inline typename wal_mt_mode_data_trait<Y, mt_mode>::strong_ptr static_pointer_cast(F&& f) {
    return wal_mt_mode_func_trait<mt_mode>::template static_pointer_cast<Y>(std::forward<F>(f));
  }

  template <class Y, class F>
  static inline typename wal_mt_mode_data_trait<Y, mt_mode>::strong_ptr const_pointer_cast(F&& f) {
    return wal_mt_mode_func_trait<mt_mode>::template const_pointer_cast<Y>(std::forward<F>(f));
  }

#if defined(LIBATFRAME_UTILS_ENABLE_RTTI) && LIBATFRAME_UTILS_ENABLE_RTTI
  template <class Y, class F>
  static inline typename wal_mt_mode_data_trait<Y, mt_mode>::strong_ptr dynamic_pointer_cast(F&& f) {
    return wal_mt_mode_func_trait<mt_mode>::template dynamic_pointer_cast<Y>(std::forward<F>(f));
  }
#endif

  template <class Y>
  using strong_ptr = typename wal_mt_mode_data_trait<Y, mt_mode>::strong_ptr;
  template <class Y>
  using weak_ptr = typename wal_mt_mode_data_trait<Y, mt_mode>::weak_ptr;
  template <class Y>
  using enable_shared_from_this = typename wal_mt_mode_func_trait<mt_mode>::template enable_shared_from_this<Y>;
};

还有一部分是我们工程里有很多读取策划通过配置Excel转出的数据,这部分访问量也很大。所以我对这个生成读表代码的模板系统 xres-code-generator 也做了改造支持。

其他业务相关的只能一个一个模块去替换了,我们也在持续替换中。

效果

其实在完成之前,其实我们也不确定到底能带来多大的提升。简单的循环benchmark其实也没啥意义,因为和实际使用场景的访问情况相差太远了。

我们在完成之后对我们实际项目路14-16个静态索引的交易行上下架请求和搜索的场景做了对比(不包含Excel读表改造),大概比 std::shared_ptr 提升了10%-16%的综合性能,这里面其实附带了其他的一些视图和索引的比较操作和其他RPC的操作。所以单单这个智能指针的部分提升应该是更大的。

未来规划

之前实现 libcopp 对C++20协程支持的时候也有几处内部生命周期引用的地方是计划中后续改成无Cache Miss的版本的,后续看有空也改造一下吧。也是能减少一些不必要的内部开销。

最终实现的代码开源在 https://github.com/owent/atframe_utils/blob/main/include/memory/rc_ptr.h

我们也在继续逐渐把一些本来也不是线程安全的模块都换成新智能指针,欢迎有兴趣的小伙伴互相交流研究。