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

推荐订阅源

T
Tenable Blog
MyScale Blog
MyScale Blog
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
博客园 - 司徒正美
D
Darknet – Hacking Tools, Hacker News & Cyber Security
量子位
N
News | PayPal Newsroom
S
Secure Thoughts
酷 壳 – CoolShell
酷 壳 – CoolShell
L
LINUX DO - 热门话题
有赞技术团队
有赞技术团队
V
Visual Studio Blog
T
Tailwind CSS Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Project Zero
Project Zero
B
Blog RSS Feed
J
Java Code Geeks
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
Cyberwarzone
Cyberwarzone
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
博客园 - 【当耐特】
Latest news
Latest news
T
Threat Research - Cisco Blogs
aimingoo的专栏
aimingoo的专栏
博客园_首页
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta
D
Docker
Forbes - Security
Forbes - Security
Help Net Security
Help Net Security
Apple Machine Learning Research
Apple Machine Learning Research
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
V2EX - 技术
V2EX - 技术
N
Netflix TechBlog - Medium
The Last Watchdog
The Last Watchdog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Threatpost
Cloudbric
Cloudbric
T
The Exploit Database - CXSecurity.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
Webroot Blog
Webroot Blog

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++生态) 指标上报的多线程优化和多拉取源点优化 通用RPC代码生成器 实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针) 手夯一个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
协程(libcopp)的Channel功能和CPU命中率优化
owent · 2025-03-13 · via I'm OWenT

blog-website

背景

设计 《libcopp对C++20协程的接入和接口设计》 的时候,由于C++20协程的promise和awaitable是链式关联的。所以当时设计promise和awaitable之间通过一个共享的context来通信交互。当时第一版实现直接使用了 std::shared_ptr 来管理共享引用,也预留了个规划是未来可以改成非线程安全的引用来减少不必要的Cache Miss开销。

大部分业务场景中,单单atomic的开销占比非常低。但是在密集计算的场景中,反复atomic操作带来的Cache Miss不仅仅影响自己,还影响后续其他的代码操作,考虑这些影响以后,总体而言开销就不能忽略不计了。 在计算密集的场景中,经验上通常是调度层处理线程安全问题,然后分发到worker的时候尽可能不要设计线程同步,这样比每个接口去保证线程安全有更好的新能。

在之前 《实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针)》 的分享中,我给我们项目组的交易行模块实现了通用的不使用 atomic 的 std::shared_ptr 替代品(strong_rc_ptrweak_rc_ptr)。在附带大量业务逻辑代码的实际业务场景的压测Case中,仅仅是替换索引的指针,最高可以提升16%的性能。

同时虽然 libcopp 的有栈协程部分可以有选项关闭掉部分atomic操作。但是栈池管理的部分仍然用的 std::shared_ptr ,关闭得并不彻底。

另一方面,之前在给框架层实现RPC框架的时候,有些时候需要其他同学实现新的接入层。虽然之前实现了 rpc::custom_resumerpc::custom_wait 来方便接入。但是这个缺乏严格的类型检查,而且需要组合使用,仍然会对一些同学造成困惑。现在golang的channel模型很多同学都比较习惯了。所以也想做一个类似 go channel 模型的抽象。方便使用和理解,也能在上面实施一些静态检查。

方案和实现

针对以上问题,这次的调整主要就是改动这两块。一个是 channel 模型组件,另一个是把 strong_rc_ptrweak_rc_ptr 移植入 libcopp 并且应用到所有数据管理层。

简化编译选项

libcopp 之前用 LIBCOPP_DISABLE_ATOMIC_LOCK, LIBCOPP_LOCK_DISABLE_THIS_MTLIBCOPP_LOCK_DISABLE_MT 分别控制原子操作是否用atomic,是否 this_coroutine 和 this_task 使用atomic防止冲突和是否关闭 task_manager 的加锁。这次再加入栈管理和C++20协程共享context管理层面的选项的话太过于复杂了。所以现在改成 LIBCOPP_ENABLE_MULTI_THREAD 统一管控是否启用线程安全。

C++20协程的 channel 模型实现

