跳转至内容
  • A place to talk about whatever you want

    22 主题
    168 帖子
    没有新主题
  • 7 主题
    20 帖子
    SPeakS

    @dustchens 链表结构损坏, 不闭环了 (如果问题解决可以把帖子状态设置为已解决

  • 开源软件 | 开源社区 | 开源理念 | 开源与商业 | 开源可持续发展 等相关话的交流讨论
    注: 这里的"开源"是泛化的共建共享概念, 范围包含 OSI的范围、自由软件、CC等相关内容

    57 主题
    246 帖子
    MoYingJiM

    补一个 0BSD,这个许可证很多时候也被放在与 Unlicense 和 WTFPL 相提并论的(都是公共领域)

  • 60 主题
    253 帖子
    SPeakS
    GCC 16 上手初体验:环境配置、默认 C++20、模块化、诊断增强和 C++26 反射

    从去年发布GCC 15.1 并支持 import std开始, 我的新项目也全面转向C++23 + 模块化 (踩了不少坑 [逃]). 而最近几天 GCC 16.1 也正式发布了, 这篇文章就来初步的上手体验一下.

    主要内容包括:

    0.GCC 16 环境配置 1.默认标准: 从C++17升级到C++20, 正式进入C++2x时代 2.模块化: 有一定优化, 但任处于实验性支持阶段 (悲) 3.代码诊断和静态分析增强 4.C++ 26 反射实验性支持 5.总结及相关链接 0. 环境配置 (Linux)

    安装GCC 16可以选择按官方文档从源码进行构建 GCC WIKI 或 直接使用xlings工具安装预构建版本(注: 该方式有个好处不污染系统环境)

    安装xlings包管理工具

    curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/refs/heads/main/tools/other/quick_install.sh | bash source ~/.bashrc # 或重新打开一次控制台 xlings -h

    xlings工具安装后, 创建隔离环境并安装gcc 16

    xlings subos new gcc-test xlings subos use gcc-test xlings install gcc@16.1.0 -y gcc --version

    2026-05-04_22-10.png

    1.默认标准: 从 C++17 升级到 C++20, 正式进入 C++2x 时代

    GCC 16 的一个关键变化是: C++ 编译默认标准从 GNU++17 变成 GNU++20。不用额外再写 -std=c++20 也能编译下面的程序了:

    #include <iostream> consteval int answer() { return 16; } int main() { std::cout << "__cplusplus=" << __cplusplus << ", consteval=" << answer() << '\n'; }

    编译运行:

    g++ cpp20-default.cpp -o cpp20-default ./cpp20-default

    输出:

    __cplusplus=202002, consteval=16

    如果显式退回 C++17:

    g++ -std=c++17 cpp20-default.cpp -o cpp17-check

    会报错:

    t.cpp:3:1: error: 'consteval' does not name a type; did you mean 'constexpr'? 3 | consteval int answer() { | ^~~~~~~~~ | constexpr

    默认标准的改变, 不仅仅只是帮我们省了-std=c++20,而是默认构建行为的改变 会加速C++ 2x时代生态的发展

    2.模块化: 有一定优化, 但任处于实验性支持阶段

    模块化早在C++20就发布, 他的目标是想解决 C++ 头文件模型长期存在的问题: 重复解析、宏污染、包含顺序敏感、接口和实现边界不够清晰。在工程方面他对C++的影响是颠覆性的, 但是由于历史/兼容性/复杂度等多方面原因导致, GCC 16 的默认标准虽然是 GNU++20,但 modules 支持仍然是实验性的,所以GCC 16对模块化爱好者来说只能算波澜不惊

    只不过, GCC16 还是有在模块化做了一些工作的

    修复了一些模块化的bug 新增 --compile-std-module,用于更方便地构建标准库相关模块和 header unit

    其中 --compile-std-module 的增加 更方便大家体验模块化和import std, 避免了之前连一个hello world级别的main.cpp编译都要分几步的情况(有时候还有问题), 所以这也算是一大进步 - 现在可以像下面一样一键编译 import std 的程序了

    // main.cpp import std; int main() { std::println("{}", 42); }

    编译:

    g++ -std=c++23 -fmodules --compile-std-module main.cpp -o app ./app 3. 诊断增强: 错误信息结构化 + 编译期静态代码分析

    C++ 报错经常被吐槽,不是因为编译器不知道哪里错了,而是因为错误链太长,人很难从一堆候选函数、模板实例化和类型展开里看出主线。

    GCC 16 的一个明显变化是,部分 C++ 错误信息开始用层级结构展示。例如这个声明和定义不一致的例子:

    class Foo { public: void test(int i, int j, void *ptr, int k); }; void Foo::test(int i, int j, const void *ptr, int k) { } int main() { return 0; }

    编译:

    g++ diagnostics-mismatch.cpp

    GCC 16 会把候选函数和参数差异分层展示,核心信息大致是:

    error: no declaration matches 'void Foo::test(int, int, const void*, int)' - there is 1 candidate - candidate is: 'void Foo::test(int, int, void*, int)' - parameter 3 of candidate has type 'void*' - which does not match type 'const void*'

    这比以前只告诉你 找不到匹配声明 更有用,它一定程度指出了候选是谁、哪里不一致、哪一个参数出了问题。

    甚至还能生成更适合浏览器查看的诊断html页面:

    g++ diagnostics-mismatch.cpp \ -fdiagnostics-add-output=experimental-html

    2026-05-04_22-55.png

    如果想给 CI、IDE 或代码扫描平台使用,可以生成 SARIF:

    g++ diagnostics-mismatch.cpp \ -fdiagnostics-format=sarif

    这个的优化, 不仅 报错更容易看了,而且后面可能会影响工具链体验。编译器不再只是把错误打印到终端,而是可以把结构化诊断交给网页、编辑器、代码审查和静态分析系统。

    配合上 -fanalyzer 的能力。它是 GCC 自带的静态分析能力。如果加上 -Werror 体验就有点类似Rust的严格编译期检查功能

    看一个 use-after-delete:

    #include <iostream> int main() { int *p = new int(42); delete p; std::cout << *p << "\n"; }

    编译:

    g++ -std=c++20 -fanalyzer analyzer-uaf.cpp

    GCC 16 会给出类似警告:

    warning: use after 'delete' of 'p' [CWE-416] [-Wanalyzer-use-after-free]

    6ac11e44-be1d-4668-bd20-b4243eb62f81-image.png

    4. C++26 特性: Reflection / 反射

    GCC 16 实现了若干 C++26 特性,这里简单试一试

    -std=c++26 -freflection

    以一个简单的结构体为例, 看看 C++26反射 的功能:

    #include <iostream> #include <meta> #include <string_view> struct User { int id; double score; }; consteval std::size_t field_count() { auto fields = std::meta::nonstatic_data_members_of( ^^User, std::meta::access_context::unchecked() ); return fields.size(); } consteval std::string_view field_name(std::size_t index) { auto fields = std::meta::nonstatic_data_members_of( ^^User, std::meta::access_context::unchecked() ); return std::meta::identifier_of(fields[index]); } consteval std::string_view field_type(std::size_t index) { auto fields = std::meta::nonstatic_data_members_of( ^^User, std::meta::access_context::unchecked() ); return std::meta::display_string_of(std::meta::type_of(fields[index])); } int main() { constexpr auto type_name = std::meta::display_string_of(^^User); std::cout << type_name << " has " << field_count() << " fields\n"; std::cout << field_name(0) << ": " << field_type(0) << '\n'; std::cout << field_name(1) << ": " << field_type(1) << '\n'; }

    编译运行:

    g++ -std=c++26 -freflection reflection-user.cpp -o reflection-user ./reflection-user

    输出:

    User has 2 fields id: int score: double

    这里的 ^^User 是对 User 这个结构体类型做静态反射,std::meta::display_string_of 可以拿到类型的显示名。std::meta::nonstatic_data_members_of 则能拿到结构体的非静态数据成员列表,再通过 identifier_of 和 type_of 分别取得成员名和成员类型。

    C++ 代码可以在编译期拿到结构体自身的字段信息。这个功能对 序列化、ORM、RPC、命令行参数解析、UI 绑定、测试生成等大量现在依赖宏、模板技巧或外部代码生成的场景 有重要影响

    5. 总结

    GCC 16.1.0 感觉最核心的就是默认标准从C++17变成了C++20, 某种意义上也算正式迈入C++2x时代了, 下面是一些个人感觉

    1.默认 C++ 标准从 GNU++17 变成 GNU++20。能进一步加速C++生态进入C++2x的时代 2.C++20 modules特性 在 GCC 16 里只是做了部分优化, 相比GCC 15.1.0引入import std来说, 没有特别惊喜 3.诊断输出更结构化了。嵌套错误、HTML、SARIF 会让编译器错误更适合教学、CI 和 IDE 集成。 4.C++26 Reflection 已经可以初步尝试。对于个人项目可以上手用一用 (但可能存在很多潜在的坑)

    我个人是比较关注模块化的进展, 总体来说GCC16虽然模块化特性上没有过多惊喜, 但在import std之后, 模块化算是已经初步可以在项目里尝试的水平了, 并且我们 mcpp-community 现代C++爱好者社区 已经在 模块化的 项目/库的 工程化、包管理、工具链 等方面有一定的探索, 欢迎感兴趣的朋友加入讨论

    社区Github主页: https://github.com/mcpp-community C++23模块化项目: https://github.com/openxlings/xlings C++23模块化库: https://github.com/mcpplibs 相关链接 xlings: https://github.com/openxlings/xlings mcpp-community: https://github.com/mcpp-community GCC 16.1 Released: https://gcc.gnu.org/pipermail/gcc-announce/2026/000190.html GCC 16 Release Series: Changes, New Features, and Fixes: https://gcc.gnu.org/gcc-16/changes.html GCC 16.1.0: https://sourceware.org/pub/gcc/releases/gcc-16.1.0/
  • 36 主题
    74 帖子
    DoomjustinD

    在前面的写路径优化里,我们已经用 writev 这类手段减少了系统调用的次数。接下来这一篇要解决另一个方向的开销:数据复制

    传统的 send 流程里,用户态的数据会先被复制进内核 socket buffer,再由驱动发给网卡。对于大块数据和高频发送,这一次复制本身就是明显的开销。

    Linux io_uring 的 IORING_OP_SEND_ZC 就是为了避免这一步。但要用对它,需要理解它的完成语义——这是大多数人第一次用时最容易出错的地方。

    1. 为什么需要零拷贝发送

    普通 send 的流程:

    用户 buffer ↓ (内核 memcpy) kernel socket buffer ↓ (DMA 或驱动) 网卡

    SEND_ZC 想跳过中间那一步复制,直接让驱动从用户态内存读取:

    用户 buffer ↓ (直接 DMA,无 memcpy) 网卡

    代价呢?驱动访问内存的时间变长了,所以在这期间,用户态不能释放或改写这块 buffer

    2. 两个 CQE:数据结果 vs 内存释放通知

    SEND_ZC 的完成模式跟普通 send 不一样。它会回两个 CQE:

    第一个 CQE(不带 IORING_CQE_F_NOTIF,带 IORING_CQE_F_MORE)

    内核告诉你这次 send 的结果:成功了多少字节或者失败原因 但驱动仍在使用你的 buffer 协程此时不会恢复

    第二个 CQE(带 IORING_CQE_F_NOTIF,不带 IORING_CQE_F_MORE)

    内核通知:我已经用完你的这块 buffer,可以释放了 协程在这一刻恢复执行 await_resume() 返回第一个 CQE 里已经存好的发送结果

    所以从协程的角度,返回 = notification 已到 = 内核已停止引用你的 buffer。buffer 完全可以释放或改写。

    3. 用 tag type 在 API 层标记零拷贝意图

    如果 async_send_some 直接接收普通 buffer 加一个 bool 标志,很容易在调用点看不出端倪:

    // 危险写法:一眼看不出 buffer 需要特殊处理 co_await socket.async_send_some(payload, true);

    更好的做法是引入一个 tag type,强制显式说明:

    struct ZeroCopyT { std::span<const std::byte> span; }; template<std::ranges::contiguous_range T> auto zero_copy(const T& range) -> ZeroCopyT { return { std::as_bytes(std::span{ range }) }; } // 调用时意图清晰 co_await socket.async_send_some(net::zero_copy(payload));

    这样做的好处:

    类型强制约束:传错类型编译就过不了。 调用点意图清晰:看到 zero_copy(...) 立刻知道这块 buffer 要特殊对待。 完成语义差异明确:零拷贝和普通发送的完成流程完全不同,标签清楚地区分了两条路。 4. Awaiter 的状态机:按 CQE flags 区分两阶段

    对应 SendZCAwaiter 的核心就是这个 complete() 方法,按照 flags 区分 CQE 的含义:

    void SendZCAwaiter::complete(int result, std::uint32_t flags) noexcept { // 不带 NOTIF:这是数据发送结果 CQE,保存结果 if (!(flags & IORING_CQE_F_NOTIF)) set_result(result, flags); // 不带 MORE:标志最后一个 CQE,结束整个操作 if (!(flags & IORING_CQE_F_MORE)) { context().untrack(this); if (handle_) handle_.resume(); } }

    这里的关键点:

    第一个 CQE(不带 NOTIF,带 MORE):存下发送字节数或错误码,不恢复协程 第二个 CQE(带 NOTIF,不带 MORE):跳过结果保存(已有了),恢复协程

    时序是这样的:

    User coroutine SendZCAwaiter io_uring kernel | | | | co_await async_send_some | | | (net::zero_copy(buf)) | | |-------------------------> | | | await_suspend() | | - prepare SQE | | - track(this) | | | submit & wait | | |--------------------------> | | 🔄 (suspended) | | | | 1st CQE: send result | | complete(...) | | !(NOTIF)✓ | set_result(bytes sent) | | (MORE)✓ | (don't resume yet) | | | | | | 2nd CQE: notification | | complete(...) | | !(NOTIF)✗ | (skip set_result) | | (MORE)✗ | untrack(this) | | | handle_.resume() | | 🔄 (awoken) | | | auto result = | | | await_resume() | | | (return byte_sent_) | | | <-- proceed | |

    两个 CQE 都到了,buffer 安全可释放,协程才真正恢复。

    5. 实战边界条件

    虽然协程返回时 notification 已到,但从工程实践角度有几点值得注意:

    Buffer 生命周期的本质约束
    本质上只需要保证 buffer 在协程返回前保持有效。由于协程返回 = notification 已到,这个条件在实际代码中很容易满足。对于栈上的 std::string、std::vector 或其他作用域内存,这不成问题。只有当 buffer 是通过 new 分配且在其他线程被 delete 时,才会违反这个约束——但这种情况通常表明应用层本身的内存管理有问题。

    内存被占用的时间,比想象中还要长(TCP ACK 陷阱)
    第 1 节提到"驱动访问内存的时间变长了",乍一看像是微秒级的 DMA 操作。但在真实场景中,这个"变长"往往不是驱动的事儿,而是毫秒到秒级的网络 RTT。很多网卡驱动和协议栈实现中,内核必须等到对端返回 TCP ACK 确认包,确认数据不需要重传了,才会吐出带 NOTIF 的第二个 CQE。这意味着 SEND_ZC 的 notification 延迟直接和恶劣的物理网络环境强绑定:丢包、重传、网络拥塞都会拉长等待时间。在一个跨洲际链路上发送,notification 可能要等好几秒,期间内存始终被内核占用。这对内存规划和资源隔离的影响不可忽视。

    大文件的"内存锁定"爆炸(RLIMIT_MEMLOCK)
    虽然大文件(MB 级)发送时零拷贝有明显优势,但绝对不能一次性把几个 GB 的大文件全都梭哈给 SEND_ZC。原因是:内核在等待 notification 期间,会把这块物理内存 Pin 住(锁定,防止被 Swap)。如果瞬间提交过大内存,极易触发操作系统的 RLIMIT_MEMLOCK 限制导致直接报错,或者把物理内存撑爆。工业界的正确做法是分片(Chunking):用一个 while 循环,每次 co_await socket.async_send_some(net::zero_copy(chunk)) 发送几 MB,等这几 MB 的 notification 回来(协程唤醒)后,再发下一块。这样既能享受零拷贝的收益,又能保持内存占用在可控范围内。

    错误也要等 notification
    即使第一个 CQE 返回错误,completion 流程也要走完(等 notification)。不要假设出错时内核会跳过 notification。

    示例调用(小消息场景):

    std::string payload = "Zero-copy message from io_uring SEND_ZC\n"; auto result = co_await socket.async_send_some(net::zero_copy(payload)); if (!result) { log::error("send_zc failed: {}", result.error()); co_return; } // 作用域结束时 payload 自动销毁,此时已安全

    大文件分片示例:

    std::ifstream file("large_file.bin", std::ios::binary); const size_t chunk_size = 1024 * 1024; // 1 MB std::vector<char> buffer(chunk_size); while (file.read(buffer.data(), chunk_size)) { size_t bytes_read = file.gcount(); auto result = co_await socket.async_send_some( net::zero_copy(std::span(buffer.data(), bytes_read)) ); if (!result) { log::error("send chunk failed: {}", result.error()); break; } // notification 回来,内存可重用于下一块 } 6. 小结

    SEND_ZC 的核心是一个两阶段完成模型

    第一个 CQE:发送是否成功、发了多少字节 第二个 CQE:内核通知"我用完你的 buffer 了"

    协程在第二个 CQE 到来时才恢复,这样调用方既得到了发送结果,也确切知道何时可以释放 buffer。

    在 API 层用 ZeroCopyT tag type 强制显式选择零拷贝,在 awaiter 层按 CQE flags 正确分发两阶段完成,这两层结合才是鲁棒的设计——既不会因为侥幸巧合而偶然正确,也能清晰表达意图。

  • 一个技术知识分享、学习、交流的社区

    16 主题
    54 帖子
    sunrisepeakS

    @dustchens 已支持

  • Got a question? Ask away!

    4 主题
    14 帖子
    SPeakS

    备注一下使数学公式的使用语法

    单行公式语法 - $ 你的公式 $

    $ log_2^n $

    $ log_2^n $

    多行公式语法 - $$ 你的公式 $$

    $$ log_2^n => log_2^9 = 3 , n = 9 $$

    $$
    log_2^n =>
    log_2^9 = 3, n = 9
    $$

公告栏 | Bulletin Board

欢迎加入d2learn社区 - 社区指南
Welcome to the d2learn Community - Community Guide

一个以 [知识、技术、代码、项目、想法、开源] 相关话题为主导的社区
A community focused on topics related to [knowledge, technology, code, projects, ideas, and open source].


在线用户