第36篇的两个榜单实现回答相同查询,却把成本放在不同位置。VersionedBoard维护AVL和路径复制树,SnapshotBoard复制完整字典并扫描或排序。对数复杂度并不能直接回答“小数据上哪个Python实现更快”,一次本机测量也不能回答“所有负载都该选哪个”。

实验先固定语义,再改变输入规模、分数分布、更新比例与整数位长。近似算法允许不同输出质量,应另外报告质量,不能让它与精确实现只比一个耗时数字。

同一语义的比较需要固定什么

榜单实验沿用36篇契约:ID稳定、排名按(-score,id)、范围按ID半开区间、历史查询指向明确版本。两种实现从相同初始分数开始,执行完全相同的更新与查询,保留全部历史版本。

输入规模n取16、64、256;分布包含重复分数多的tied与较分散的uniform;更新比例取0.1、0.8。每条操作流长度256,查询按rank、select、当前随机范围和、历史全域和轮换。历史全域查询在持久化树上可直接读取根摘要,不代表任意历史子区间都只访问根。更新率是生成概率,实际更新数另记。输入在计时外生成并保存散列,结果校验和相等是进入比较的条件,不是“答案大致一样”。

本实验初始化时插入n条记录,随后只更新已有ID,没有在计时区间插入或删除。因此当前记录数保持n,测到的是这个固定成员负载,不代表含大量删除、扩容或版本回收的系统。容量也取n,不比较不同容量上限带来的空间差异。

版本号包括初始化插入产生的版本。历史查询必须根据操作流已经完成的更新数生成合法版本,不能让一个实现查最新根、另一个实现查旧快照,却把不同答案的时间放进同一张表。

时间和内存分开运行

计时使用Python官方的perf_counter_ns,取开始与结束的差值。返回值单位为纳秒,不代表实际时钟分辨率就是一纳秒;原始结果同时记录get_clock_info提供的分辨率、解释器和平台。

每个配置先预热,再运行五轮,交替候选实现的先后顺序。每轮使用新实例,构造与初始插入不计入操作流时间;计时包含实际操作循环和结果汇总。保存所有样本并给出中位数,不把五个样本包装成经过统计设计的置信区间。

垃圾回收策略也属于实验条件。手写perf_counter循环不会像timeit默认行为那样自动关闭GC,本文保持GC开启并记录状态。若后台进程抢占CPU,elapsed时间会包含这些干扰;交替顺序只能减轻部分系统漂移,不能消除噪声。

内存另起一次tracemalloc测量,先开始跟踪,再构造实例、插入初始记录并执行同一操作流。峰值因此包含构建和保留历史版本的Python分配,与只计操作流的时间区间不同。输入流生成在跟踪外,两种实现采用同样边界。

tracemalloc峰值是被跟踪分配块的峰值,不是进程RSS、总物理内存或硬件缓存占用。跟踪本身会改变运行开销,所以不在它开启时收集用于比较的时间样本。没有被跟踪的原生分配也不能凭这个数字补算出来。

位长与近似质量不能混为一谈

普通整数和附加200位偏移的整数都使用Python精确算术,二者不会因为整数很大就自动产生舍入误差。位长轴检验的是大整数比较、加减与对象存储的实际代价;它不把位长增加叫作“降低正确性”。36篇树上O(log n)操作次数也没有承诺每个大整数操作耗时不变。

另一类精度要求来自37篇覆盖问题。exact_cover给出小实例精确最优成本,coverage_plan给出可行解及H_d近似保证。两者求解同一个覆盖目标,但输出承诺不同,应该报告贪心成本C、精确OPT、比值C/OPT及保证H_d。

当C/OPT大于1时,这不是贪心违反接口;只要满足已证明的界,近似契约仍成立。反过来,即使若干小实例的比值都等于1,也不能把近似算法改称精确算法。空全集的0/0不定义比值,不可覆盖输入报告失败状态,不能用“成本0”混进质量统计。

结果怎样支持一个有限判断

本次实际运行环境为Python 3.14.4、macOS 27.0 arm64,计时器报告分辨率约41.67纳秒。共16个配置:12个规模、分布和更新概率组合,另有4个n=64的201位整数配置。下表时间为每条256操作流的五轮中位数,单位微秒;内存为单独测量的峰值字节。