channel模型主要是提供一个receiver给调用房使用,还有一个sender给接入方使用。其实和之前 copp::generator_future + copp::generator_context 的能力是一样的。 但是当时为了最佳适应性,提供了一个vtable组件来做事件通知,结构大概这样:

template <class TCONTEXT>
class LIBCOPP_COPP_API_HEAD_ONLY generator_vtable {
 public:
  using context_type = TCONTEXT;
  using context_pointer_type = LIBCOPP_COPP_NAMESPACE_ID::memory::default_strong_rc_ptr<context_type>;
  using value_type = typename context_type::value_type;
  using await_suspend_callback_type = std::function<void(context_pointer_type)>;
  using await_resume_callback_type = std::function<void(const context_type&)>;

  // ...
 private:
  LIBCOPP_UTIL_INTRUSIVE_PTR_ATOMIC_TYPE intrusive_ref_counter_;
  await_suspend_callback_type await_suspend_callback_;
  await_resume_callback_type await_resume_callback_;
}

可以看到,除了引用计数外,回调函数使用的是 std::function , 它还是有一定开销的。所以当时有个计划是实现一个使用更轻量级的裸指针和一个不包含任何事件回调通知的版本。 对于channel功能而言,其实就是不需要任何事件回调的版本。

为了复用之前实现的 copp::somecopp::anycopp::all 这类设计模式的抽象,这次的优化通过对 copp::generator_future 特化实现来进行。

enum class generator_vtable_type : uint8_t {
  kDefault = 0,
  kLightWeight = 1,
  kNone = 2,
};

// 对外接口
template <class TVALUE, class TERROR_TRANSFORM = promise_error_transform<TVALUE>,
          generator_vtable_type VTABLE_TYPE = generator_vtable_type::kDefault>
class generator_future;

template <class TPROMISE, bool RETURN_VOID, generator_vtable_type VTABLE_TYPE>
class generator_awaitable;

template <class TVALUE, class TERROR_TRANSFORM = promise_error_transform<TVALUE>>
using generator_lightweight_future = generator_future<TVALUE, TERROR_TRANSFORM, generator_vtable_type::kLightWeight>;

template <class TVALUE, class TERROR_TRANSFORM = promise_error_transform<TVALUE>>
using generator_channel_future = generator_future<TVALUE, TERROR_TRANSFORM, generator_vtable_type::kNone>;

// ============ 特化回调类型 ============
template <class TCONTEXT>
struct generator_vtable_type_trait<TCONTEXT, generator_vtable_type::kDefault> {
  using context_type = TCONTEXT;
  using context_pointer_type = LIBCOPP_COPP_NAMESPACE_ID::memory::default_strong_rc_ptr<context_type>;
  using value_type = typename context_type::value_type;
  using await_suspend_callback_type = std::function<void(context_pointer_type)>;
  using await_resume_callback_type = std::function<void(const context_type&)>;
};

template <class TCONTEXT>
struct generator_vtable_type_trait<TCONTEXT, generator_vtable_type::kLightWeight> {
  using context_type = TCONTEXT;
  using context_pointer_type = LIBCOPP_COPP_NAMESPACE_ID::memory::default_strong_rc_ptr<context_type>;
  using value_type = typename context_type::value_type;
  using await_suspend_callback_type = void (*)(context_pointer_type);
  using await_resume_callback_type = void (*)(const context_type&);
};

// ============ 特化vtable容器代理 ============
template <class TCONTEXT>
class LIBCOPP_COPP_API_HEAD_ONLY generator_vtable_delegate<TCONTEXT, generator_vtable_type::kLightWeight> {
 public:
  using context_type = TCONTEXT;
  using vtable_type = generator_vtable<context_type, generator_vtable_type::kLightWeight>;
  using context_pointer_type = LIBCOPP_COPP_NAMESPACE_ID::memory::default_strong_rc_ptr<context_type>;
  using value_type = typename context_type::value_type;
  using await_suspend_callback_type = typename vtable_type::await_suspend_callback_type;
  using await_resume_callback_type = typename vtable_type::await_resume_callback_type;

  template <class TSUSPEND, class TRESUME>
  LIBCOPP_UTIL_FORCEINLINE generator_vtable_delegate(TSUSPEND&& await_suspend_callback,
                                                     TRESUME&& await_resume_callback) noexcept
      : vtable_(new vtable_type(std::forward<TSUSPEND>(await_suspend_callback),
                                std::forward<TRESUME>(await_resume_callback))) {}

