猎头公司网站建设方案,wordpress 插件中心,网站整体迁移该怎么做,台州网站关键字优化详情一、背景#xff1a;为什么需要 Atomic Min / Max#xff08;P0493#xff09;
1⃣ 问题本质
在多线程程序中#xff0c;经常会出现这样的需求#xff1a; 多个线程并发地更新一个共享变量#xff0c;用来记录 最小值 / 最大值 数学上可以抽象为#xff1a; x ← min…一、背景为什么需要 Atomic Min / MaxP04931⃣ 问题本质在多线程程序中经常会出现这样的需求多个线程并发地更新一个共享变量用来记录最小值 / 最大值数学上可以抽象为x ← min ( x , v i ) 或 x ← max ( x , v i ) x \leftarrow \min(x, v_i) \quad \text{或} \quad x \leftarrow \max(x, v_i)x←min(x,vi)或x←max(x,vi)其中x xx是共享原子变量v i v_ivi是线程i ii提供的候选值2⃣ 没有原子操作会发生什么如果用普通变量或非原子更新if(vx)xv;在并发下会出现读-改-写非原子线程交错导致丢更新lost update数据不一致、不可预测行为UB3⃣ 为什么“加锁”不够好虽然mutex能解决正确性问题但高争用时性能差阻塞 → 上下文切换难以构建lock-free/wait-free结构4⃣ Atomic Min / Max 的核心价值把 “比较 更新” 合并成一个原子操作保证update ( x ) ; 是线性化的 \text{update}(x) ;\text{是线性化的}update(x);是线性化的从而实现无锁并发更新数据完整性高并发可扩展性二、典型应用场景Atomic min / max 在以下领域非常常见Lock-free 数据结构并发优先级队列最小时间戳 / 最大序号维护并行归约Parallel ReductionOpenMP / TBB / GPU-style reduction例如并行计算最小误差、最大评分优化算法全局最优解best-so-far分支限界branch bound统计信息收集延迟最大值最小响应时间峰值资源占用三、C26 提议接口标准层面1⃣ 非成员函数形式namespacestd{templateclassTTatomic_fetch_max(atomicT*obj,typenameatomicT::value_type arg)noexcept;templateclassTTatomic_fetch_max_explicit(atomicT*obj,typenameatomicT::value_type arg,memory_order order)noexcept;// 同样提供 atomic_fetch_min / _explicit}2⃣ 返回值语义非常重要与atomic_fetch_add一致返回的是“更新前的旧值”形式化表达old x before \text{old} x_{\text{before}}oldxbefore并执行x ← max ( x , arg ) x \leftarrow \max(x, \text{arg})x←max(x,arg)3⃣ 成员函数版本atomic标准还会提供std::atomicTa;autoolda.fetch_max(v);autoolda.fetch_max(v,std::memory_order_relaxed);四、内存序memory_order语义默认版本atomic_fetch_max(a,v);等价于atomic_fetch_max_explicit(a,v,std::memory_order_seq_cst);显式版本的使用原则统计 / reduction→memory_order_relaxed同步 / 发布语义→release/acquire数学上可以理解为atomicity ; ≠ ; ordering \text{atomicity} ;\neq; \text{ordering}atomicity;;orderingAtomic min/max 只保证原子性内存顺序仍需你自己选。五、实现方式标准并不强制怎么实现C26 只规定语义实现依赖平台。六、实现方式一CAS LoopCompare-And-Swap1⃣ 基本 CAS 实现T olda.load();while(oldv!a.compare_exchange_weak(old,v)){// old 被更新为当前值}returnold;数学模型loop until { x ≥ v or CAS succeeds \text{loop until } \begin{cases} x \ge v \\ \text{or CAS succeeds} \end{cases}loop until{x≥vor CAS succeeds2⃣ 问题高争用时自旋严重cache line ping-pong性能随线程数恶化七、实现方式二Optimized CAS优化点包括先load判断是否需要 CAS减少不必要的 CAS 尝试使用compare_exchange_weak backoff这种实现在低~中等争用下性能可接受在无硬件指令的平台上是主力方案八、实现方式三硬件直接支持最快1⃣ Direct Instruction部分架构提供类似指令x86LOCK CMPXCHG组合优化GPU原生 atomic min/maxRISC-V / ARM特定原子扩展效果单条或少量指令极低延迟极高吞吐2⃣ LL/SCLoad-Link / Store-Conditional在 ARM / PowerPC 上LL r0, [x] cmp r0, v SC r1, v, [x]若中途被打断SC失败自动重试。九、性能对比总结实现方式特点性能基本 CAS简单、可移植高争用差优化 CAS工程友好可接受LL/SC架构支持很好硬件指令原生支持最优十、为什么 C26 才加原因总结各架构支持不统一早期API 设计需要与 memory_order 对齐避免滥用 / 误用与 GPU / 并行算法需求成熟度有关十一、一句话总结工程视角Atomic min/max 把“并发归约”的核心原语正式提升为标准一等公民。公式化总结atomic_fetch_max atomic ( max ) \text{atomic\_fetch\_max} \text{atomic}( \max )atomic_fetch_maxatomic(max)它不是语法糖而是lock-free 结构的地基并行算法的标配高性能统计与优化的关键原语一、这篇论文到底在解决什么问题一句话版把“并发条件更新conditional write”这个长期存在、硬件早已支持的原语正式引入 Catomic标准库。用公式表示就是把下面这个并发模式x ← max ( x , v ) 或 x ← min ( x , v ) x \leftarrow \max(x, v) \quad\text{或}\quad x \leftarrow \min(x, v)x←max(x,v)或x←min(x,v)升级为标准原子读-改-写操作RMW。二、历史背景为什么不是新东西却拖了这么久1⃣ 原子加法比原子 max/min 还“年轻”fetch_add1982NYU Ultracomputerfetch_max / fetch_min1988 年前后就已经出现比很多人想象的要早但问题是各架构语义不一致C 内存模型成熟得比较晚conditional write 在标准里一直是“灰色地带”2⃣ 为什么应用层强烈需要论文列的动机本质可以归为四类Lock-free 数据结构无锁队列序号 / 时间戳推进并行归约reductionOpenMP 的reduction(max: ...)数据并行统计优化 / 搜索算法global best-so-farbranch bound 剪枝统计信息最大延迟峰值内存最大输入规模这些场景的共同点是写入发生不频繁但读取和比较非常频繁三、核心难点conditional write 的语义之争这是整篇论文最关键、也最有争议的部分。1⃣ 什么是 conditional write伪代码if(newold)oldnew;在并发里这可以有两种实现语义2⃣ 方案 Aread-modify-writeRMW无论值是否改变都算一次“写”x ← max ( x , v ) x \leftarrow \max(x, v)x←max(x,v)特点和fetch_add完全一致每个参与线程都要抢 cache line高争用下性能差3⃣ 方案 Bread-and-conditional-storeRCS只有真的变大 / 变小才写if v x then x ← v \text{if } v x \text{ then } x \leftarrow vifvxthenx←v特点写入次数显著减少硬件ARM / RISC-V / POWER天然支持性能非常好但问题是这不是严格意义上的 RMW4⃣ 标准委员会的最终结论R3 → R5语义上规定为 RMW实现上允许用 RCS 修补也就是行为像 RMW但实现可以“偷懒”四、三种 CAS-loop 实现对比5.1 / 5.2 / 5.3这是论文里工程含量最高的部分。5.1 非 conforming不符合标准的实现while(max(v,t)!t){if(pv-compare_exchange_weak(t,v))break;}特点如果值没变 → 完全不写不满足 RMW 语义被标准否掉5.2 纯 RMW 实现最“老实”的while(!pv-compare_exchange_weak(t,max(v,t)));特点每轮都会写cache line 抢占严重高争用下性能灾难5.3“聪明”的折中实现最终推荐autot(mr!m)?pv-fetch_add(0,m):pv-load(mr);while(max(v,t)!t){if(pv-compare_exchange_weak(t,v,m,mr))returnt;}returnt;这个实现的关键思想只有当 memory_order 要求 release 时才强制一次写否则允许只读不写用fetch_add(0)人为制造一次“可观察的写”为什么这是合法的吗因为额外一次 dummy 写 ; ⇏ ; 可观测语义变化 \text{额外一次 dummy 写} ;\not\Rightarrow; \text{可观测语义变化}额外一次dummy写;⇒;可观测语义变化在 C 内存模型下这是不可区分的行为。五、为什么没有 infix operator现状→ 有→ 有max→ 不存在能不能提供“返回新值”的接口比如autonewa.fetch_max_new(v);论文结论是不值得原因max/min是不可逆操作新值可以用max ( old , v ) \max(\text{old}, v)max(old,v)即autoolda.fetch_max(v);autonewvstd::max(old,v);所以最终只保留fetch_xxx返回旧值六、指针语义为什么这么“严格”标准规定fetch_max/fetch_min对指针的比较等价于使用std::max / std::min也就是说只能比较指向同一个 complete object或同一数组内的指针否则不形成 strict weak ordering→行为未定义为什么不像其他 atomic 那样“兜底”原因是fetch_add可能产生“新值”fetch_max只会返回旧值或调用者提供的值不“创造”新指针所以不提供 escape clause工程建议论文暗示如果你真的需要atomicT*a;跨对象比较自己转成uintptr_t七、为什么浮点被移除了这是 R5 的重要变化。根本原因浮点的 max/min 不是良序问题点NaN0 / -0IEEE 754 特殊规则例如max ( NaN , x ) ≠ x \max(\text{NaN}, x) \neq xmax(NaN,x)x结论std::min / std::max本身就对浮点特殊处理原子语义会更复杂已有P3008专门处理本提案专注整数 指针八、Benchmark为什么硬件支持这么重要测试平台ARM Graviton364 核对比CAS-loopsmart硬件指令ldsmax结果趋势简化版核数CAS-loop硬件指令2相近相近8CAS 明显慢稳定32CAS 急剧上升线性增长结论硬件原生 min/max 是数量级优势九、标准改动总结 新增内容包括非成员函数atomic_fetch_max atomic_fetch_min成员函数atomicT::fetch_max atomicT::fetch_minatomic_ref支持整数 指针特化feature test macro__cpp_lib_atomic_min_max十、整篇论文的“真正价值”总结P0493 不是“加几个 API”而是把“条件更新”正式纳入 C 原子语义体系。公式化总结conditional write ; ⟶ ; standard RMW primitive \text{conditional write} ;\longrightarrow; \text{standard RMW primitive}conditional write;⟶;standard RMW primitive你可以把它理解为lock-free 算法的“最后一块基础砖”并行 reduction 的原生支持为 GPU / heterogeneous 并发铺路一、示例代码在“并发层面”到底做了什么#includeatomic#includethread#includevectorstd::atomicintmax_value{0};①std::atomicint max_value{0};这是一个原子共享变量所有线程都会并发读取它并发尝试更新它不需要互斥锁mutexvoidfind_max_in_range(intstart,intend){for(intistart;iend;i){max_value.fetch_max(i,std::memory_order_relaxed);}}②fetch_max的语义核心这一句在语义上等价于old ← max_value max_value ← max ( old , i ) \text{old} \leftarrow \text{max\_value} \ \text{max\_value} \leftarrow \max(\text{old}, i)old←max_valuemax_value←max(old,i)但整个过程是原子的不可被打断。③ 多线程下会发生什么假设两个线程并发执行线程 Afetch_max(137)线程 Bfetch_max(245)最终结果满足max_value max ( … , 137 , 245 ) \text{max\_value} \max(\dots, 137, 245)max_valuemax(…,137,245)顺序不确定但结果确定。intmain(){std::vectorstd::threadthreads;for(inti0;i10;i){threads.emplace_back(find_max_in_range,i*100,(i1)*100);}④ 线程划分方式共 10 个线程每个线程处理一个区间[ 0 , 100 ) , [ 100 , 200 ) , … , [ 900 , 1000 ) [0,100), [100,200), \dots, [900,1000)[0,100),[100,200),…,[900,1000)这保证了所有i都会被尝试写入理论最大值应为max 999 \max 999max999for(autothread:threads){thread.join();}⑤join()的并发语义非常重要join()不是原子操作但它提供了一个线程完成的 happens-before 保证即线程内的所有操作 ; ; happens-before ; ; join() 返回 \text{线程内的所有操作} ;;\text{happens-before};; \text{join() 返回}线程内的所有操作;;happens-before;;join()返回std::coutMaximum value: max_valuestd::endl;⑥ 为什么这里读max_value是安全的因为所有线程已经join()不再有并发写入即使fetch_max用的是memory_order_relaxedjoin 提供了同步而不是 atomic 本身二、为什么memory_order_relaxed在这里是完全正确的这是这段代码最容易被误解的地方。1⃣ 我们真正关心的是什么我们关心的是max_value 最终等于所有 i 的最大值 \text{max\_value 最终等于所有 i 的最大值}max_value最终等于所有i的最大值我们不关心每一次fetch_max的先后顺序某个线程“看到”另一个线程的中间状态2⃣relaxed保证了什么std::memory_order_relaxed保证两件事原子性atomicity修改是不可撕裂的no tearing但不保证顺序可见性传播时机与其他内存的关系3⃣ 为什么这已经足够因为这里的计算是一个幂等归约idempotent reductionmax ( max ( a , b ) , c ) max ( a , max ( b , c ) ) \max(\max(a,b),c) \max(a,\max(b,c))max(max(a,b),c)max(a,max(b,c))也就是说顺序完全不重要不需要 happens-before不需要跨变量同步这是relaxed 的经典使用场景三、Memory Ordering 强弱层级从弱到强你列出的层级可以用一条“语义单调增强链”表示relaxed ⊂ acquire / release ⊂ acq_rel ⊂ seq_cst \text{relaxed} \subset \text{acquire / release} \subset \text{acq\_rel} \subset \text{seq\_cst}relaxed⊂acquire / release⊂acq_rel⊂seq_cst1⃣memory_order_relaxedfetch_max(v,relaxed)原子性✘ 不同步其他内存✘ 不建立 happens-before适合计数器max/min统计并行 reduction2⃣memory_order_releasefetch_max(v,release)语义在这次原子操作之前的所有写入对随后 acquire 读取它的线程可见数学化表示writes before → release atomic \text{writes}_\text{before} \xrightarrow{\text{release}} \text{atomic}writesbeforereleaseatomic3⃣memory_order_acquireload(acquire)语义看到这个原子值⇒ 一定能看到对应的 release 写入atomic → acquire reads after \text{atomic} \xrightarrow{\text{acquire}} \text{reads}_\text{after}atomicacquirereadsafter4⃣memory_order_acq_relfetch_max(v,acq_rel)对写release对读acquire用于状态推进阶段切换lock-free 协议5⃣memory_order_seq_cst最强fetch_max(v,seq_cst)保证所有seq_cst原子操作存在一个全局总序∃ single total order \exists\text{single total order}∃single total order性能代价最高四、什么时候不能用relaxed考虑这种情况structData{intpayload;};Data data;std::atomicintmax_version;线程 Adata.payload42;max_version.fetch_max(1,std::memory_order_release);线程 Bif(max_version.load(std::memory_order_acquire)1){// 希望看到 payload 42}这里必须release acquire否则 B 可能看到max_version 1但payload仍是旧值五、为什么fetch_max特别适合 relaxed从数学角度看max : ( T , T ) → T \max: (T, T) \to Tmax:(T,T)→T满足交换律结合律幂等性这意味着只要保证单次操作原子整体结果天然正确这正是 relaxed 的理想适用场景。六、把这段代码用一句“专家级描述”总结这是一个无锁、无同步依赖、基于幂等归约的并行最大值计算使用atomic::fetch_maxmemory_order_relaxed正确性由原子性 join 同步共同保证。一、Current Status提案目前处在什么阶段1⃣ 提案历史与状态最早提出时间2016 年目标标准C26当前版本P0493R52024 年 2 月实现经验已在Clang中进行过实验性实现表明该设计可实现、可优化、可落地这类信息在 WG21 语境中意味着“该提案已经过多年打磨进入收敛阶段”2⃣ 为什么拖了这么久关键原因不是整数而是浮点原子 min/max 的语义争议整数版本语义清晰与数学min / max \min / \maxmin/max一致无 NaN、无符号零问题浮点版本涉及 IEEE-754 的角落语义标准库内部历史包袱极重二、Open Questions委员会仍在讨论什么1⃣ 是否支持浮点类型问题本质是否存在一个“足够一致、可移植”的浮点 min/max 语义 \text{是否存在一个“足够一致、可移植”的浮点 min/max 语义}是否存在一个“足够一致、可移植”的浮点min/max语义GPU / ARM已经有硬件支持x86支持在增长标准库语义仍未完全统一2⃣ 是否提供“返回新值”的变体当前提案接口是Tatomic_fetch_max(atomicT*,T);语义return old_value \text{return old\_value}return old_value开放问题是否也应该支持Tatomic_fetch_max_new(...);即return new_value max ( o l d , i n p u t ) \text{return new\_value} \max(old, input)return new_valuemax(old,input)委员会通常更保守增加 API 意味着 ABI / 教学成本可通过一次 load fetch 实现三、结论性判断整数版本Atomic min/max operations are valuable additions原因总结为三点正确性消除数据竞争明确原子语义性能避免 mutex利用硬件原子指令适用性高并发高扩展性适用场景包括Lock-free 数据结构并行归约OpenMP / TBB优化算法统计与监控四、Atomic Floating-Point Min/MaxP3008这是当前最难、也是最有争议的部分。1⃣ 为什么浮点比整数难浮点存在三个“非直觉点”(1) NaNNot a NumberNaN不满足自反性NaN ≠ NaN \text{NaN} \neq \text{NaN}NaNNaN比较结果不稳定NaN x , ; NaN x , ; NaN x ; 均为 false \text{NaN} x,; \text{NaN} x,; \text{NaN} x ;\text{均为 false}NaNx,;NaNx,;NaNx;均为false(2) 有符号零0 与 −0− 0.0 0.0 但符号位不同 -0.0 0.0 \quad \text{但符号位不同}−0.00.0但符号位不同比较关系并非完全直觉。(3) IEEE-754 历史差异不同平台、不同指令对NaNsigned zero处理方式不同。2⃣ C 中已有的“混乱历史”(a)std::min / std::maxstd::min(NaN,2.0f);// Undefined Behavior标准明确UB这是为了性能 简化语义(b) C 函数族fmin / fmax将 NaN 视为missing data返回“非 NaN 的值”signed zero 行为依实现© C23 新引入的一组函数函数NaN 处理signed zerofminimumNaN error− 0 0 -0 0−00fmaximumNaN error− 0 0 -0 0−00fminimum_numNaN missing− 0 0 -0 0−00fmaximum_numNaN missing− 0 0 -0 0−003⃣ 硬件现状非常关键GPU / ARM 的事实GPU原生支持 atomic float min/maxNaN 直接忽略ARM v8.1提供原子浮点最值指令语义接近fminimum_num即硬件已经在用一种“事实标准”。五、P3008 的设计取向1⃣ 提议的浮点原子接口floatfetch_min(float,memory_orderseq_cst)noexcept;floatfetch_max(float,memory_orderseq_cst)noexcept;语义约束NaN行为未指定unspecifiedsigned zero推荐− 0 0 -0 0−00但非强制这里用的是unspecified而不是 UB是一次重大改进2⃣ 明确语义版本更安全基于 C23floatfetch_fminimum(...);floatfetch_fmaximum(...);floatfetch_fminimum_num(...);floatfetch_fmaximum_num(...);语义对应关系fminimum / fmaximumNaN 错误fminimum_num / fmaximum_numNaN 缺失数据数学上fminimum_num ( x , NaN ) x \text{fminimum\_num}(x, \text{NaN}) xfminimum_num(x,NaN)x六、为什么委员会现在“敢”推进浮点版本原因有三硬件已经在用GPU / ARM 已落地C23 已统一语义C 与 C 再次对齐经验教训足够多std::min/max的 UB 被认为是失败设计七、最终总结专家视角Atomic min/max整数已经是“无争议必需品”。浮点版本不再追求完美而是追求“可实现、可解释、可移植”。核心思想转变从“数学完美” → “工程可用” \text{从“数学完美”} \rightarrow \text{“工程可用”}从“数学完美”→“工程可用”Higher Thread CountModern GPUsAtomic Float Min/Max OperationsSome newer CPUsTraditional ApproachIncreased ContentionNative Hardware SupportCAS Loop ImplementationCAS Performance DegradationHigh PerformanceLower PerformanceLeads toCausesProvideCan useFallback toEnablesStarting to provideUsesResults in一、核心结论先给答案在高并发高线程数场景下Native atomic min/max ≫ CAS loop 实现 \text{Native atomic min/max} \gg \text{CAS loop 实现}Native atomic min/max≫CAS loop实现性能差距会随着并发度上升而指数式放大。原因不是“实现技巧”而是硬件一致性协议与冲突模型的根本不同。二、CAS loop为什么会在高并发下性能崩塌1⃣ CAS loop 的基本结构一个典型的fetch_maxCAS 实现T oldatomic.load(relaxed);while(oldvalue!atomic.compare_exchange_weak(old,value,relaxed,relaxed)){// old 被更新为当前值继续重试}returnold;逻辑抽象为repeat until success: { read x try write max ( x , v ) \text{repeat until success:} \begin{cases} \text{read } x \\ \text{try write } \max(x, v) \end{cases}repeat until success:{readxtry writemax(x,v)2⃣ 高并发下的本质问题冲突概率设N NN 并发线程数所有线程都在更新同一个 cache line那么在任意时间点P ( CAS success ) ≈ 1 N P(\text{CAS success}) \approx \frac{1}{N}P(CAS success)≈N1即一个线程成功N − 1 N-1N−1个线程失败、回退、重试3⃣ CAS loop 的性能退化链条对应图左侧(1) Higher Thread Count⬇(2) Increased Contention同一 cache lineMESI 协议频繁失效cache line 在核之间“抖动”(3) Increased Contention⬇(4) CAS Performance Degradation退化体现在重试次数↑ \uparrow↑cache miss↑ \uparrow↑pipeline flush↑ \uparrow↑内存序列化↑ \uparrow↑可以抽象为Latency ∗ C A S ≈ O ( N ⋅ T ∗ c o h e r e n c e ) \text{Latency}*{CAS} \approx O(N \cdot T*{coherence})Latency∗CAS≈O(N⋅T∗coherence)三、Native atomic min/max为什么硬件支持能“救命”1⃣ 硬件原子 ≠ CAS 循环关键区别CAS loop 是“读 → 比较 → 写可能失败”原生原子指令是“比较 写 一个不可分割的硬件事务”2⃣ 硬件实现方式取决于架构常见路径包括单指令原子GPU、ARMLL/SCLoad-Link / Store-Conditional内部 microcodex86 扩展统一特性不会在失败后反复读 cache失败路径极短减少 coherence 往返3⃣ 抽象性能模型对原生 atomic min/maxLatency n a t i v e ≈ O ( 1 ) \text{Latency}_{native} \approx O(1)Latencynative≈O(1)对 CAS loopLatency C A S ≈ O ( contention ) \text{Latency}_{CAS} \approx O(\text{contention})LatencyCAS≈O(contention)当线程数N → ∞ N \to \inftyN→∞Latency ∗ C A S Latency ∗ n a t i v e → ∞ \frac{\text{Latency}*{CAS}}{\text{Latency}*{native}} \to \inftyLatency∗nativeLatency∗CAS→∞四、SVG 图的逐层语义解读下面我用“图 → 含义”的方式解释你 SVG 中的每一块。第一层Top LevelHigher Thread Count并行规模增大GPU / many-core CPU 的常态Modern GPUs数千线程几乎必然高冲突Atomic Float Min/Max Operations抽象 API可映射到原生指令或 CAS fallbackSome newer CPUsARM v8.1正在补齐硬件支持Traditional Approach历史实现完全依赖 CAS loop第二层Second LevelIncreased Contentioncache line 被频繁 invalidation写者竞争Native Hardware Support原子在硬件层完成不暴露中间状态CAS Loop Implementationcompare_exchange 重试coherence 风暴中心第三层Third LevelCAS Performance Degradation延迟急剧上升吞吐下降High PerformanceGPU / 原生 atomic延迟稳定Lower PerformanceCAS fallback并发度越高越糟五、为什么这对浮点 atomic min/max特别重要因为浮点 min/max 常见于归约图像处理物理模拟机器学习归约 高并发 高冲突GPU 已证明没有原生原子就不可用这也是 P3008 强调“Hardware reality must inform standard design.”六、一个形式化总结可以直接放在 slide 里对共享变量x xx并发执行x ← min ( x , v i ) x \leftarrow \min(x, v_i)x←min(x,vi)CAS 实现cost ∼ O ( N ) \text{cost} \sim O(N)cost∼O(N)原生原子cost ∼ O ( 1 ) \text{cost} \sim O(1)cost∼O(1)当N NN很大时只有原生 atomic min/max 具有可扩展性。七、工程实践建议非常重要如果平台支持原生 atomic min/max一定要用即使语义稍微宽松如 NaN如果只能 CAS fallback尽量分片sharding局部归约 最终合并降低冲突频率最终一句话总结Atomic min/max 的标准化价值不在“语法”而在“性能可扩展性”。没有原生支持高并发下的 CAS 方案迟早会成为瓶颈。一、代码整体在表达什么这段代码想说明的是如果 C 提供原生的 atomic floating-pointfetch_min / fetch_max那么它们的使用方式、返回值语义、并发行为将与整数版本保持一致。也就是返回旧值原子地更新为 min/max无需 CAS loop二、逐行详细解析#includeatomic#includecmath#includeiostreamatomic提供std::atomicTcmath在真实实现中浮点 min/max 语义通常与fmin / fmax / fminimum_num等相关iostream演示输出std::atomicfloatatomic_value(10.0f);含义创建一个原子浮点变量初始值为10.0f在抽象内存模型中a t o m i c v a l u e 10.0 atomic_value 10.0atomicvalue10.0重要背景在C23 / C26 之前std::atomicfloat存在但没有fetch_min / fetch_max成员函数这里的代码是基于 P3008Atomic FP min/max提案语义的示例第一段fetch_maxfloatnew_max15.0f;floatresult_maxatomic_value.fetch_max(new_max);语义按提案fetch_max(x)的抽象语义是old a t o m i c _ v a l u e a t o m i c _ v a l u e max ( old , x ) return old \text{old} atomic\_value \\ atomic\_value \max(\text{old}, x) \\ \text{return old}oldatomic_valueatomic_valuemax(old,x)return old代入当前数值max ( 10.0 , 15.0 ) 15.0 \max(10.0, 15.0) 15.0max(10.0,15.0)15.0所以result_max 10.0atomic_value 15.0std::coutOld max: result_max, New max: atomic_valuestd::endl;输出解释Old max: 10, New max: 15完全符合fetch-then-modify的 RMW 语义与整数fetch_add / fetch_max行为一致第二段fetch_minfloatnew_min5.0f;floatresult_minatomic_value.fetch_min(new_min);此时原子变量的值是a t o m i c v a l u e 15.0 atomic_value 15.0atomicvalue15.0计算min ( 15.0 , 5.0 ) 5.0 \min(15.0, 5.0) 5.0min(15.0,5.0)5.0但注意示例输出中并没有发生更新这是因为示例假设的语义是“min/max 只在更小/更大时才写入”而给出的Expected output是Old min: 15, New min: 15说明此示例的隐含语义是a t o m i c _ v a l u e min ( 15.0 , 5.0 ) 15.0 atomic\_value \min(15.0, 5.0) 15.0atomic_valuemin(15.0,5.0)15.0也就是说这里的fetch_min并不是std::min意义上的 min而是“更新为更小者如果更小”的conditional update。这正是P3008 / 硬件 atomic min/max 的真实语义“如果新值不会改变结果可以不写”三、为什么第二次没有更新关键点抽象规则P3008 推荐设旧值为x xx新值为v vvf e t c h _ m i n ( v ) : { x ← v if v x x ← x otherwise fetch\_min(v): \begin{cases} x \leftarrow v \text{if } v x \\ x \leftarrow x \text{otherwise} \end{cases}fetch_min(v):{x←vx←xifvxotherwise当前v 5 , x 15 v 5, x 15v5,x15但示例输出却保持为15说明这是在演示一种硬件友好的“min-so-far”语义而不是数学意义上的x min ( x , v ) x \min(x, v)xmin(x,v)四、这正是浮点 atomic 的“敏感点”1⃣ NaN 问题如果floatvNaN;那么std::min(x, NaN)→未定义fmin(x, NaN)→ 返回x硬件 atomic →通常把 NaN 当“缺失值”所以 P3008 明确指出NaN 行为是“未指定”或“硬件一致即可”2⃣ Signed Zero 问题浮点中− 0.0 ≠ 0.0 -0.0 \neq 0.0−0.00.0但-0 0是否成立硬件大多认为成立标准只“推荐”不强制五、并发角度为什么 fetch_min/max 很重要在多线程中你实际是在做x min ( x , v 1 , v 2 , … , v n ) x \min(x, v_1, v_2, \dots, v_n)xmin(x,v1,v2,…,vn)这类模式出现在并行归约reductionGPU kernel统计最大误差 / 最小能量机器学习 loss tracking如果没有原生 atomic只能do{oldload();newmin(old,v);}while(!CAS(old,new));其成本为O ( contention ) O(\text{contention})O(contention)如果有原生 atomicO ( 1 ) O(1)O(1)六、结论部分逐条解释Atomic floating-point min/max 是现代并发系统的刚需GPU / many-core CPU 已大量使用标准滞后于硬件需要谨慎对待 corner casesNaNSigned zero浮点比较不满足全序性能决定一切Native atomic ≫ CAS loop尤其在高并发、热点变量上。语义应贴近硬件现实P3008 的核心哲学不要强行用std::min的“数学完美性”去约束已经存在的硬件原子语义提供更精细的 API例如fetch_fminimum fetch_fminimum_num让用户显式选择 NaN 处理方式七、一句话总结可直接放在最后一页Atomic 浮点 min/max 的价值不在“语法糖”而在于让并发归约真正具备可扩展性。 1 ns~ 0 nsasynchronousamortizedProtect objectUse objectUnprotect objectRemove objectRetire objectReclaim object// // 可被 Hazard Pointer 机制管理的对象基类// templatetypenameT,typenameDdefault_deleteTclasshazard_pointer_obj_base{public:// retire 的语义将对象标记为“退休retired”//// 含义// 1. 当前线程声明我已经不再需要该对象// 2. 对象不会立刻析构delete// 3. 对象会被放入 Hazard Pointer 的 retired list// 4. 只有当系统确认// “没有任何 hazard_pointer 正在保护该对象”// 时才会真正调用析构//// 数学化条件描述// 对象可安全析构 ⇔// $$ \forall hp,\; hp.ptr \neq this $$//// D 是析构器deleter// - 默认为 default_deleteT// - 可用于自定义释放逻辑内存池、统计等//// noexcept// - 退休操作必须是无异常的// - 否则会破坏并发回收算法的安全性voidretire(D dD())noexcept;};// // hazard_pointer线程声明“我正在使用这个指针”// classhazard_pointer{public:// 构造一个“空”的 hazard_pointer//// 含义// - 当前不保护任何对象// - 等价于 hazard pointer 中存储的是 nullptr//// 空 hazard_pointer 不会阻止任何对象被回收hazard_pointer()noexcept;// 只允许移动构造//// 原因// - 每个 hazard_pointer 对应一个全局注册槽位// - 拷贝会导致多个对象指向同一个槽位 → 数据竞争hazard_pointer(hazard_pointer)noexcept;// 只允许移动赋值hazard_pointeroperator(hazard_pointer)noexcept;// 析构函数//// 行为// - 自动取消当前的保护如果有// - 释放 hazard pointer 在全局表中的占用~hazard_pointer();// 判断当前 hazard_pointer 是否为空//// 返回 true 表示// - 没有保护任何指针// - 不影响任何对象的回收[[nodiscard]]boolempty()constnoexcept;// // protect核心操作发布 hazard// templatetypenameTT*protect(constatomicT*src)noexcept;//// 语义说明// 1. 从原子指针 src 中读取一个指针值 p// 2. 将 p 写入当前 hazard_pointer发布保护// 3. 再次验证 src 仍然等于 p// - 若改变则重试//// 保证// - 返回的指针在 hazard_pointer 生命周期内// 不会被回收//// 等价伪代码// do {// p src.load()// hp p// } while (src.load() ! p)//// 这是经典 Hazard Pointer “读-验证”模式// // try_protect非阻塞版本的保护// templatetypenameTbooltry_protect(T*ptr,constatomicT*src)noexcept;//// 行为// - 尝试一次性建立保护// - 若 src 在过程中发生变化返回 false//// 适用场景// - 无锁算法中希望避免自旋// - 更偏向“尝试-失败-回退”逻辑//// 成功返回 true// - ptr 被成功保护// - 对象在保护期间不会被回收// // reset_protection取消保护// templatetypenameTvoidreset_protection(constT*ptr)noexcept;//// 含义// - 取消对指定 ptr 的保护// - 允许该对象在未来被回收//// 注意// - 仅取消保护不会触发立即回收// 取消当前 hazard_pointer 的所有保护//// 等价于// hp nullptr//// 常用于// - 读临界区结束// - 提前释放保护以降低回收延迟voidreset_protection(nullptr_tnullptr)noexcept;// 交换两个 hazard_pointer//// 语义// - 交换其底层的保护槽位// - 不影响被保护对象的正确性voidswap(hazard_pointer)noexcept;};// // 工厂函数创建“非空”的 hazard_pointer// // 构造一个已经分配好全局槽位的 hazard_pointer//// 对比默认构造// - make_hazard_pointer() → 可立即使用// - hazard_pointer() → 空对象可能需要后续初始化hazard_pointermake_hazard_pointer();// 交换两个 hazard_pointer 的自由函数版本//// 提供与 std::swap 兼容的接口voidswap(hazard_pointer,hazard_pointer)noexcept;// T 继承自 hazard_pointer_obj_base// 表示该类型的对象可以被 Hazard Pointer 机制安全回收classT:publichazard_pointer_obj_baseT{/* T members */};// 原子指针指向当前“对外可见”的 T 对象// 多线程同时读写必须是 std::atomicT*std::atomicT*src_;// 高频调用的读路径read path// 特点// - 多线程并发调用// - 必须无锁 / 极低开销// - 不能阻塞 update()// - 不能访问已被释放的对象templatetypenameFuncUreadAndAccess(Func userFn){// Called frequently// 创建一个 hazard_pointer//// 语义// - 在全局 hazard pointer 表中占用一个槽位// - 用于声明“我正在使用某个对象”//// 生命周期// - 从这里开始到函数结束// - 在此期间被保护的对象禁止回收hazard_pointer hpmake_hazard_pointer();// Construct hazard pointer.// 从原子指针 src_ 中读取当前对象指针并建立保护//// protect 的关键保证// 1. 读取 src_// 2. 将读取到的指针发布到 hazard pointer// 3. 再次验证 src_ 未发生变化//// 成功返回后保证// $$ ptr \neq nullptr \Rightarrow ptr 在 hp 生命周期内不会被 delete $$//// 即使另一个线程同时调用 update() 并 retire 旧对象// 该对象也不会被释放直到 hp 释放保护T*ptrhp.protect(src_);// Get pointer to a protected object.// 在 hazard pointer 保护范围内安全访问对象//// userFn 可以// - 读取 ptr 的成员// - 调用 const / 非 const 方法//// 安全性保证// - ptr 不会悬空no use-after-free// - 即使 update() 并发执行returnuserFn(ptr);// 函数结束时// - hp 析构// - hazard pointer 自动取消保护// - 若没有其他线程保护该对象则允许回收}// 低频调用的写路径update path// 特点// - 通常写少读多// - 允许一定开销// - 不能阻塞读线程voidupdate(T*newptr){// Called infrequently// 原子交换// - 将 src_ 更新为 newptr// - 返回旧的指针 oldptr//// 这是一个线性化点linearization point// - 在这一瞬间所有后续读线程只能看到 newptrT*oldptrsrc_.exchange(newptr);// 将旧对象标记为“退休retired”//// 重要// - retire() 并不会立刻 delete oldptr// - oldptr 会被加入 hazard pointer 的退休列表//// 回收条件// $$ \forall hp,\; hp.ptr \neq oldptr $$//// 即// - 当且仅当没有任何线程的 hazard_pointer// 正在保护 oldptr// - 才会真正执行 delete//// 因此// - 不会阻塞读线程// - 不会产生 use-after-freeoldptr-retire();// Pass to hazard pointer library for safe reclamation.}一、Hazard Pointer 是什么一句话版Hazard Pointer危险指针是一种无锁内存回收机制用来保证当一个线程正在使用某个动态对象时其他线程不会把它释放掉。核心目标只有一个Use-after-free ⇒ 避免 \text{Use-after-free} \Rightarrow \text{避免}Use-after-free⇒避免二、C26 中 Hazard Pointers 的背景Hazard pointers protect dynamic objects from being reclaimed, allowing safe access to protected objects without additional synchronization逐句拆解protect dynamic objects保护的是动态分配对象heap objectfrom being reclaimed防止它们被回收delete / freeallowing safe access允许线程安全地访问对象without additional synchronization不需要 mutex / lock / refcount用公式表示这个保证如果线程T i T_iTi声明H P i p HP_i pHPip那么系统必须保证∀ t ∈ [ protect , unprotect ] , p 不会被 reclaim \forall t \in [\text{protect}, \text{unprotect}],\quad p \text{ 不会被 reclaim}∀t∈[protect,unprotect],p不会被reclaim三、图中流程的整体含义非常重要你给的 SVG 实际上是Hazard Pointer 的完整生命周期图分成左右两条“时间线”。左侧读线程 / 使用线程这是fast path几乎是纳秒级 1ns。1⃣ Protect object保护对象hp.store(ptr);含义把指针ptr发布到线程本地 hazard slot告诉系统“我现在要用这个对象了别删”这是一个非常短的原子操作。2⃣ Use object使用对象图中绿色的大块区域// 任意读操作autoxptr-field;ptr-method();关键点可以很长可以无锁可以跨函数不需要再同步3⃣ Unprotect object取消保护hp.clear();表示“我不用它了你们可以考虑回收了”左侧时间特征图中标注 1 ns~ 0 ns说明Hazard Pointer 的读路径几乎是“零成本”这就是它存在的意义。四、右侧写线程 / 删除线程这是slow path但异步 均摊。4⃣ Remove object逻辑删除list.remove(node);从数据结构中断开但不 delete此时object is unreachable but still alive \text{object is unreachable but still alive}object is unreachable but still alive5⃣ Retire object标记为待回收retire(node);含义“这个对象不再被任何结构引用但可能还有线程在用”对象进入retired list。6⃣ Reclaim object真正回收deletenode;发生条件∀ H P i , H P i ≠ n o d e \forall HP_i,\quad HP_i \neq node∀HPi,HPinode也就是说没有任何 hazard pointer 指向它右侧时间特征图中标注asynchronousamortized说明回收不在关键路径成本被分摊不阻塞读线程五、Hazard Pointer 的核心不变量Invariant整个机制围绕一个不变量( ∃ H P i p ) ; ⇒ ; p must not be reclaimed (\exists\ HP_i p) ;\Rightarrow; p \text{ must not be reclaimed}(∃HPip);⇒;pmust not be reclaimed反过来p can be reclaimed ; ⇔ ; ( ∀ H P i , H P i ≠ p ) p \text{ can be reclaimed} ;\Leftrightarrow; (\forall HP_i,\ HP_i \neq p)pcan be reclaimed;⇔;(∀HPi,HPip)六、为什么这对 C 标准很重要在 C26 之前Hazard Pointer每家自己实现API 不统一很难写泛型无锁结构C26 引入标准化版本意味着lock-free 容器可以进标准库用户代码不用关心回收细节编译器 / 库可以优化实现七、“Hazard Pointer Extensions beyond C26” 在暗示什么这是你标题里最有意思的一行。暗示的方向包括但不限于1⃣ 更通用的回收策略epoch-based reclamationhybrid HP epochNUMA-aware HP2⃣ 更强的语言集成编译器自动插入 protect / unprotect基于生命周期分析与std::atomic_ref、std::rcu协作3⃣ 更低开销的实现减少全局扫描cache-line 友好布局每核 hazard domain八、Hazard Pointer vs 其他方案简表方案读路径写路径适合场景mutex慢慢简单refcount中中共享所有权epoch极快批量批处理hazard pointer极快慢但异步无锁结构九、一句话总结非常关键Hazard Pointer 的本质不是“延迟删除”而是把“对象是否还能被释放”的判断从“时间”变成“是否被任何线程声明正在使用”。用一句公式收尾$$\text{Memory Safety} \text{Explicit Usage Declaration}\text{Deferred Reclamation}$$一、hazard_pointer_obj_base把“可回收性”挂到对象上templatetypenameT,typenameDdefault_deleteTclasshazard_pointer_obj_base{voidretire(D dD())noexcept;};1⃣ 设计目的这是一个CRTP 基类让对象具备“我可以被 hazard pointer 系统安全回收”的能力。关键点T真实对象类型D删除器支持自定义 allocator2⃣retire()的真实语义oldptr-retire();并不等于 delete。而是retire ( p ) ⇒ p ∈ RetiredList \text{retire}(p) \Rightarrow p \in \text{RetiredList}retire(p)⇒p∈RetiredList表示这个对象逻辑上已经死亡但物理内存是否释放要等安全时机二、hazard_pointer读线程使用的保护工具classhazard_pointer{hazard_pointer()noexcept;// emptyhazard_pointer(hazard_pointer)noexcept;hazard_pointeroperator(hazard_pointer)noexcept;~hazard_pointer();1⃣ RAII move-only不可拷贝可移动析构时自动 unprotect保证scope end ⇒ unprotect \text{scope end} \Rightarrow \text{unprotect}scope end⇒unprotect2⃣ 查询状态[[nodiscard]]boolempty()constnoexcept;是否未绑定任何 hazard slot用于调试 / 复用3⃣ 核心protecttemplatetypenameTT*protect(constatomicT*src)noexcept;语义这是经典 HP 循环的标准封装伪代码do{psrc.load();hp.store(p);}while(src.load()!p);returnp;数学表达p protect ( s r c ) ⇒ { p s r c p 已被 hazard 保护 p \text{protect}(src) \Rightarrow \begin{cases} p src \\ p \text{ 已被 hazard 保护} \end{cases}pprotect(src)⇒{psrcp已被hazard保护4⃣try_protect非阻塞版本templatetypenameTbooltry_protect(T*ptr,constatomicT*src)noexcept;如果src改变返回false不自旋适合低延迟路径5⃣ 手动重置保护templatetypenameTvoidreset_protection(constT*ptr)noexcept;voidreset_protection(nullptr_tnullptr)noexcept;用途同一个hazard_pointer保护不同对象显式声明“我不用这个对象了”6⃣swap高效管理 hazard slotvoidswap(hazard_pointer)noexcept;O(1)无额外同步7⃣ 创建非空 hazard pointerhazard_pointermake_hazard_pointer();区别于默认构造分配 hazard slot注册到 HP 域三、完整使用示例解析非常重要1⃣ 可回收对象类型classT:publichazard_pointer_obj_baseT{/* T members */};意义T 的生命周期由 Hazard Pointer 系统管理2⃣ 共享原子指针std::atomicT*src_;这是无锁结构的入口指针。3⃣ 高频读路径fast pathUreadAndAccess(Func userFn){hazard_pointer hpmake_hazard_pointer();T*ptrhp.protect(src_);returnuserFn(ptr);}行为分解分配 hazard slot一次性保护src_当前值在保护期内安全使用重要不变量userFn ( p t r ) 执行期间 p t r 不会被 delete \text{userFn}(ptr) \text{ 执行期间 } ptr \text{ 不会被 delete}userFn(ptr)执行期间ptr不会被delete4⃣ 低频写路径slow pathvoidupdate(T*newptr){T*oldptrsrc_.exchange(newptr);oldptr-retire();}行为分解原子替换把旧对象交给 HP 系统不阻塞读线程数学形式exchange ⇒ logical remove \text{exchange} \Rightarrow \text{logical remove}exchange⇒logical removeretire ⇒ eventual reclaim \text{retire} \Rightarrow \text{eventual reclaim}retire⇒eventual reclaim四、Hazard Pointer 的正确性不变量再次强调整个系统只依赖一个条件( ∃ H P i p ) ⇒ p must not be reclaimed (\exists\ HP_i p) \Rightarrow p \text{ must not be reclaimed}(∃HPip)⇒pmust not be reclaimed而回收条件是p reclaimed ⟺ ( ∀ H P i , H P i ≠ p ) p \text{ reclaimed} \iff (\forall HP_i,\ HP_i \neq p)preclaimed⟺(∀HPi,HPip)五、P3135R1Hazard Pointer 扩展提案beyond C26你给的内容已经是委员会讨论层级的东西了。1⃣ 不需要扩展的部分● Protection Counting多 hazard 指向同一对象已可在库层实现不需要语言支持● Asynchronous Reclamation Execution目前的设计已经是异步均摊标准不强制执行策略2⃣ 提议中的标准扩展Synchronous reclamation同步回收含义“在确定安全时立即回收而不是排队”数学条件∀ H P i , H P i ≠ p ; ⇒ ; delete ( p ) \forall HP_i,\ HP_i \neq p ;\Rightarrow; \text{delete}(p)∀HPi,HPip;⇒;delete(p)用途低内存环境实时系统确定性延迟Batch creation and destruction批量创建 / 销毁目的降低 hazard slot 管理开销提高 cache locality提供批量 API例如autohpsmake_hazard_pointersN();3⃣ 为什么这些没进 C26原因很现实ABI / API 稳定性风险实现复杂度各家库已有不同实现委员会选择先标准化“最小可用、可证明正确”的核心六、Hazard Pointer 在 C 生态中的地位你现在看到的是第一次把无锁内存回收正式带入标准为 lock-free 容器铺路为 future RCU / Epoch 奠基七、一句话总结终局版C26 Hazard Pointers 把“我正在用这个对象”从隐含的时间假设变成了显式、可验证的协议。最终公式$$\text{Safety} \text{Explicit Protection}\text{Deferred Reclamation}$$一、为什么不需要扩展 C26 的部分1⃣ Protection Counting保护计数含义是什么Protection Counting指的是同一个对象被多个 hazard pointer 同时保护需要记录“被保护的次数”。形式化表示prot_count ( p ) # H P i ∣ H P i p \text{prot\_count}(p) \#{ HP_i \mid HP_i p }prot_count(p)#HPi∣HPip为什么不需要进入标准原因有三层1. Hazard Pointer 的安全性不依赖计数HP 的基本不变量是( ∃ H P i p ) ⇒ p 不能被回收 (\exists HP_i p) \Rightarrow p \text{ 不能被回收}(∃HPip)⇒p不能被回收而不是prot_count ( p ) 0 \text{prot\_count}(p) 0prot_count(p)0只要知道有没有人保护而不是有多少人保护。2. 计数可以完全在库内部实现例如扫描所有 hazard slot用哈希表 / 排序数组统计不影响用户 API3. 不同实现策略差异极大per-object 计数per-domain 批量统计线程本地缓存如果标准化反而限制实现自由度。结论Protection Counting 是“实现细节”不是“语言或库语义”。2⃣ Execution of Asynchronous Reclamation异步回收执行含义是什么指的是何时、在哪个线程执行真正的delete例如写线程后台 GC 线程某次 retire 时顺带执行为什么不需要标准化1. 语义已完全确定HP 已经规定p reclaim ⟺ ( ∀ H P i , H P i ≠ p ) p \text{ reclaim} \iff (\forall HP_i,\ HP_i \neq p)preclaim⟺(∀HPi,HPip)什么时候做、谁来做不影响正确性。2. 应用场景差异巨大场景最佳策略游戏引擎写线程顺带回收实时系统后台低优先级服务器批量 amortized3. 标准只需定义“允许”不需定义“如何执行”这符合 C 一贯哲学Standard defineswhat, nothow.结论异步回收是策略问题不是接口问题。二、为什么需要扩展的部分下面是P3135R1 的核心动机。三、扩展 1Synchronous Reclamation同步回收1⃣ 现状问题C26当前 C26 HP 是必然异步最终回收无时间保证形式化p ∈ RetiredList ⇒ ∃ t , delete ( p ) at time t p \in \text{RetiredList} \Rightarrow \exists t,\ \text{delete}(p) \text{ at time } tp∈RetiredList⇒∃t,delete(p)at timet但t 未定义。2⃣ 同步回收要解决什么有些系统不能接受“以后再回收”实时系统RTOS嵌入式内存极小确定性延迟系统他们希望“如果现在没人用我现在就删。”3⃣ 同步回收的语义同步回收意味着( ∀ H P i , H P i ≠ p ) ⇒ delete ( p ) 立即执行 (\forall HP_i,\ HP_i \neq p) \Rightarrow \text{delete}(p) \text{ 立即执行}(∀HPi,HPip)⇒delete(p)立即执行即retire 时或显式调用时阻塞检查 hazard slots若安全 → 立刻回收4⃣ 为什么这是“扩展”而不是默认因为代价很大需要全局扫描破坏 lock-free 的进度保证可能引入延迟抖动5⃣ 设计取舍总结维度异步回收同步回收延迟不确定确定吞吐高低实时性差强适用场景通用RT / embedded四、扩展 2Batch Creation and Destruction批量创建 / 销毁1⃣ 现状问题C26当前hazard_pointer hpmake_hazard_pointer();每次分配 hazard slot注册析构时回收在高频路径中开销不可忽略。2⃣ 批量 API 的核心思想一次性做H P 1 , H P 2 , … , H P n {HP_1, HP_2, \dots, HP_n}HP1,HP2,…,HPn而不是H P 1 H P 2 ⋯ H P n HP_1 HP_2 \dots HP_nHP1HP2⋯HPn3⃣ 性能优势① amortized 成本cost ∗ b a t c h ≪ n × cost ∗ s i n g l e \text{cost}*{batch} \ll n \times \text{cost}*{single}cost∗batch≪n×cost∗single② cache localityhazard slots 连续存储扫描时顺序访问减少 cache miss③ 更适合算法级结构例如一次遍历多个指针树 / 图算法多指针一致性保护4⃣ 为什么这是标准级别的扩展因为它影响API 形态生命周期管理可移植性库作者没法在不暴露接口的情况下通用实现。五、P3135R1 的整体定位一句话C26 给了“正确性下限”P3135R1 给“工程可控性”。总结表终局版项目是否进 C26原因Protection Counting实现细节异步回收执行策略差异同步回收提议实时 / 确定性批量创建 / 销毁提议性能 可扩展性// ---------------- 第一种情况析构函数不依赖外部资源 ----------------templateclassTclassContainer{// Obj 继承自 hazard_pointer_obj_base// 表示 Obj 的生命周期由 Hazard Pointer 机制管理// retire() 后并不会立即 deleteclassObj:hazard_pointer_obj_baseObj{T data;// 实际存储的数据/* etc */// 其它成员};// 向容器中插入一个新对象voidinsert(T data){// 动态分配一个 Obj// 此时对象处于“活跃live”状态Obj*objnewObj(data);/* etc */// 将 obj 挂入容器的内部数据结构}// 从容器中删除对象voiderase(Args args){// 查找要删除的对象Obj*objfind(args);/* Remove obj from container */// 从容器逻辑结构中移除 obj// 从这一刻起后续读线程不再能通过容器访问到它// 将对象标记为“退休retired”//// 重要语义// - retire() 只是声明“我不再需要这个对象”// - 并不会立刻执行 delete// - 实际析构发生的时间是不确定的//// 对象只有在// 所有线程都不再持有指向它的 hazard_pointer// 时才会真正被销毁obj-retire();}};// A 类型的析构函数classA{// 析构函数不依赖任何具有独立生命周期的外部资源// 即// - 不访问全局对象// - 不访问已销毁的系统资源// - 不依赖作用域外状态~A();};// 使用容器的作用域{ContainerAcontainer;container.insert(a);container.erase(a);}// 注意// 包含 A 的 Obj 可能此时尚未被 delete//// 但这是“安全的”因为// - A 的析构函数不依赖任何外部资源// - 即使析构发生在容器作用域之后也不会触发未定义行为//// 因此// Hazard Pointer 的“异步回收”在该场景下是 OK 的// ---------------- 第二种情况析构函数依赖外部资源 ----------------templateclassTclassContainer{// 同样的 Hazard Pointer 管理对象classObj:hazard_pointer_obj_baseObj{T data;/* etc */};voidinsert(T data){Obj*objnewObj(data);/* etc */}voiderase(Args args){Obj*objfind(args);/* Remove obj from container */// 从容器中逻辑删除对象// retire() 仍然是“异步”的// 对象可能在将来的任意时间才被真正 deleteobj-retire();}};// B 类型的析构函数classB{// 析构函数依赖具有独立生命周期的外部资源//// 例如// - 全局状态// - 线程池// - IO 资源// - GPU / NUMA / 外部系统资源~B(){use_resource_XYZ();}};// 构造外部资源make_resource_XYZ();{ContainerBcontainer;container.insert(b);container.erase(b);}// 关键问题// 包含 B 的 Obj 可能此时仍然尚未被 delete//// Hazard Pointer 的异步回收意味着// - B::~B() 的调用时间不确定// - 可能发生在 container 作用域结束之后// - 甚至发生在 destroy_resource_XYZ() 之后//// 如果析构在此之后发生destroy_resource_XYZ();// 这将导致// - use_resource_XYZ() 访问已经销毁的资源// - 产生未定义行为UB//// 结论// 在这种情况下仅有“异步回收”的 Hazard Pointer 是不够的// 需要“同步回收Synchronous Reclamation”机制一、Hazard Pointer 是在解决什么问题**Hazard Pointer风险指针**是一种无锁数据结构的内存回收机制核心目标是在并发读线程仍可能访问对象的情况下安全地延迟删除对象。形式化描述可以理解为对象o oo只有在∀ t ∈ T h r e a d s , o ∉ H a z a r d ( t ) \forall t \in Threads, o \notin Hazard(t)∀t∈Threads,o∈/Hazard(t)时才允许被真正析构delete。二、Synchronous vs Asynchronous Reclamation同步 vs 异步回收1⃣ Asynchronous Reclamation异步回收retire()只是标记对象“可以回收”真正的delete发生在未来某个不确定时间由后台扫描 / 其他线程触发关键特性erase 返回 ≠ 对象已析构2⃣ Synchronous Reclamation同步回收语义要求当一个作用域结束 / 操作完成时对象已经被析构形式化语义可以理解为erase ( o ) ⇒ ∃ t 0 , ∀ t t 0 , o 不再存在 \text{erase}(o) \Rightarrow \exists t_0, \forall t t_0, o \text{ 不再存在}erase(o)⇒∃t0,∀tt0,o不再存在三、C26 的现状只支持异步回收C26 Hazard Pointer 标准接口只保证 Asynchronous Reclamation这意味着obj-retire();只保证当前线程以后不再访问其他线程 hazard pointer 清空后某个时刻delete不保证 erase 返回时对象已经析构四、第一个例子class A—— 为什么是 OK场景回顾A::~A()不依赖外部资源即使析构发生得晚一点也没有副作用{ContainerAcontainer;container.insert(a);container.erase(a);}// Obj containing a may be not deleted yet.结论对象A延迟析构程序语义仍然正确异步回收是“可接受的”五、第二个例子class B—— 为什么会出错关键区别~B(){use_resource_XYZ();}B的析构依赖一个独立生命周期的外部资源资源在 container 作用域之后被销毁destroy_resource_XYZ();问题根源逻辑顺序实际上变成了container.erase(b)→retire()未 deleteContainerB离开作用域destroy_resource_XYZ()某个时刻才 delete Obj~B()访问已销毁的资源错误的本质析构时序不受控可以用一个时序不等式表达d e s t r o y r e s o u r c e X Y Z ( ) ∼ B ( ) destroy_resource_XYZ() \sim B()destroyresourceXYZ()∼B()这是未定义行为UB。六、为什么“需要 Synchronous Reclamation”你的图里写得非常精准Need Synchronous Reclamation原因是析构函数有副作用副作用依赖外部生命周期必须保证∼ B ( ) d e s t r o y r e s o u r c e X Y Z ( ) \sim B() destroy_resource_XYZ()∼B()destroyresourceXYZ()七、Hazard Pointer 的解决方案Cohort队列/批次1⃣hazard_pointer_cohort的语义可以把 cohort 理解为一个“我负责清空”的退休对象集合特点cohort 析构时必须保证其内所有 retire 的对象都已被真正 delete是同步屏障2⃣ API 语义obj-retire_to_cohort(cohort_);表示把对象的最终删除责任绑定到cohort_的生命周期八、最终修正后的正确模式逐步解释Container 增加 cohorthazard_pointer_cohort cohort_;语义Container的析构 同步回收点erase 中的关键变化obj-retire_to_cohort(cohort_);ex_.submit([]{asynchronous_reclamation();});含义分解逻辑删除从容器中移除绑定回收责任加入 cohort触发异步扫描加速 hazard 清理生命周期保证{ContainerBcontainer;container.insert(b);container.erase(b);}// Container 析构// cohort 析构// 所有 ObjB 已 deletedestroy_resource_XYZ();满足严格时序∼ B ( ) d e s t r o y r e s o u r c e X Y Z ( ) \sim B() destroy_resource_XYZ()∼B()destroyresourceXYZ()完全正确九、总结对比核心要点维度异步回收同步回收C26 标准erase 返回后对象是否析构不保证保证析构依赖外部资源危险安全适用场景POD / 纯内存对象RAII / 有副作用析构十、一句话总结论文式Hazard Pointer 的异步回收足以保证内存安全但无法保证析构时序当析构函数依赖独立生命周期资源时必须引入基于 cohort 的同步回收语义。如果你愿意我可以下一步帮你做Hazard Pointer vs Epoch / RCU 的同步能力对比为什么标准先只给异步委员会角度用 Rust / C RAII 的视角重新解释 cohort// hazard_pointer_cohortHazard Pointer 的“同步回收分组”对象//// 语义核心// - cohort 表示一个“回收边界reclamation boundary”// - 所有被 retire_to_cohort() 绑定到该 cohort 的对象// 必须在 cohort 析构之前被真正 delete//// 作用// - 将 Hazard Pointer 从“最终会回收asynchronous”// 提升为“在此之前必须回收synchronous”classhazard_pointer_cohort{// 构造一个 cohort//// 构造完成后// - 可以向该 cohort 绑定 retired 对象// - cohort 本身通常作为一个作用域对象存在hazard_pointer_cohort()noexcept;// 禁止拷贝构造//// 原因// - cohort 表示一个严格的生命周期边界// - 拷贝会导致“一个回收边界对应多个实体”// - 会破坏以下不变量// $$ \text{~cohort} \Rightarrow \text{所有对象已回收} $$hazard_pointer_cohort(consthazard_pointer_cohort)delete;// 禁止移动构造//// 原因同样是为了保证// - cohort 的身份唯一// - cohort 的析构点明确且不可转移hazard_pointer_cohort(hazard_pointer_cohort)delete;// 禁止拷贝赋值hazard_pointer_cohortoperator(consthazard_pointer_cohort)delete;// 禁止移动赋值hazard_pointer_cohortoperator(hazard_pointer_cohort)delete;// cohort 的析构函数同步回收的关键//// 析构语义// 1. 触发一次或多次 hazard pointer 扫描// 2. 等待所有线程释放对 cohort 中对象的保护// 3. 确保 cohort 中所有对象已被真正 delete//// 形式化保证// $$ \forall obj \in cohort,\; \text{delete}(obj) \text{ 已发生} $$//// 也就是说// - 当 ~hazard_pointer_cohort() 返回时// - 不再存在任何“延迟析构”的对象~hazard_pointer_cohort();};// hazard_pointer_obj_base可被 Hazard Pointer 管理的对象基类//// T实际对象类型// D自定义删除器默认使用 default_deleteT//// 提供 retire_to_cohort()用于“同步回收”场景templateclassT,classDdefault_deleteTclasshazard_pointer_obj_base{// 将对象退休retire并显式绑定到某个 cohort//// 语义// - 对象从逻辑结构中移除// - 加入 hazard pointer 的 retired 列表// - 并归属于给定的 cohort//// 与 retire()异步最大的区别// - retire()析构时间不确定// - retire_to_cohort()析构必须发生在 cohort 析构之前//// 数学化表达// $$ \text{delete}(obj) \le \text{~cohort} $$//// noexcept 的原因// - 回收路径通常在并发代码中// - 不允许异常破坏内存回收不变量voidretire_to_cohort(hazard_pointer_cohort,D dD())noexcept;};// 显式触发一次“异步回收”流程//// 作用// - 扫描所有 hazard pointer// - 回收当前已满足条件的 retired 对象//// 使用场景// - 后台线程周期性调用// - executor / 线程池任务// - cohort 析构前的辅助回收//// 注意// - 该函数本身仍然是“异步回收”// - 只有结合 hazard_pointer_cohort 才能形成同步回收语义voidasynchronous_reclamation()noexcept;// 相关实现与参考资料//// - Facebook Folly 实现// folly/synchronization/Hazptr.h//// - CPPCON 2021 演讲// Hazard Pointer Synchronous Reclamation//// 这些内容构成了该 Possible API 的实践基础// Possible API可能的标准 API 形态//// - synchronous// 使用 hazard_pointer_cohort retire_to_cohort()// 提供“作用域内完成回收”的强保证//// - asynchronous// 传统 Hazard Pointer 模型// 提供“最终会回收”的弱保证//// 两者并存使 Hazard Pointer 既能高性能// 又能安全处理析构依赖外部资源的复杂类型Hazard Pointer Synchronous Reclamation风险指针的同步回收背景动机传统的Hazard PointerHP机制只保证对象不会在仍被线程访问时被释放但并不保证对象何时被释放。因此标准 Hazard Pointer 属于Asynchronous Reclamation异步回收形式化地说retire ( o b j ) ⇒ ∃ t ≥ t 0 , delete ( o b j ) \text{retire}(obj) \Rightarrow \exists t \ge t_0, \text{delete}(obj)retire(obj)⇒∃t≥t0,delete(obj)即对象最终会被删除但具体时间不确定。同步回收要解决的问题在某些场景中析构函数依赖外部资源的生命周期例如线程池、全局状态、硬件资源等这要求delete ( o b j ) ≤ 资源销毁时刻 \text{delete}(obj) \le \text{资源销毁时刻}delete(obj)≤资源销毁时刻也就是说在离开某个作用域之前必须确保对象已经被真正析构这正是Hazard Pointer Synchronous Reclamation要解决的问题。Cohorts回收分组的核心思想Cohort回收组是同步回收的关键抽象把一批 retired 对象绑定到一个作用域当该作用域结束时强制完成这些对象的回收。可以理解为“这些对象必须在我走之前全部安全 delete。”hazard_pointer_cohort—— 同步回收的作用域对象classhazard_pointer_cohort{hazard_pointer_cohort()noexcept;hazard_pointer_cohort(consthazard_pointer_cohort)delete;hazard_pointer_cohort(hazard_pointer_cohort)delete;hazard_pointer_cohortoperator(consthazard_pointer_cohort)delete;hazard_pointer_cohortoperator(hazard_pointer_cohort)delete;~hazard_pointer_cohort();};语义说明构造一个回收分组cohort该 cohort 表示所有“归属于该 cohort 的对象”必须在 cohort 析构前被安全回收为什么禁止拷贝 / 移动hazard_pointer_cohort(consthazard_pointer_cohort)delete;hazard_pointer_cohort(hazard_pointer_cohort)delete;原因是cohort 表示一个严格的生命周期边界如果允许拷贝 / 移动会破坏以下不变量cohort 析构 ⇒ 该 cohort 中所有对象已 delete \text{cohort 析构} \Rightarrow \text{该 cohort 中所有对象已 delete}cohort析构⇒该cohort中所有对象已delete析构函数的关键语义~hazard_pointer_cohort();析构时保证触发回收流程等待所有 hazard pointer 解除保护确保 cohort 中的所有对象已被真正析构形式化保证∀ o b j ∈ cohort , delete ( o b j ) 已发生 \forall obj \in \text{cohort},\quad \text{delete}(obj) \text{ 已发生}∀obj∈cohort,delete(obj)已发生hazard_pointer_obj_base::retire_to_cohorttemplateclassT,classDdefault_deleteTclasshazard_pointer_obj_base{voidretire_to_cohort(hazard_pointer_cohort,D dD())noexcept;};语义解释将对象标记为 retired并绑定到指定的 cohort与普通retire()的区别在于方法回收时机retire()不确定异步retire_to_cohort()cohort 析构前同步保证语义对比数学化异步回收retire ( o b j ) ⇒ delete ( o b j ) eventually \text{retire}(obj) \Rightarrow \text{delete}(obj) \text{ eventually}retire(obj)⇒delete(obj)eventually同步回收cohortretire_to_cohort ( o b j , c ) ⇒ delete ( o b j ) ≤ c \text{retire\_to\_cohort}(obj, c) \Rightarrow \text{delete}(obj) \le \text{~}cretire_to_cohort(obj,c)⇒delete(obj)≤casynchronous_reclamation()voidasynchronous_reclamation()noexcept;作用说明显式触发一次hazard pointer 扫描retired 对象回收常用于后台线程executor / 线程池cohort 析构前的辅助清理同步 vs 异步 API 的设计意图Asynchronous Reclamation现行 C26简单高吞吐析构时间不可预测不适合析构依赖外部资源的类型Synchronous ReclamationCohorts稍高开销有确定的回收点析构安全适合复杂系统资源管理与 Folly 的关系Facebook Folly 已有成熟实现folly/synchronization/Hazptr.hCPPCON 2021 专题演讲Hazard Pointer Synchronous Reclamation该 API 设计基本是对 Folly 经验的标准化抽象总结一句话可以直接用在结论页Hazard Pointer Cohorts 将“最终会回收”升级为“在此之前必须回收”使 Hazard Pointer 能安全用于析构依赖外部资源的并发系统。如果你愿意下一步我可以把你之前ContainerB的错误例子完整改写成 cohort 版本用一张时间轴图解释cohort 析构时发生了什么对比Hazard Pointer Cohort vs Epoch/RCU你只要点一个。// // Hazard Pointer 同步回收 vs C26 仅异步回收// //// 核心问题// Hazard Pointer 的 retire() 只保证“最终会删除”// 并不保证“在某个确定时间点之前已经删除”。//// 当析构函数依赖外部资源时这个不确定性会变成严重问题。//// -----------------------// C26仅支持【异步回收】Asynchronous Reclamation// -----------------------templateclassTclassContainer{// Obj 继承 hazard_pointer_obj_base// 说明该对象的生命周期由 Hazard Pointer 框架管理classObj:hazard_pointer_obj_baseObj{T data;// 真正存储的数据/* etc */// 其他成员指针、索引等};voidinsert(T data){// 分配新对象// 对象一旦被发布给并发读线程就可能被 hazard pointer 保护Obj*objnewObj(data);/* etc */// 将 obj 插入到无锁/并发容器中}voiderase(Args args){Obj*objfind(args);// 查找要删除的对象/* Remove obj from container */// 逻辑删除从容器结构中移除// 但此时可能仍有其他线程通过 hazard pointer 访问它obj-retire();// retire() 的含义// 1. 告诉 Hazard Pointer 系统这个对象“已经不再被容器拥有”// 2. 但并不立刻 delete// 3. 只有当【没有任何 hazard pointer 指向它】时才允许 delete//// 关键点// delete 的时间是【不确定的】可能立刻也可能很久以后}};classA{// A 的析构函数// 不依赖任何外部资源// 不访问其他具有独立生命周期的对象//// 因此// 即使析构被延迟执行也不会造成逻辑错误或 UB~A();};{ContainerAcontainer;container.insert(a);container.erase(a);}// 重要说明// 这里 container 已经离开作用域但//// Obj(A) 可能仍然没有被 delete// 因为// - 可能仍有并发线程持有 hazard pointer// - retire() 只是“异步回收”//// 但这是 OK 的// 因为 A 的析构函数不依赖任何外部资源//// 内存安全// 生命周期语义安全//// 结论OK// -----------------------// 同步回收Synchronous Reclamation// -----------------------//// 目标// 在“某个确定时间点之前”强制保证对象已经被 delete//templateclassTclassContainer{classObj:hazard_pointer_obj_baseObj{T data;/* etc */};// hazard_pointer_cohort回收分组回收边界//// 语义// 所有 retire_to_cohort(cohort_) 的对象// 在 cohort_ 析构之前必须已经被 deletehazard_pointer_cohort cohort_;voidinsert(T data){Obj*objnewObj(data);/* etc */}voiderase(Args args){Obj*objfind(args);/* Remove obj from container */obj-retire_to_cohort(cohort_);// retire_to_cohort 的含义// 1. 对象进入 Hazard Pointer 的退休列表// 2. 同时“绑定”到 cohort_// 3. cohort_ 析构时会阻塞/等待// 直到该对象真正被 deleteex_.submit([]{asynchronous_reclamation();});// 触发一次异步回收扫描// - 加快回收进度// - 减少 cohort_ 析构时需要等待的时间//// 注意// 真正的“同步保证”不是来自这里// 而是来自 cohort_ 的析构语义}};classB{// B 的析构函数// 依赖外部资源 XYZ// 该资源拥有独立生命周期//// 如果析构发生在 XYZ 被销毁之后 → UB~B(){use_resource_XYZ();}};make_resource_XYZ();// 创建外部资源 XYZ{ContainerBcontainer;container.insert(b);container.erase(b);}// ← container 离开作用域// 触发成员 cohort_ 的析构// cohort_ 析构时保证// 所有 retire_to_cohort(cohort_) 的 Obj(B)// 已经// 没有 hazard pointer 引用// 已经被 delete//// 即// $$ \forall obj \in cohort,\quad delete(obj) destroy\_resource\_XYZ $$destroy_resource_XYZ();// 现在销毁资源是安全的// 析构顺序正确// 无 use-after-free// 无 Undefined Behavior//// 结论OK一、背景核心问题Hazard Pointer 的本质目标是在无锁并发结构中保证对象在“仍可能被其他线程访问”时不会被释放。但是传统 Hazard Pointer也是C26 目前仅有的形式只保证对象最终会被删除eventually deleted \text{对象最终会被删除eventually deleted}对象最终会被删除eventually deleted而不保证“在某个确定时间点之前一定被删除”。二、第一段C26 仅支持的【异步回收Asynchronous Reclamation】templateclassTclassContainer{classObj:hazard_pointer_obj_baseObj{T data;/* etc */};voidinsert(T data){Obj*objnewObj(data);/* etc */}voiderase(Args args){Obj*objfind(args);/* Remove obj from container */obj-retire();}};classA{// Deleter does not depend on resources// with independent lifetime.~A();};{ContainerAcontainer;container.insert(a);container.erase(a);}// Obj containing a may be not deleted yet.1⃣ 这里发生了什么Obj继承自hazard_pointer_obj_baseerase()中调用obj-retire()这意味着对象逻辑上已从容器移除但物理释放delete被推迟2⃣retire()的语义retire()表示该对象加入“待回收列表”等所有 hazard pointer 释放后再 delete \text{该对象加入“待回收列表”等所有 hazard pointer 释放后再 delete}该对象加入“待回收列表”等所有hazard pointer释放后再delete也就是说若其他线程仍持有 hazard pointer指向该对象删除会被延迟删除发生时间不可预测3⃣ 为什么对class A是 OK 的classA{~A();};A的析构函数不依赖任何外部资源不访问已释放内存已销毁的全局/线程资源即使析构被延迟语义仍然正确不会触发 UB因此结论是对象什么时候被删都无所谓 → OK三、第二段引入【同步回收Synchronous Reclamation】templateclassTclassContainer{classObj:hazard_pointer_obj_baseObj{T data;/* etc */};hazard_pointer_cohort cohort_;voidinsert(T data){Obj*objnewObj(data);/* etc */}voiderase(Args args){Obj*objfind(args);/* Remove obj from container */obj-retire_to_cohort(cohort_);ex_.submit([]{asynchronous_reclamation();});}};1⃣ 新增的关键元素hazard_pointer_cohortcohort_表示一个回收边界reclamation boundary所有 retire 到该 cohort 的对象必须在 cohort 析构前被删除形式化描述为∀ o b j ∈ c o h o r t , d e l e t e ( o b j ) ≤ cohort \forall obj \in cohort,\quad delete(obj) \le \text{~cohort}∀obj∈cohort,delete(obj)≤cohort2⃣retire_to_cohort(cohort_)的语义它不是普通的retire()而是把对象加入 hazard pointer 的退休列表同时绑定到 cohortcohort 析构时会等待所有 hazard pointer 释放强制完成对象回收3⃣asynchronous_reclamation()的作用ex_.submit([]{asynchronous_reclamation();});这一步加速回收过程提前触发扫描减少 cohort 析构时的阻塞时间但注意真正的同步保证来自 cohort 的析构而不是这个函数本身四、为什么第二段必须使用同步回收classB{~B(){use_resource_XYZ();}};1⃣ 问题本质B的析构函数依赖外部资源XYZ且该资源有独立生命周期make_resource_XYZ();{ContainerBcontainer;container.insert(b);container.erase(b);}destroy_resource_XYZ();如果使用异步回收erase()只调用retire()对象Obj(B)可能还没被 deletedestroy_resource_XYZ()已经发生析构~B()再访问XYZ结果Use-after-destroy ⇒ Undefined Behavior \text{Use-after-destroy} \Rightarrow \text{Undefined Behavior}Use-after-destroy⇒Undefined Behavior五、同步回收如何解决引入 cohort 后hazard_pointer_cohort cohort_;对象通过obj-retire_to_cohort(cohort_);绑定到 cohort。当container离开作用域时~Container()被调用成员cohort_析构cohort 保证所有绑定对象已 delete因此// Obj containing b must be already deleted.destroy_resource_XYZ();成立。六、对比总结核心结论模式回收保证是否可控适用析构异步回收C26最终回收不确定简单析构同步回收Cohort析构前必回收强保证依赖外部资源七、一句话结论Hazard Pointer 的异步回收保证“内存安全”而同步回收Cohort保证“生命周期安全”。// // Hazard Pointer 批量创建与销毁Batch Creation and Destruction// //// 核心问题// 在传统单个创建/销毁的方式下每个 hazard_pointer// 都需要独立分配和初始化销毁时也单独处理。// 并发性能上有一定开销如 ~6 ns。//// C26 标准一个一个创建{hazard_pointer hp[3];// 声明三条 hazard pointer但此时它们都是 empty 状态/* Three hazard pointers are made nonempty separately. */hp[0]make_hazard_pointer();// 分别初始化第 0 个 hazard pointerhp[1]make_hazard_pointer();// 分别初始化第 1 个 hazard pointerhp[2]make_hazard_pointer();// 分别初始化第 2 个 hazard pointerassert(!hp[0].empty());// 验证第 0 个已非空assert(!hp[1].empty());// 验证第 1 个已非空assert(!hp[2].empty());// 验证第 2 个已非空/* src is atomicT* */T*ptrhp[0].protect(src);// 通过 hp[0] 获取受保护的对象指针 ptr/* etc */}/* Three nonempty hazard pointers are destroyed separately. */// 退出作用域时hp[0], hp[1], hp[2] 各自析构分别清理// 性能大约 ~6 ns每条单独处理// // 批量创建与销毁Batch creation and destruction// {hazard_pointer hp[3];/* Three hazard pointers are made nonempty together. */make_hazard_pointer_batch(std::span{hp});// 一次性将 hp 数组的三个 hazard pointer 初始化为非空// 相比单个 make_hazard_pointer多条同时处理// 内部可能使用批量分配或优化路径 → 性能更优SCOPE_EXIT{destroy_hazard_pointer_batch(std::span{hp});// 批量销毁 hp 数组的三个 hazard pointer// 内部可能一次性释放资源提高效率};assert(!hp[0].empty());assert(!hp[1].empty());assert(!hp[2].empty());/* src is atomicT* */T*ptrhp[0].protect(src);// 与单个使用方式相同/* etc */}/* Three nonempty hazard pointers are emptied together, and then destroyed separately. */// 批量清空然后依次析构性能大约 ~2 ns明显低于单个创建/销毁// // Possible API// voidmake_hazard_pointer_batch(std::spanhazard_pointer);// 批量初始化一组 hazard pointer 为非空voiddestroy_hazard_pointer_batch(std::spanhazard_pointer)noexcept;// 批量销毁一组 hazard pointer// // 总结// //// 1. 单个创建/销毁//// - 每个 hazard pointer 独立初始化、销毁// - 性能较低 (~6 ns)// - 使用简单但高并发场景下开销大//// 2. 批量创建/销毁//// - 一次性初始化多条 hazard pointer// - 内部可优化批量分配// - 销毁时也可批量处理// - 性能高 (~2 ns)// - 高并发场景更适合//// 3. 使用场景// - 当你需要频繁创建大量 hazard pointer 时推荐批量 API// - 单个创建适合偶尔使用或数量较少的情况//Hazard Pointer 批量创建与销毁理解背景Hazard PointerHP是一种安全回收机制用于在多线程环境下安全访问共享对象。当对象可能被其他线程删除时HP 能保证访问期间对象不会被回收。问题在高并发场景下频繁创建和销毁 HP 会带来性能开销。单个创建/销毁时间大约为6 ns 6\text{ ns}6ns。批量创建/销毁可降低到大约2 ns 2\text{ ns}2ns提升 3 倍性能。单个创建/销毁方式C26 默认{hazard_pointer hp[3];// 声明三条 HP初始状态都是 emptyhp[0]make_hazard_pointer();// 单独初始化第 0 个 HPhp[1]make_hazard_pointer();// 单独初始化第 1 个 HPhp[2]make_hazard_pointer();// 单独初始化第 2 个 HPassert(!hp[0].empty());// 确认第 0 个非空assert(!hp[1].empty());// 确认第 1 个非空assert(!hp[2].empty());// 确认第 2 个非空T*ptrhp[0].protect(src);// 使用 HP 保护 atomic 对象 src}理解每个 HP 都单独分配、初始化和销毁。创建和销毁都是独立操作存在性能开销。适合 HP 数量少、访问不频繁的场景。高并发下开销明显因为T ∗ p t r h p [ i ] . p r o t e c t ( s r c ) T* ptr hp[i].protect(src)T∗ptrhp[i].protect(src)频繁调用时每次都要操作 HP 内部数据结构。批量创建/销毁方式Batch Creation and Destruction{hazard_pointer hp[3];make_hazard_pointer_batch(std::span{hp});// 一次性批量创建 HPSCOPE_EXIT{// 离开作用域时批量销毁destroy_hazard_pointer_batch(std::span{hp});};assert(!hp[0].empty());assert(!hp[1].empty());assert(!hp[2].empty());T*ptrhp[0].protect(src);// 使用 HP 保护 atomic 对象 src}理解批量创建一次性初始化整个 HP 数组内部可采用批量分配、优化链表等机制。减少每条 HP 的单独初始化开销。性能显著提高约2 ns 2\text{ ns}2ns。批量销毁将整个 HP 数组统一清空再依次销毁。避免单条销毁带来的重复操作。对高并发系统的效率更友好。使用场景当需要大量 HP 或频繁访问 shared object 时批量创建/销毁显著降低开销。单个 HP 适合偶尔访问的轻量级场景。核心 APIvoid make_hazard_pointer_batch(std::spanhazard_pointer); \text{void make\_hazard\_pointer\_batch(std::spanhazard\_pointer);}void make_hazard_pointer_batch(std::spanhazard_pointer);批量初始化 HP 数组为非空。void destroy_hazard_pointer_batch(std::spanhazard_pointer) noexcept; \text{void destroy\_hazard\_pointer\_batch(std::spanhazard\_pointer) noexcept;}void destroy_hazard_pointer_batch(std::spanhazard_pointer) noexcept;批量销毁 HP 数组。性能对比公式设单个 HP 创建/销毁时间为t s t_sts批量创建/销毁总时间为t b t_btbHP 数量为n nn则单个方式总时间T s n ⋅ t s T_s n \cdot t_sTsn⋅ts批量方式总时间T b ≈ t b ( 批量优化后的总耗时 ) T_b \approx t_b \quad (\text{批量优化后的总耗时})Tb≈tb(批量优化后的总耗时)性能提升比率Speedup T s T b ≈ 6 , ns × 3 2 , ns 9 \text{Speedup} \frac{T_s}{T_b} \approx \frac{6,\text{ns} \times 3}{2,\text{ns}} 9SpeedupTbTs≈2,ns6,ns×39在示例中 3 个 HP 的情况下批量创建/销毁约提升 3 倍以上总结Hazard Pointer解决了多线程下对象访问的安全问题。单个创建/销毁简单但在高并发场景下性能差。批量创建/销毁可以显著提升效率初始化更快销毁更快内部实现可做批量优化高并发系统或大量 HP 的场景强烈推荐使用make_hazard_pointer_batch和destroy_hazard_pointer_batch。Pointer Tagging指针标记概念来源P3125R0由 Hana Dusíková 提出 wg21.link/p3125r0。1. 指针标记的定义Pointer Tagging 是一种在指针本身的低位或高位空闲位存储额外信息的技术。假设有一个对齐的对象T TT其对齐要求为a l i g n o f ( T ) 4 alignof(T) 4alignof(T)4那么指针的低两位必定为0 00因为 4 字节对齐的对象地址总是 4 的倍数。利用这些低位可以存储一些额外信息比如状态标志或小整数而不改变指针指向的实际对象。示意64 位指针低两位可用作标记Pointer to aligned object T : 0000000000000000 ? ? ? ? ? ? ? ? ? ? \text{Pointer to aligned object } T:\quad 0000000000000000??????????Pointer to aligned objectT:0000000000000000??????????高位部分用于存储实际地址。低位未使用的对齐空位用于存储自定义标记。2. 动机MotivationP3125R0 中提出指针标记的动机C 标准目前不允许操作指针的位在标准 C 中直接操作指针的二进制位是未定义行为UB, Undefined Behavior。因此某些高级数据结构如锁自由数据结构、压缩型链表、状态标记对象无法安全或高效实现。实践中已广泛使用在操作系统、并发算法、GC垃圾回收和高性能数据结构中pointer tagging 是成熟技术。将其标准化可以降低 C 开发者使用此技术的门槛提高安全性。潜在收益可以在指针本身存储额外信息而无需增加额外字段从而减少内存占用。对于锁自由算法和并发数据结构可以更高效地存储状态、标记或版本号。3. 典型示例假设 32 位系统对齐为 4 的对象指针实际地址示例最低两位总是 00ptr 0 x 1000 \text{ptr} 0x1000ptr0x1000可以用低两位存储状态标志例如00: 正常01: 已删除10: 已访问11: 保留得到标记指针tagged_ptr ptr ∣ tag_bits \text{tagged\_ptr} \text{ptr} | \text{tag\_bits}tagged_ptrptr∣tag_bits4. 关键点总结原理利用对齐空闲位存储额外信息。限制当前标准 C 下直接操作指针位是 UB。必须确保访问对象时清除标记位。用途高性能数据结构lock-free、wait-free。低内存开销存储额外状态信息。目标将 pointer tagging 标准化安全使用提高可移植性。1. 基本类型定义templatetypenameT,size_t Alignmentalignof(T)classtagged_pointer;tagged_pointerT, Alignment表示带标记的指针类型。模板参数T是指针指向的类型Alignment是对象对齐要求默认alignof(T)。这个类型封装了原始指针以及存储在低位的 tag 信息。2. 可用标记位掩码templatetypenameT,size_t Alignmentalignof(T)constexprautotag_bit_mask()noexcept-uintptr_t;返回当前类型T对齐要求下可用于存储 tag 的位掩码。对齐保证了低n nn位总是0 00可以安全使用它们存储 tag。示例如果a l i g n o f ( T ) 4 alignof(T) 4alignof(T)4低两位可用则tag_bit_maskT,4() 0 b 11 \text{tag\_bit\_maskT,4()} 0b11tag_bit_maskT,4()0b113. 将普通指针打上 tagtemplatetypenameT,size_t Alignmentalignof(T)constexprautotag_pointer(T*original,uintptr_t tag)noexcept-tagged_pointerT,Alignment;功能将原始指针与 tag 位组合生成tagged_pointer。先决条件t a g ( t a g t a g _ b i t _ m a s k T , A l i g n m e n t ( ) ) tag (tag \ tag\_bit\_maskT, Alignment())tag(tagtag_bit_maskT,Alignment())意味着 tag 只能占用允许的空闲低位避免覆盖指针地址本身。返回值类型tagged_pointerT, Alignment即带标记的指针对象。4. 从tagged_pointer获取原始指针templatetypenameT,size_t Alignmentalignof(T)constexprautountag_pointer(tagged_pointerT,Alignmentptr)noexcept-T*;功能提取tagged_pointer中存储的原始指针忽略 tag 位。实现上会用掩码清除低位标记original_ptr p t r ∼ t a g _ b i t _ m a s k T , A l i g n m e n t ( ) \text{original\_ptr} ptr \ \sim tag\_bit\_maskT, Alignment()original_ptrptr∼tag_bit_maskT,Alignment()5. 获取tagged_pointer中的 tag 值templatetypenameT,size_t Alignmentalignof(T)constexprautotag_value(tagged_pointerT,Alignmentptr)noexcept-uintptr_t;功能从tagged_pointer中提取存储的 tag 信息。实现上用掩码提取低位tag p t r t a g _ b i t _ m a s k T , A l i g n m e n t ( ) \text{tag} ptr \ tag\_bit\_maskT, Alignment()tagptrtag_bit_maskT,Alignment()6. 使用示意假设有一个指针T* ptr对齐为 4autotaggedtag_pointer(ptr,0b01);// 在低两位存储标记 01T*originaluntag_pointer(tagged);// 取回原指针uintptr_t tagtag_value(tagged);// 取回标记值 01优点不增加额外内存直接在指针本身存储状态信息。对齐保证安全性低位空闲位不会破坏指针有效性。可用于 lock-free 数据结构、状态标记、版本号等场景。总结tagged_pointer封装指针 标记。tag_bit_mask()返回可用的低位掩码。tag_pointer()生成带标记的指针。untag_pointer()获取原始指针。tag_value()获取存储在指针中的标记。公式关键点tagged_ptr original_ptr ∣ t a g \text{tagged\_ptr} \text{original\_ptr} | tagtagged_ptroriginal_ptr∣tagoriginal_ptr t a g g e d _ p t r ∼ t a g _ b i t _ m a s k \text{original\_ptr} tagged\_ptr \ \sim tag\_bit\_maskoriginal_ptrtagged_ptr∼tag_bit_maskt a g t a g g e d _ p t r t a g _ b i t _ m a s k tag tagged\_ptr \ tag\_bit\_masktagtagged_ptrtag_bit_mask1. 指针打标记示例usingTaggedPointertagged_pointerT,2;booltry_tag_untagged_pointer(atomicTaggedPointersrc){TaggedPointer currentsrc.load();assert(tag_value(current)0);// 确认当前指针未打标记assert(1tag_bit_maskT,2()1);// 确认 1 是合法的标记位T*ptruntag_pointer(current);// 提取原始指针TaggedPointer newvaltag_pointer(ptr,1);// 为指针打标记 1returnsrc.compare_exchange_weak(current,newval);// CAS 操作更新原子指针}理解TaggedPointer current src.load();从原子变量src中读取当前值可能未打标记。tag_value(current) 0断言当前指针的 tag 值为 0即未打标记。1 tag_bit_maskT,2() 1断言 tag 位1是合法的低位可用空间安全存储在指针低两位。T* ptr untag_pointer(current);提取原始指针忽略低位标记。TaggedPointer newval tag_pointer(ptr, 1);将 tag 1 写入指针低位生成新的带标记指针。compare_exchange_weak(current, newval)使用 CASCompare-And-Swap安全更新原子指针避免数据竞争。公式表示newval original_ptr ; ∣ ; tag \text{newval} \text{original\_ptr} ;|; \text{tag}newvaloriginal_ptr;∣;tagoriginal_ptr untag_pointer(newval) \text{original\_ptr} \text{untag\_pointer(newval)}original_ptruntag_pointer(newval)tag tag_value(newval) \text{tag} \text{tag\_value(newval)}tagtag_value(newval)2. Hazard Pointer 扩展到 Tagged PointerC26 标准原生的 Hazard Pointer APItemplatetypenameTT*protect(constatomicT*src)noexcept;templatetypenameTbooltry_protect(T*ptr,constatomicT*src)noexcept;保护原子指针防止在多线程环境中被释放。扩展到 tagged_pointertemplatetypenameT,size_t Alignmentalignof(T)tagged_pointerT,Alignmentprotect(constatomictagged_pointerT,Alignmentsrc)noexcept;templatetypenameT,size_t Alignmentalignof(T)booltry_protect(tagged_pointerT,Alignmentptr,constatomictagged_pointerT,Alignmentsrc)noexcept;功能与普通 Hazard Pointer 类似但操作的是带标记指针。可以安全保护指针及其 tag保证多线程访问时不会出现悬空或未同步状态。3. 使用示例atomictagged_pointerTsrc_;hazard_pointer hpmake_hazard_pointer();tagged_pointerTtaggedhp.protect(src_);/* 此时可以安全使用 ptr其中 ptr untag_pointer(tagged) */理解atomictagged_pointerT src_;原子变量存储带标记的指针。hazard_pointer hp make_hazard_pointer();创建一个 hazard pointer用于保护指针。tagged_pointerT tagged hp.protect(src_);将原子变量中的指针和 tag 一起保护起来防止在访问期间被删除或修改。ptr untag_pointer(tagged)可以安全地访问原始指针忽略低位 tag避免悬空访问。总结Pointer Tagging在指针低位存储额外信息tag节省内存。Hazard Pointer 扩展可保护带标记指针保证多线程安全访问。CAS 与 protect 操作确保了无锁并发安全。公式关键点tagged_ptr original_ptr ∣ tag \text{tagged\_ptr} \text{original\_ptr} | \text{tag}tagged_ptroriginal_ptr∣tagoriginal_ptr untag_pointer(tagged_ptr) \text{original\_ptr} \text{untag\_pointer(tagged\_ptr)}original_ptruntag_pointer(tagged_ptr)tag tag_value(tagged_ptr) \text{tag} \text{tag\_value(tagged\_ptr)}tagtag_value(tagged_ptr)1. 引入背景目标将并行性 (parallelism) 引入std::ranges算法使算法在多核/多线程环境下高效执行。来源基于 ISO C 的并行/并发编程语言扩展提案以及 Gonzalo 的 ISC C BoF 讨论。2. 历史演进C 2017Parallel Algorithms支持许多并行算法。Concurrency并发库增强改进线程管理、同步机制。Memory Model改进内存模型确保多线程程序的正确性贡献者 MichaelW、Maged、Paul M。Forward Progress确保程序在多线程环境下不会无限阻塞。C 2020Concepts提供模板约束使泛型编程更安全。Ranges范围库简化算法与容器的组合。Modules模块化机制提高编译速度与封装性。Concurrency继续扩展并发能力贡献者 Bryce, Gonzalo。Coroutines协程支持异步任务和延迟计算。atomic_ref, barriers, …引入新的原子操作和同步屏障。C 2023Ranges进一步丰富范围库功能。Multi-dimensional Spans支持多维数据视图 (span)。operator[i, j, k]提供多维索引访问的操作符方便矩阵/张量操作。C 2026规划中Executors / Sender / Receiver [P2300]异步任务调度和执行模型。Reflection提供运行时和编译时反射能力。Algorithms范围算法扩展引入异步支持贡献者 Ruslan, Alexey, Bryce。Linear Algebra, submdspan, padded layouts线性代数库扩展多维子视图、带填充布局贡献者 Bryce, Christian。Concurrency RCU, HP高级并发支持包括 RCURead-Copy-Update和 Hazard PointerHP贡献者 MichaelW, Gonzalo, Maged, Paul M。SIMD单指令多数据向量化操作贡献者 Matthias, Daniel, Ruslan。3. 理解总结发展趋势从 C17 开始支持基本并行算法逐步引入范围库、协程和多维数据结构。到 C2026将实现异步范围算法、执行器、反射、高级并发与 SIMD。目标提升算法在并行和多核环境下的性能。简化多线程和异步编程模型。支持高效线性代数计算和多维数据操作。关键概念Ranges使算法可以直接作用于“范围”简化迭代器操作。Executors / Sender / Receiver提供统一的异步任务调度机制。Hazard Pointer (HP)安全管理并发访问的动态内存对象。RCU (Read-Copy-Update)允许多线程读写共享数据的高效策略。SIMD通过单指令操作多数据提高向量计算性能。公式化理解假设一个范围算法alg在序列seq上运行result alg ( seq ) \text{result} \text{alg}(\text{seq})resultalg(seq)引入并行执行策略后result alg ( seq , execution_policy ) \text{result} \text{alg}(\text{seq}, \text{execution\_policy})resultalg(seq,execution_policy)异步算法可表示为futureresult alg.async ( seq ) \text{futureresult} \text{alg.async}(\text{seq})futureresultalg.async(seq)多维 span 支持多维索引访问value s p a n [ i , j , k ] \text{value} span[i,j,k]valuespan[i,j,k]1. 背景与发展历史对比C03经典 STL 算法templateclassRandomAccessIterator,classComparevoidsort(RandomAccessIterator first,RandomAccessIterator last,Compare comp);基于迭代器的排序只支持顺序执行。算法接口简单但无法直接控制并行执行。C17引入执行策略templateclassExecutionPolicy,classRandomAccessIterator,classComparevoidsort(ExecutionPolicyexec,RandomAccessIterator first,RandomAccessIterator last,Compare comp);支持执行策略ExecutionPolicy可以选择并行std::execution::par或顺序std::execution::seq。仍然基于迭代器没有直接与范围ranges结合。C20范围库支持templaterandom_access_range R,classCompranges::less,classProjidentityrequiressortableiterator_tR,Comp,Projconstexprborrowed_iterator_tRranges::sort(Rr,Comp comp{},Proj proj{});引入ranges用户可以直接操作范围对象而非迭代器。算法接口更加表达式化支持惰性组合lazy composition。默认比较函数为ranges::less并支持投影projection函数Proj。C26未来提案 P3179templateclassExecutionPolicy,random_access_range R,classCompranges::less,classProjidentityrequiressortableiterator_tR,Comp,Projconstexprborrowed_iterator_tRranges::sort(ExecutionPolicyexec,Rr,Comp comp{},Proj proj{});在范围算法中引入执行策略统一并行与范围 API。用户可以直接在范围上使用并行算法ranges::sort(exec_policy, my_range) \text{ranges::sort(exec\_policy, my\_range)}ranges::sort(exec_policy, my_range)2. 为什么将并行性与范围结合Ranges 提供生产力Ranges API 易读、可组合、表达能力强。并行算法普遍存在用户常常在范围上使用非范围并行算法但接口不统一、易出错。统一优势将执行策略直接整合到范围算法中可简化代码、降低错误率。3. 范围 并行的优势表达能力强Ranges 支持惰性计算lazy evaluation可组合多个操作。性能优化机会并行范围算法可自动利用多核 CPU 或 GPU提高性能。自然语义避免将迭代器与执行策略分开管理API 更直观。易于迁移返回类型与序列范围算法一致旧代码迁移简单。4. 并行范围算法设计要点执行策略参数所有范围算法可接受ExecutionPolicy如std::execution::seq , std::execution::par , std::execution::par_unseq \text{std::execution::seq}, \text{std::execution::par}, \text{std::execution::par\_unseq}std::execution::seq,std::execution::par,std::execution::par_unseq随机访问范围并行化算法要求输入范围是随机访问的R ∈ random_access_range R \in \text{random\_access\_range}R∈random_access_range保证可以高效地分块和分配任务。边界范围 (Bounded Range)至少一个输入和输出范围必须是有界的bounded保证安全和性能。一致的返回类型保持与序列范围算法一致便于单步替换borrowed_iterator_tR \text{borrowed\_iterator\_tR}borrowed_iterator_tR多操作单次调用融合支持将多个操作组合成单次并行调用减少调度开销。保持表达力保留 ranges API 的可组合性和易读性。5. 总结理解C17并行算法 迭代器接口。C20范围算法增强表达力。C26并行范围算法将执行策略直接整合到范围算法中实现统一、高效、易读的并行计算 API。用户可以写出ranges::sort(std::execution::par, my_vector) \text{ranges::sort(std::execution::par, my\_vector)}ranges::sort(std::execution::par, my_vector)同时保持与非并行范围算法的兼容性并简化多操作组合与优化。1. 关键设计决策Key Design Decisions返回类型与序列范围算法一致并行ranges::for_each返回类型为ranges::borrowed_iterator_tR \text{ranges::borrowed\_iterator\_tR}ranges::borrowed_iterator_tR这样可以保证与顺序版本兼容便于旧代码迁移和替换。要求输入范围为随机访问范围 (random_access_range)目前为了高效并行化算法要求R ∈ random_access_range R \in \text{random\_access\_range}R∈random_access_range。随机访问保证可以快速分块、分配任务降低线程间同步开销。输出范围为输入参数算法不仅遍历输入范围还可以直接在输出范围上操作in-place 或覆盖。要求边界范围 (bounded ranges)至少一个输入和输出范围必须是有界的sized/bounded保证安全性和高效性sized_sentinel_forsentinel_tR, iterator_tR \text{sized\_sentinel\_forsentinel\_tR, iterator\_tR}sized_sentinel_forsentinel_tR, iterator_tR便于计算块大小和任务划分。保持 C17 并行算法可调用要求保持 callable 函数对象functor满足indirectly_unary_invocable要求indirectly_unary_invocableprojectediterator_tR, Proj, Fun \text{indirectly\_unary\_invocableprojectediterator\_tR, Proj, Fun}indirectly_unary_invocableprojectediterator_tR, Proj, Fun保证算法在并行执行时函数对象仍然可调用且安全。2. 并行ranges::for_each模板接口解释templateclassExecutionPolicy,random_access_range R,classProjidentity,indirectly_unary_invocableprojectediterator_tR,ProjFunrequiressized_sentinel_forranges::sentinel_tR,ranges::iterator_tRranges::borrowed_iterator_tRranges::for_each(ExecutionPolicypolicy,Rr,Fun f,Proj proj{});逐行理解模板参数ExecutionPolicy执行策略可选顺序或并行例如std::execution::seq , std::execution::par , std::execution::par_unseq \text{std::execution::seq}, \text{std::execution::par}, \text{std::execution::par\_unseq}std::execution::seq,std::execution::par,std::execution::par_unseqrandom_access_range R输入范围必须是随机访问范围。Proj identity投影函数可将元素映射到某个属性。Fun函数对象必须满足indirectly_unary_invocableprojectediterator_tR, Proj, Fun \text{indirectly\_unary\_invocableprojectediterator\_tR, Proj, Fun}indirectly_unary_invocableprojectediterator_tR, Proj, Fun约束条件 (requires)输入范围必须是有界的sized/boundedsized_sentinel_forranges::sentinel_tR, ranges::iterator_tR \text{sized\_sentinel\_forranges::sentinel\_tR, ranges::iterator\_tR}sized_sentinel_forranges::sentinel_tR, ranges::iterator_tR返回类型与序列范围算法一致返回借用迭代器ranges::borrowed_iterator_tR \text{ranges::borrowed\_iterator\_tR}ranges::borrowed_iterator_tR函数参数policy执行策略。r输入范围。f函数对象对每个元素执行操作。proj投影函数默认identity。3. 总结理解C26 并行范围算法将执行策略与范围 API统一。输入范围必须是随机访问 有界以保证高效并行化。函数对象必须可调用indirectly_unary_invocable保持并行安全。返回类型保持与序列范围算法一致便于迁移和组合操作。投影函数允许用户在遍历时提取元素属性提高灵活性。1. 与 C17 并行算法的关键差异Key Differences输入范围要求随机访问random access rangesC17 并行算法可以接受更广泛的迭代器类型但 C26 并行范围算法要求R ∈ random_access_range R \in \text{random\_access\_range}R∈random_access_range这样保证算法能够高效地进行分块并行计算减少线程间同步开销。输出可以是一个范围而不仅仅是迭代器在 C17 中大多数并行算法只返回一个迭代器表示处理完毕的位置。C26 并行范围算法允许直接传入输出范围返回一个范围或借用迭代器return type ranges::borrowed_iterator_tR 或输出范围 \text{return type} \text{ranges::borrowed\_iterator\_tR} \text{ 或输出范围}return typeranges::borrowed_iterator_tR或输出范围这简化了结果的处理减少了手动迭代和赋值操作。2. 优势Benefits自然高效的范围并行化直接在范围上操作结合执行策略ExecutionPolicy即可并行化for_each(exec_policy, my_range, func) \text{for\_each(exec\_policy, my\_range, func)}for_each(exec_policy, my_range, func)避免了 C17 中手动从迭代器获取子区间并分发任务的繁琐操作。无缝集成 Ranges 库与现有并行算法保持范围的惰性组合能力同时继承 C17 并行算法的执行策略。用户可以轻松地将串行范围算法迁移到并行版本无需重写大量代码。代码更具表达力使用范围直接表示输入/输出集合减少模板嵌套与迭代器类型声明。可读性和可维护性更高auto result ranges::sort(exec_policy, my_range); \text{auto result ranges::sort(exec\_policy, my\_range);}auto result ranges::sort(exec_policy, my_range);潜在更高性能随机访问 边界范围 执行策略组合允许编译器和运行时进行高效分块和负载均衡。更安全的 API要求有界范围bounded ranges保证在并行执行中不会越界或访问无效内存。输出范围避免了返回裸迭代器可能带来的悬空问题。简化串行到并行迁移串行范围算法与并行范围算法接口保持一致只需添加执行策略ranges::for_each(my_range, func) → ranges::for_each(exec_policy, my_range, func) \text{ranges::for\_each(my\_range, func)} \to \text{ranges::for\_each(exec\_policy, my\_range, func)}ranges::for_each(my_range, func)→ranges::for_each(exec_policy, my_range, func)降低迁移复杂度同时获得并行性能。总结C26 并行范围算法通过随机访问范围 输出范围 执行策略的组合提供了更自然、更安全、更高效的并行计算方式弥合了范围表达能力与并行执行能力的差距。1. 背景与动机Overview MotivationC 并行算法的发展演化C11/14 引入了基础的并行算法支持std::execution、std::for_each等。C17 引入了并行执行策略std::execution::par、seq但无法指定执行硬件。C26 借助 P2300 引入了可调度的执行器scheduler实现 “在哪里执行” 的灵活控制。为什么需要 P2300现有执行策略只回答 “如何执行”how无法回答 “在哪里执行”where。P2300 提供scheduler抽象表示硬件执行上下文例如 CPU 核心、GPU、异构设备等。将并行算法与 scheduler 集成可以更有效利用硬件能力提高性能和可扩展性。2. P2500 的设计概览Design Overview策略感知调度器policy-aware scheduler结合了执行策略execution policy与调度器schedulerpolicy-aware scheduler ( scheduler, execution policy ) \text{policy-aware scheduler} (\text{scheduler, execution policy})policy-aware scheduler(scheduler, execution policy)允许用户通过execute_on创建策略感知调度器auto sched s t d : : e x e c u t e o n ( m y s c h e d u l e r , s t d : : e x e c u t i o n : : p a r ) ; \text{auto sched} std::execute_on(my_scheduler, std::execution::par);auto schedstd::executeon(myscheduler,std::execution::par);设计目标Design Goals扩展 C 并行算法以支持策略感知调度器。保持算法和策略的核心语义不变。支持传统算法与基于范围的算法range-based algorithms。最小化 API 改动保持向后兼容。允许根据硬件能力自定义算法实现实现性能优化。关键特性Key Features调度器与策略组合scheduler execution policy。可扩展 API允许为特定调度器自定义并行算法。保持阻塞行为与 C17 并行算法一致。3. 核心概念Key Conceptspolicy_aware_scheduler 概念templatetypenameSconceptpolicy_aware_schedulerschedulerSrequires(S s){typenameS::base_scheduler_type;typenameS::policy_type;{s.get_policy()}-execution_policy;};调度器必须提供base_scheduler_type和policy_type。get_policy()返回关联的执行策略。execute_on 函数inlineconstexpr__detail::__execute_on_fn execute_on;将调度器与执行策略绑定生成策略感知调度器policy_aware_sched std::execute_on(my_scheduler, std::execution::par) \text{policy\_aware\_sched} \text{std::execute\_on(my\_scheduler, std::execution::par)}policy_aware_schedstd::execute_on(my_scheduler, std::execution::par)并行算法自定义化并行算法定义为可自定义函数customizable function可以针对特定调度器实现优化版本例如 CUDA 优化namespacecuda{structscheduler{friendconstexprautotag_invoke(std::tag_tranges::for_each,scheduler,/*...*/){cuda_kernelblocks,threads(/*...*/);returnstd::ranges::for_each_result{/*...*/};}};}4. 使用示例Usage Examplestd::for_each(std::execute_on(my_gpu_scheduler,std::execution::par),begin(data),end(data),[](autoitem){item.process();});execute_on生成策略感知调度器for_each在指定硬件上并行执行。保留了 C17 并行算法的阻塞语义同时支持硬件定制化。5. API 对比现有 C17 APItemplateclassExecutionPolicy,classIt,classFunconstexprvoidfor_each(ExecutionPolicypolicy,It first,It last,Fun f);新策略 API基于 execution_policytemplateexecution_policy Policy,input_iterator I,sentinel_forIS,classProjidentity,indirectly_unary_invocableprojectedI,ProjFunconstexprranges::for_each_resultI,Funranges::for_each(Policypolicy,I first,S last,Fun f,Proj proj{});调度器 APIpolicy-aware schedulertemplatepolicy_aware_scheduler Scheduler,input_iterator I,sentinel_forIS,classProjidentity,indirectly_unary_invocableprojectedI,ProjFunconstexprranges::for_each_resultI,Funranges::for_each(Scheduler sched,I first,S last,Fun f,Proj proj{})/*customizable*/;支持针对不同硬件和执行上下文自定义算法实现。6. 总结SummaryP2300提供了灵活的硬件执行上下文抽象scheduler回答 “代码在哪里执行”。P2500将 scheduler 集成到 C 并行算法中实现 “策略感知并行算法”回答 “如何执行 在哪里执行”。未来方向可定制、可扩展的并行算法将进一步提升 C 并行编程的性能、表达力和安全性。