n / 分布 / 更新概率 实际更新数 Versioned 时间 Snapshot 时间 Versioned 峰值 Snapshot 峰值
16 / uniform / 0.1 20 203.250 222.791 32496 33240
64 / uniform / 0.1 18 255.500 722.333 84824 149000
64 / uniform / 0.8 210 1494.166 211.291 198744 578616
256 / uniform / 0.1 20 346.709 3021.500 324704 1583536
256 / uniform / 0.8 213 1937.792 663.834 473080 3365496

同为n=64,查询较多时VersionedBoard更快,更新较多时SnapshotBoard更快;两者的峰值内存排序没有随时间排序反转。这是负载轴上的胜负变化,没有测出一个精确的更新比例交叉点。每个更新比例只取了一个固定种子流,也不能把两行之间的时间线性插值当作真实阈值。

n=64、tied、更新概率0.8时,普通小整数的中位数分别为1454.375与239.000微秒;201位整数分别为1493.041与309.500微秒。两种输入都得到精确一致的答案。这个差异是当前观测,未做统计显著性分析,也没有把整数操作与分配开销单独隔离。

覆盖质量使用U={0,1},候选[{0},{0,1}],成本[1,2]。初始单位新增覆盖成本打平,按编号先选单元素集合,随后还要选择全集,贪心成本3;精确参照只选全集,OPT=2。比值3/2恰好等于H₂,明确否定“这些贪心选择必然最优”的判断。这里报告质量而未给覆盖程序做耗时排名。

结果的适用范围是这台机器、这个解释器、这些输入和当前代码。若不同配置发生快慢反转,可以说明只按一个规模或一个更新比例作选择会丢失信息;它仍不能证明一个跨机器固定阈值。若全部配置同一实现更快,也应报告尚未观察到交叉点,不能编造一个让图表更完整的阈值。

正确性依靠36篇接口与证明,并由每条流的参照结果核对;性能结论依靠这里的实际测量,两者证据不能互换。内存峰值也不是理论节点数:Python对象头、引用、整数和临时结果都可能改变常数。

怎样复跑与改变一个条件

在仓库根目录执行:

1
python3 examples/advanced-algorithms/benchmark_matrix.py

本次退出0。每个候选预热一次、五轮实际运行以及内存测量的完整结果列表都与SnapshotBoard参照相等,空覆盖与不可覆盖边界也通过。原始stdout在writing-plans/advanced-algorithms/evidence/benchmark-matrix-results.json,包含输入SHA256、结果SHA256、实际更新数、全部时间样本和跟踪峰值;相邻的benchmark-matrix-source-hashes.json记录本次源码散列。复跑耗时会变化,不要求重现同一纳秒数。

程序以固定种子生成输入,散列用于确认同一输入;它没有保存后台负载、CPU频率和系统热状态的完整轨迹,也没有测试其他解释器、机器或生产流量。

改变矩阵时,每次明确改变哪一轴。如果扩大n同时减少操作数、关闭历史保留或换一个结果更弱的查询接口,曲线变化就不再只由规模解释。需要测删除负载时,应新增双方都支持的同一操作流,并重新核对所有结果。

这不是用跑分代替模型:理论解释应先指出哪项操作随n、更新次数或位长增长,测量再检验在给定环境中哪些成本占主导。没有隔离的因素只能作为待验证解释,不能写成已经证明的原因。

练习

  1. 如果把SnapshotBoard的构造时间计入、VersionedBoard的构造时间排除,比较违反了哪条条件?为“从零加载到完成查询”的目标重新定义一致的计时区间。
  2. 一个覆盖贪心运行1毫秒、精确枚举运行20毫秒,但成本分别为12与8。列出必须报告的质量指标,并解释为何“快20倍”不能独立决定选型。

参考资料

  • Python time:perf_counter_ns:经过时间、整数单位及相关时钟信息接口。
  • Python timeit:setup、垃圾回收与重复样本解释;本文使用自己的循环并明确其测量边界。
  • Python tracemalloc:跟踪起点、当前/峰值分配及跟踪开销。数据结构正确性和覆盖近似证明沿用36、37篇,不由测量文档提供。