  template <class TSUSPEND>
  LIBCOPP_UTIL_FORCEINLINE generator_vtable_delegate(TSUSPEND&& await_suspend_callback) noexcept
      : vtable_(new vtable_type(std::forward<TSUSPEND>(await_suspend_callback), nullptr)) {}

  LIBCOPP_UTIL_FORCEINLINE generator_vtable_delegate() noexcept : vtable_(nullptr) {}

  LIBCOPP_UTIL_FORCEINLINE generator_vtable_delegate(const generator_vtable_delegate& other) noexcept
      : vtable_(other.vtable_) {}

  LIBCOPP_UTIL_FORCEINLINE void trigger_suspend_callback(context_type& context) {
    if (vtable_ && vtable_->get_await_suspend_callback()) {
      vtable_->get_await_suspend_callback()(context.shared_from_this());
    }
  }

  LIBCOPP_UTIL_FORCEINLINE void trigger_resume_callback(const context_type& context) {
    if (vtable_ && vtable_->get_await_resume_callback()) {
      vtable_->get_await_resume_callback()(context);
    }
  }

 private:
  copp::memory::intrusive_ptr<vtable_type> vtable_;
};

template <class TCONTEXT>
class generator_vtable_delegate<TCONTEXT, generator_vtable_type::kNone> {
 public:
  using context_type = TCONTEXT;
  using context_pointer_type = LIBCOPP_COPP_NAMESPACE_ID::memory::default_strong_rc_ptr<context_type>;
  using value_type = typename context_type::value_type;

  generator_vtable_delegate() noexcept {}
  generator_vtable_delegate(const generator_vtable_delegate&) noexcept {}

  void trigger_suspend_callback(context_type& /*context*/) noexcept {}

  void trigger_resume_callback(const context_type& /*context*/) noexcept {}
};

最后提供一组类似 Rust的 std::sync::mpsc::channel 来创建channel就行了。

template <class TVALUE, class TERROR_TRANSFORM = promise_error_transform<TVALUE>>
using generator_channel_receiver = generator_channel_future<TVALUE, TERROR_TRANSFORM>;

template <class TVALUE, class TERROR_TRANSFORM = promise_error_transform<TVALUE>>
using generator_channel_sender = typename generator_channel_receiver<TVALUE, TERROR_TRANSFORM>::context_pointer_type;

template <class TVALUE, class TERROR_TRANSFORM = promise_error_transform<TVALUE>>
inline std::pair<generator_channel_receiver<TVALUE, TERROR_TRANSFORM>,
                 generator_channel_sender<TVALUE, TERROR_TRANSFORM>>
make_channel() {
  generator_channel_receiver<TVALUE, TERROR_TRANSFORM> future;
  generator_channel_sender<TVALUE, TERROR_TRANSFORM> context = future.get_context();
  return std::make_pair(std::move(future), std::move(context));
}

stackful协程的 channel 模型实现

stackful模式下之前没有现成的实现,为了保持使用上统一,所以需要新实现一个。 原理和结构也类似,也是receiver和sender通过共享的context来交互。

classDiagram
    stackful_channel_context <|-- stackful_channel_receiver
    stackful_channel_context <|-- stackful_channel_sender
    stackful_channel_context: -future data_
    stackful_channel_context: -variant callers_
    stackful_channel_context: +bool is_ready()
    stackful_channel_context: +bool is_pending()
    stackful_channel_context: +void reset_value()
    stackful_channel_context: +const value_type *get_value() const
    stackful_channel_context: +value_type *get_value()
    stackful_channel_context: +void add_caller(handle_delegate handle)
    stackful_channel_context: +bool remove_caller(handle_delegate handle)
    stackful_channel_context: +size_t resume_callers()
    stackful_channel_context: +bool has_multiple_callers() const
    class stackful_channel_receiver{
      -context_pointer_type context_;
      +bool is_ready()
      +bool is_pending()
      +void reset_value()
      +const value_type *get_value() const
      +value_type *get_value()
      +value_type inject_await(TCONTEXT *ctx, TERROR_TRANSFORM &&error_transform)
      +const context_pointer_type &get_context() const
      +context_pointer_type &get_context()
    }
    class stackful_channel_sender{
      -context_pointer_type context_;
      +bool is_ready()
      +bool is_pending()
      +void reset_value()
      +void set_value(U &&in)
      +const context_pointer_type &get_context() const
      +context_pointer_type &get_context()
    }

