/remove()/now() 的列表操作性能)
RIOT xtimer 定时器基准测试全解析深入理解 set()/remove()/now() 的列表操作性能【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOTRIOT 操作系统将定时器抽象层 xtimer 的实现建立在next-first 单向链表之上因此其插入、删除操作的复杂度为 O(n)n 为当前活跃定时器数量。本文以 tests/bench/xtimer/README.md 为骨架结合 基准测试源码 与 xtimer 内核实现系统讲解这一基准测试的设计思路、每个测试场景的测量目标、底层实现原理以及如何编译运行并正确解读结果。读完本文你将掌握如何用一套可复现的基准程序量化定时器列表操作在目标硬件上的真实开销并理解这些数字背后的链表算法细节。基准测试概述它究竟在测量什么该基准测试位于 tests/bench/xtimer 目录用于对 xtimer 的三个核心操作执行基准测试set()向定时器列表插入一个定时器xtimer_setremove()从列表中移除一个定时器xtimer_removenow()读取当前系统时间xtimer_now。这套基准测试测量的是xtimer 的列表操作效率而非定时器的实际触发精度。从 xtimer.h 的模块文档可知xtimer 用单个底层定时器periph timer做多路复用多路复用通过next-first 单向链表实现因此插入和删除均为 O(n) 复杂度——这正是这套基准想要量化的成本。关键参数与默认值基准程序的可调参数定义在 main.c 中参数默认值含义NUMOF_TIMERS1000同时参与测试的定时器数量REPEAT1000每个基准循环重复的次数BASE100000000定时器基准偏移100 秒单位微秒SPREAD10000相邻定时器目标时间间隔10 毫秒由于多个基准需要维护一张已设置的定时器列表NUMOF_TIMERS越大占用的内存越多每个xtimer_t结构约 28 字节。因此测试会根据目标板的内存情况自动降档默认使用1000个定时器对于内存受限的板子LOW_MEMORY_BOARDS列表如 arduino-mega2560、feather-m0、telosb 等降为100个对于内存极小的板子SUPER_LOW_MEMORY_BOARDS列表如 arduino-uno、atmega328p、nucleo-f031k6 等进一步降档。上述逻辑实现在 tests/bench/xtimer/Makefile 中。需要指出README 描述第三档为 20 个定时器而当前仓库的 Makefile 实际取值为12NUMOF_TIMERS ? 12实际数值以代码为准。用户也可以在命令行通过make NUMOF_TIMERS...显式覆盖Makefile 使用?保证命令行优先级最高。防误触机制只测操作不测触发基准的核心前提是只测量操作本身因此不允许任何定时器真正触发。源码用_triggers变量记录被触发的次数所有定时器的回调都被设置为_callback它仅将_triggers加一main.c。每一个基准场景结束后都会执行expect(!_triggers)main.c 等一旦任何定时器误触发测试立即失败。这保证了测得的耗时纯粹来自 set/remove/now 操作本身。时间补偿机制保证 first / middle / last 落在预期位置re-set() first/middle/last类测试假设 first、middle、last 三个定时器在队列中始终处于相同的索引位置。但前序操作本身也消耗时间若不补偿目标时间会整体漂移。为此程序引入了_base变量uint32_t _base; /* returns the interval for timer n that has to be set in order to insert it * into position n */ static uint32_t _timer_val(unsigned n) { return _base (SPREAD * n); }每个场景开始前都会执行_base BASE - (before - start)例如 main.c即用当前已流逝的时间反向修正基准时间从而让第 n 个定时器始终能以_base SPREAD*n的目标时间落入队列中第 n 个位置。_timer_val()返回的就是为将定时器插入到位置 n 而需要设置的间隔。十个基准场景逐一拆解程序共输出 13 行结果10 个场景 头部说明 sizeof(xtimer_t) 结束标记测试脚本 01-run.py 中range(13)与之对应。下面按 README 的编排逐个说明。set() one向空列表反复设置单个定时器循环REPEAT次调用xtimer_set(_timers[0], ...)main.c。除第一次外每次 set 都会隐式地先移除已设置的定时器见下文原理同时每次迭代都会更新底层 periph timer——因为新定时器成为链表头需要重新调度底层硬件中断。该场景衡量的是单个定时器反复重设的最小成本。remove() one反复移除单个定时器循环调用xtimer_remove(_timers[0])main.c。由于上一场景结束后定时器已全部被清掉除第一次外此场景中列表是空的。它衡量 remove() 在空列表/单元素列表上的固定开销。set() remove() one先设后删每次迭代先 set 再 removemain.c迭代前后列表均为空。每次 set 都会更新底层 periph timer。该场景综合了完整的一次设置与一次撤销的往返成本。set() many increasing targets批量递增插入连续插入NUMOF_TIMERS个定时器目标时间依次递增main.c。由于_timer_val(n)严格递增每个新定时器都会被追加到链表末尾因此只有第一次插入会更新底层 periph timer此后链表头不变。该场景结束后列表中保留着NUMOF_TIMERS个已设置的定时器供后续场景使用。它衡量的是向有序链表末尾追加的平均插入成本。re-set() first / middle / last定点重设三个位置三个场景分别反复重设链表中的第一个索引 0、中间索引NUMOF_TIMERS/2和最后一个索引NUMOF_TIMERS-1定时器main.c目标时间保持不变re-set() first每次都会成为链表头因此每次迭代都更新底层 periph timerre-set() middle重设位于链表中部插入位置在链表中间但链表头不变不更新底层硬件定时器re-set() last重设位于链表末尾插入位置在链表尾部也不更新底层硬件定时器。这三个场景共同刻画了在 N 个定时器存在的列表中定点重设的best case / average case / worst case成本。remove() set() first / middle / last先删后设与上面三个场景一一对应但每次迭代先 remove() 再 set()main.c。由于显式 remove 会把定时器状态清零set() 不再需要隐式移除因此列表只被遍历一次。这类测试用于验证xtimer 能否正确识别未设置的定时器——若 remove() 后定时器仍残留于列表_triggers会意外累加或插入位置发生偏移测试结果即可暴露问题。remove() many decreasing从尾到头批量删除从最后一个定时器开始逆序移除全部NUMOF_TIMERS个定时器main.c。由于每次删除的总是当前列表末尾元素链表被完整遍历的次数逐次递减平均遍历约一半节点。该场景衡量从尾部逐个删除的累计成本。xtimer_now()纯时间读取在一个循环中反复调用xtimer_now_usec()main.c不涉及任何列表操作。它给出xtimer_now()的单次调用开销作为其他场景的底噪参考同时也用于隔离读时钟与操作链表两部分成本。附加输出sizeof(xtimer_t)程序最后输出sizeof(xtimer_t)乘以NUMOF_TIMERS的结果main.c即所有定时器结构体占用的内存字节数帮助评估该测试在目标板上的内存足迹。底层原理为什么是 O(n) 单链表理解结果之前必须先看懂 xtimer 的实现。核心代码位于 sys/xtimer/xtimer_core.c。双向列表结构与插入xtimer 内部维护两条按目标时间排序的链表短定时器链timer_list_head与长定时器链long_list_headxtimer_core.c。插入走_add_timer_to_list()static void _add_timer_to_list(xtimer_t **list_head, xtimer_t *timer, uint32_t now) { while (*list_head _timer_comparison((*list_head), timer, now)) { list_head ((*list_head)-next); } timer-next *list_head; *list_head timer; }插入需要从链表头开始逐个比较目标时间直到找到正确位置这正是 O(n) 的来源——set() many increasing targets之所以只更新一次底层定时器就是因为所有新定时器都排在链表末尾之后链表头从未变化xtimer_core.c 中只有timer_list_head timer时才重新调度底层硬件定时器。set() 的隐式 remove列表被遍历两次_xtimer_set64()的第一件事就是调用xtimer_remove(timer)xtimer_core.c。这意味着对一个已设置定时器执行 set() 时会先完整遍历一次链表把它找出来移除再遍历一次链表找到新位置插入——这就是 README 中每个 set() 都会隐式触发 remove()定时器列表因此被迭代两次的由来。这也解释了为什么set() one、re-set() first等场景的开销天然高于先显式 remove 再 set的组合。remove() 与未设置定时器的识别xtimer_remove()先将定时器的offset、long_offset、start_time、long_start_time全部清零再调用_remove_timer_from_list()从两条链表中查找并摘除节点xtimer_core.c。_remove_timer_from_list同样是线性遍历。而未设置状态的判定非常轻量xtimer_is_set()仅检查timer-offset || timer-long_offsetimplementation.h。remove() set()系列测试正是利用这一特性显式 remove 将字段清零后后续 set() 不会做隐式移除从而把两次遍历降为一次测出的是针对已清空定时器的最优插入路径。短定时器的自旋优化另一个影响测量结果的因素是XTIMER_BACKOFF默认 30 tickxtimer.h当定时器目标时间在XTIMER_BACKOFF以内时_xtimer_set64()不会插入链表而是直接自旋等待并立即触发回调xtimer_core.c。基准程序中BASE与SPREAD都取很大的值10 万微秒级别正是为了确保所有定时器都远大于 backoff 阈值、老老实实走链表路径保证测的是列表操作而非自旋。编译、运行与自动测试编译并运行在仓库根目录执行BOARD可替换为目标板native可在宿主机直接运行make -C tests/bench/xtimer BOARDnative flash term程序启动后会打印标题与各场景结果。若显式覆盖参数make -C tests/bench/xtimer BOARDnative NUMOF_TIMERS100 REPEAT500其中NUMOF_TIMERS会通过CFLAGS -DNUMOF_TIMERS$(NUMOF_TIMERS)Makefile传入编译。输出格式每个场景的输出由_print_result()格式化main.c%30s %8u / %u %u即场景描述 总耗时 / 迭代次数 单次平均耗时单位微秒。例如set() one 1234567 / 1000 1234自动化验证仓库提供配套的测试脚本 tests/bench/xtimer/tests/01-run.py使用 RIOT 的 testrunner 框架验证程序行为精确匹配启动横幅xtimer benchmark application.、校验 13 行结果均符合数字 / 数字 数字格式并以done.结束。运行方式为make -C tests/bench/xtimer BOARDnative test如何解读结果越低越好看相对位置根据 README 的指引解读结果时把握以下几个要点测量目标明确所有数字表示的都是 xtimer 列表操作消耗的时间数值越低越好。first / middle / last 对应三档复杂度当链表中有NUMOF_TIMERS个定时器时re-set() first代表best case链表头部操作re-set() middle代表average case链表中部操作re-set() last代表worst case链表末尾操作。三者之间的差异直接反映了单链表线性遍历的成本若last明显慢于first说明插入/删除确实在退化到 O(n) 的线性扫描。注意隐式 remove 的双倍遍历凡是对已设置定时器直接 set()的场景如set() one、re-set() *每次操作实际上会遍历链表两次先隐式 remove 再插入其开销应高于对应的remove() set()版本。remove() set() 系列验证 unset 识别remove() set() first/middle/last与对应re-set()场景的差值可以衡量 xtimer 对未设置定时器的识别路径是否正确且高效——显式 remove 之后set() 直接进入一次遍历的快速路径。xtimer_now() 作为底噪它给出读取时钟的固定成本可将其他场景的结果减去该值得到更纯粹的链表操作开销。结合sizeof(xtimer_t)评估可扩展性输出末尾的内存占用数据可用于判断目标板能够支撑的并发定时器规模与NUMOF_TIMERS的降档规则相互印证。演进说明xtimer 已被 ztimer 取代需要特别说明的是当前仓库中 xtimer 已被标记为deprecatedxtimer.h 明确建议新代码使用ztimer模块并提供了ztimer/xtimer_compat兼容层多数情况下可作为 xtimer API 的即插即用替代。对于长时间运行的定时器直接使用 ztimer 相比兼容层还能获得更低的时钟漂移与功耗。因此这套基准测试所测量的链表算法特性与性能数据对于理解 RIOT 定时器抽象层的演进、以及评估兼容层与原生 ztimer 的差异仍然具有直接的参考价值。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考