和C++20协程的 copp::generator_future 实现一样。这里也会对小的trivial类型执行 inplace 构造优化,所以后面的压力测试会区分 trivial 和 非trivial 两种值类型。 这里的caller也是记录无栈协程的上下文即可,然后增加了 inject_await 接口和stackful的协程和task对象交互。除此之外还有两处差异。

一是错误码转换是在调用 inject_await 的时候,也就是在执行等待的时候传入。这是为了类型数量可以尽可能少一点。

template <class TERROR_TRANSFORM>
value_type inject_await(coroutine_context *ctx, TERROR_TRANSFORM &&error_transform) noexcept(
    std::is_nothrow_copy_constructible<value_type>::value && noexcept(error_transform(COPP_EC_ARGS_ERROR))) {
  return internal_inject_await<coroutine_context>(ctx, std::forward<TERROR_TRANSFORM>(error_transform));
}

另一处是这里要stackful协程和task分别去适配注入。

// coroutine_context
template <class TAWAITABLE, class TERROR_TRANSFORM,
            class = nostd::enable_if_t<stackful_inject_awaitable<nostd::remove_cvref_t<TAWAITABLE>>::value>>
inline container_value_type<TAWAITABLE> await_value(TAWAITABLE &&awaitable, TERROR_TRANSFORM &&error_transform) noexcept(
      std::is_nothrow_copy_constructible<container_value_type<TAWAITABLE>>::value && noexcept(error_transform(COPP_EC_ARGS_ERROR))) {
  return awaitable.inject_await(this, std::forward<TERROR_TRANSFORM>(error_transform));
}

// task inject
LIBCOPP_COPP_NAMESPACE_BEGIN
template <class TCO_MACRO>
struct stackful_channel_resume_handle<LIBCOPP_COTASK_NAMESPACE_ID::task<TCO_MACRO>> {
  inline static int resume(void *invoke_task, stackful_channel_context_base *priv_data) {
    if (nullptr != invoke_task) {
      return reinterpret_cast<LIBCOPP_COTASK_NAMESPACE_ID::task<TCO_MACRO> *>(invoke_task)
          ->resume(reinterpret_cast<void *>(priv_data));
    }

    return 0;
  }
};
LIBCOPP_COPP_NAMESPACE_END

这是因为 cotask::task<T> 在执行resume后要做一些状态机切换的操作和触发完成事件,如果不单独注入无法及时触发完成事件和状态机切换。

最后在提供构造receiver和sender的接口就好了。

template <class TVALUE>
inline std::pair<stackful_channel_receiver<TVALUE>, stackful_channel_sender<TVALUE>> make_stackful_channel() {
  stackful_channel_receiver<TVALUE> receiver;
  stackful_channel_sender<TVALUE> sender{receiver.get_context()};
  return std::make_pair(std::move(receiver), std::move(sender));
}

strong_rc_ptrweak_rc_ptr

智能指针的实现和 《实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针)》 里一样,没啥特殊的。仅仅是多加了 default_rc_ptr_trait 来处理默认用带还是不带atomic的版本。

#if LIBCOPP_MACRO_ENABLE_MULTI_THREAD
using default_rc_ptr_trait = compat_strong_ptr_function_trait<compat_strong_ptr_mode::kStl>;
#else
using default_rc_ptr_trait = compat_strong_ptr_function_trait<compat_strong_ptr_mode::kStrongRc>;
#endif

template <class T>
using default_strong_rc_ptr = typename default_rc_ptr_trait::template shared_ptr<T>;

template <class T>
using default_weak_rc_ptr = typename default_rc_ptr_trait::template weak_ptr<T>;

template <class T>
using default_enable_shared_from_this = typename default_rc_ptr_trait::template enable_shared_from_this<T>;

template <class T, class... ArgsT>
LIBCOPP_UTIL_FORCEINLINE default_strong_rc_ptr<T> default_make_strong(ArgsT&&... args) {
  return default_rc_ptr_trait::template make_shared<T>(std::forward<ArgsT>(args)...);
}

template <class T, class Alloc, class... TArgs>
LIBCOPP_UTIL_FORCEINLINE default_strong_rc_ptr<T> default_allocate_strong(const Alloc& alloc, TArgs&&... args) {
  return default_rc_ptr_trait::template allocate_shared<T>(alloc, std::forward<TArgs>(args)...);
}

template <class T, class F>
LIBCOPP_UTIL_FORCEINLINE default_strong_rc_ptr<T> default_static_pointer_cast(F&& f) {
  return default_rc_ptr_trait::template static_pointer_cast<T>(std::forward<F>(f));
}

template <class T, class F>
LIBCOPP_UTIL_FORCEINLINE default_strong_rc_ptr<T> default_const_pointer_cast(F&& f) {
  return default_rc_ptr_trait::template const_pointer_cast<T>(std::forward<F>(f));
}

#if defined(LIBCOPP_MACRO_ENABLE_RTTI) && LIBCOPP_MACRO_ENABLE_RTTI
template <class T, class F>
LIBCOPP_UTIL_FORCEINLINE default_strong_rc_ptr<T> default_dynamic_pointer_cast(F&& f) {
  return default_rc_ptr_trait::template dynamic_pointer_cast<T>(std::forward<F>(f));
}
#endif

具体实现见: https://github.com/owent/libcopp/tree/v2/include/libcopp/utils/memory

压力测试对比和分析

下面测试分几组:

  • 低并发:基准 :同时运行协程数低,编译优化开 -O2
  • 低并发:仅命中率 :同时运行协程数低,编译优化开 -O2 ,关闭Atomic,去除task_manager锁
  • 低并发:Release :同时运行协程数低,编译优化开 -O3 ,关闭Atomic,去除task_manager锁
  • 高并发:基准 :同时运行协程数高,模拟随机访问,编译优化开 -O2
  • 高并发:仅命中率 :同时运行协程数高,模拟随机访问,编译优化开 -O2 ,关闭Atomic,去除task_manager锁
  • 高并发:Release :同时运行协程数高,模拟随机访问,编译优化开 -O3 ,关闭Atomic,去除task_manager锁
对比项 低并发:基准 低并发:仅命中率 低并发:Release 高并发:基准 高并发:仅命中率 高并发:Release
Stackful协程上下文(cotask::coroutine_context*)创建(动态栈池) - - - 66ns 41ns(-38%) 38ns(-43%)
Stackful协程上下文(cotask::coroutine_context*)切换 47ns 32ns(-32%) 25ns(-47%) 83ns 54ns(-35%) 42ns(-49%)
Stackful协程任务(cotask::task)创建(动态栈池) - - - 102ns 61ns(-40%) 64ns(-37%)
Stackful协程任务(cotask::task)切换 65ns 42ns(-35%) 34ns(-48%) 95ns 78ns(-18%) 68ns(-28%)
C++20协程任务(cotask::task_future)创建(Trivial值类型) - - - 98ns 78ns(-21%) 77ns(-22%)
C++20协程任务(cotask::task_future)创建(非Trivial值类型) - - - 109ns 91ns(-16%) 93ns(-15%)
C++20协程任务(cotask::generator_future)切换(Trivial值类型) 46ns 32ns(-31%) 28ns(-40%) 49ns 28ns(-43%) 31ns(-37%)
C++20协程任务(cotask::generator_future)切换(非Trivial值类型) 52ns 38ns(-27%) 39ns(-25%) 56ns 41ns(-27%) 44ns(-22%)
C++20协程Callable(cotask::callable_future)创建(Trivial值类型) - - - 57ns 53ns(-7%) 51ns(-3%)
C++20协程Callable(cotask::callable_future)创建(非Trivial值类型) - - - 55ns 55ns(-0%) 52ns(-11%)
C++20协程Callable(cotask::generator_future)切换(Trivial值类型) 34ns 23ns(-32%) 20ns(-41%) 35ns 27ns(-22%) 21ns(-40%)
C++20协程Callable(cotask::generator_future)切换(非Trivial值类型) 47ns 41ns(-13%) 34ns(-28%) 49ns 43ns(-12%) 38ns(-22%)
C++20协程轻量级Generator(cotask::generator_future,Trivial值类型) 47ns 29ns(-38%) 23ns(-51%) 52ns 36ns(-31%) 33ns(-36%)
C++20协程轻量级Generator(cotask::generator_future,非Trivial值类型) 54ns 44ns(-22%) 37ns(-31%) 60ns 49ns(-17%) 46ns(-23%)
C++20协程Channel(cotask::generator_future,Trivial值类型) 34ns 23ns(-32%) 20ns(-41%) 38ns 26ns(-32%) 26ns(-32%)
C++20协程Channel(cotask::generator_future,非Trivial值类型) 47ns 39ns(-17%) 33ns(-30%) 48ns 42ns(-13%) 39ns(-19%)

为什么上面的基准使用动态栈池的版本?这是因为我们实际项目里使用的是根据负载动态伸缩的栈池。 实际开销包含涉及模式带来的管理开销,比如一些简单地DAG能力,wait_all/wait_some/wait_anythen 接口,任务管理器注入等等。 所以这个组合和测试参考参考数据更贴近业务实际使用场景,而不是单纯为了“跑分”。

  • C++20协程ChannelC++20协程轻量级Generator 的Case都使用 C++20协程Callable 进行,数据都是触发协程切换的开销。
  • 低并发下创建开销误差大,无意义所以忽略数据。
  • 上面的切换开销都是一来+一回两次。在实际RPC使用场景中,一次RPC调用涉及IO写的时候切出和拿到回包后切入。这样更符合实际使用场景。
    • 有些对称式协程一般只测一次切换,要对比的话可以*2后对比。
  • C++20协程的 cotask::generator_future 组件本身创建开销大约是40ns左右。实际使用的时候可以池化复用,所以这里没有列举出 cotask::generator_future 的创建开销。
  • C++20协程涉及对分配,这个压测使用的是默认的内存分配器,没有使用 jemalloc/mimalloc/tcmalloc 等,有兴趣需要测试的同学可以自己加载PRELOAD跑。

来个图更直观一点。首先是 低并发场景

{ type: ‘line’, data: { labels: [‘基准’, ‘仅命中率’, ‘Release’], datasets: [ { label: ‘Stackful上下文切换’, data: [47, 32, 25], borderColor: ‘rgb(54, 162, 235)’, backgroundColor: ‘rgb(54, 162, 235)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘Stackful任务切换’, data: [65, 42, 34], borderColor: ‘rgb(75, 192, 192)’, backgroundColor: ‘rgb(75, 192, 192)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20任务切换(Trivial)’, data: [46, 32, 28], borderColor: ‘rgb(201, 203, 207)’, backgroundColor: ‘rgb(201, 203, 207)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20任务切换(非Trivial)’, data: [52, 38, 39], borderColor: ‘rgb(54, 72, 149)’, backgroundColor: ‘rgb(54, 72, 149)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Callable切换(Trivial)’, data: [35, 27, 21], borderColor: ‘rgb(166, 77, 121)’, backgroundColor: ‘rgb(166, 77, 121)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Callable切换(非Trivial)’, data: [47, 41, 34], borderColor: ‘rgb(77, 166, 166)’, backgroundColor: ‘rgb(77, 166, 166)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20轻量Generator(Trivial)’, data: [47, 29, 23], borderColor: ‘rgb(110, 177, 0)’, backgroundColor: ‘rgb(110, 177, 0)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20轻量Generator(非Trivial)’, data: [54, 44, 37], borderColor: ‘rgb(170, 110, 40)’, backgroundColor: ‘rgb(170, 110, 40)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Channel(Trivial)’, data: [34, 23, 20], borderColor: ‘rgb(0, 128, 128)’, backgroundColor: ‘rgb(0, 128, 128)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Channel(非Trivial)’, data: [47, 39, 33], borderColor: ‘rgb(128, 0, 128)’, backgroundColor: ‘rgb(128, 0, 128)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 } ] }, options: { plugins: { title: { display: true, text: ‘低并发场景下各操作性能优化趋势 (耗时越低越好)’ }, tooltip: { mode: ‘index’, intersect: false }, legend: { position: ‘right’, labels: { boxWidth: 12 } } }, scales: { x: { title: { display: true, text: ‘优化阶段’ } }, y: { title: { display: true, text: ‘耗时 (ns)’ } } } } }

然后是 高并发场景

{ type: ‘line’, data: { labels: [‘基准’, ‘仅命中率’, ‘Release’], datasets: [ { label: ‘Stackful上下文创建’, data: [66, 41, 38], borderColor: ‘rgb(255, 99, 132)’, backgroundColor: ‘rgb(255, 99, 132)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘Stackful上下文切换’, data: [83, 54, 42], borderColor: ‘rgb(54, 162, 235)’, backgroundColor: ‘rgb(54, 162, 235)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘Stackful任务创建’, data: [102, 61, 64], borderColor: ‘rgb(255, 159, 64)’, backgroundColor: ‘rgb(255, 159, 64)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘Stackful任务切换’, data: [95, 78, 68], borderColor: ‘rgb(75, 192, 192)’, backgroundColor: ‘rgb(75, 192, 192)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20任务创建(Trivial)’, data: [98, 78, 77], borderColor: ‘rgb(153, 102, 255)’, backgroundColor: ‘rgb(153, 102, 255)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20任务创建(非Trivial)’, data: [109, 91, 93], borderColor: ‘rgb(255, 205, 86)’, backgroundColor: ‘rgb(255, 205, 86)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20任务切换(Trivial)’, data: [49, 28, 31], borderColor: ‘rgb(201, 203, 207)’, backgroundColor: ‘rgb(201, 203, 207)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20任务切换(非Trivial)’, data: [56, 41, 44], borderColor: ‘rgb(54, 72, 149)’, backgroundColor: ‘rgb(54, 72, 149)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Callable创建(Trivial)’, data: [57, 53, 51], borderColor: ‘rgb(0, 148, 97)’, backgroundColor: ‘rgb(0, 148, 97)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Callable创建(非Trivial)’, data: [55, 55, 52], borderColor: ‘rgb(192, 75, 75)’, backgroundColor: ‘rgb(192, 75, 75)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Callable切换(Trivial)’, data: [35, 27, 21], borderColor: ‘rgb(166, 77, 121)’, backgroundColor: ‘rgb(166, 77, 121)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Callable切换(非Trivial)’, data: [49, 43, 38], borderColor: ‘rgb(77, 166, 166)’, backgroundColor: ‘rgb(77, 166, 166)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20轻量Generator(Trivial)’, data: [52, 36, 33], borderColor: ‘rgb(110, 177, 0)’, backgroundColor: ‘rgb(110, 177, 0)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20轻量Generator(非Trivial)’, data: [60, 49, 46], borderColor: ‘rgb(170, 110, 40)’, backgroundColor: ‘rgb(170, 110, 40)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Channel(Trivial)’, data: [38, 26, 26], borderColor: ‘rgb(0, 128, 128)’, backgroundColor: ‘rgb(0, 128, 128)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }, { label: ‘C++20 Channel(非Trivial)’, data: [48, 42, 39], borderColor: ‘rgb(128, 0, 128)’, backgroundColor: ‘rgb(128, 0, 128)’, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 } ] }, options: { plugins: { title: { display: true, text: ‘高并发场景下各操作性能优化趋势 (耗时越低越好)’ }, tooltip: { mode: ‘index’, intersect: false } }, scales: { x: { title: { display: true, text: ‘优化阶段’ } }, y: { title: { display: true, text: ‘耗时 (ns)’ } } } } }

总体来看,无论是高并发场景还是低并发场景,关闭atomic操作都能带来大约20%-30%的性能提升,提升量相当可观。 而进一步开启 -O3 编译后,大部分场景可以进一步提升,特别是对非Trivial类型比较明显。少量场景可能是过度inline反而导致Code Cache命中率下降从而开销轻微增加。

最新测试也可以查看 https://github.com/owent/libcopp/actions 里的CI输出。

最后

当前版本的优化已经发布到 https://github.com/owent/libcopp/releases/tag/v2.3.1 。未来可能可以针对小对象和对缓存命中率进一步优化。 也欢迎有兴趣的小伙伴们一起交流。