测试原则
- 与embassy进行对比:仅仅测试异步任务
- 与uc进行对比
- 测试异步任务与uc线程的执行效率
- 测试同步任务与uc线程的执行效率
测试程序
测试内容
时间利用
- 抢占调度时间:从中断服务程序开始,置对应任务被唤醒执行的时间
- 测试方案:在时钟中断开始时将某个引脚的电平置为高电平,在按钮的await之后将该引脚的电平翻转为低电平
- 测试目的:这一时间本质上代表了系统的实时性
这一段时间也表现出了我们系统的抢占调度的复杂性
- 时间精度:从任务被唤醒开始执行到下一次进入休眠状态的时间
- 测试方案:在bottom的await后将某个引脚的电平置为高电平,然后执行一段异步delay。在异步delay执行结束之后将该引脚的电平翻转为低电平。
- 测试目的:一方面是测试delay时间的精度,另一方面也表现了系统的实时性
设置的引脚情况:
interrupt的测试是pa1
thread的测试是pa0
空间利用
- 静态空间大小:.text段、.bss段、.data段的大小
- 测试方案:直接通过arm-none-eabi-size显示
- 动态空间大小:程序运行相同数量任务所占用的最大栈空间
测试方案:将所有可能分配栈的空间的每一个字节都设定为某个特殊值,当栈被使用时就将被覆盖- 设置memory.x将ram大小设置为24K,设定程序栈分配单位为2K(冗余),增加任务数量测试爆栈情况(不需要测试embassy和ucosii,他们是否爆栈能在编译时确定)
吞吐量
吞吐量:在规定时间内完成的任务数量测试方案:设置若干个内容相同的任务,并且这些任务在执行若干次后将停止
不再测试吞吐量是由于在嵌入式操作系统的应用场景下,所有的任务都应该是一个无限循环
测试的难点
- 时间的度量 通过拉高拉低电平,通过逻辑分析仪查看引脚输出
测试的不足
- 无法测试任务间通信的情况(或者说能测试,但是使用的方式是相同的,都是使用共享内存进行传递,所以就没必要测试了。另外一个方面的原因是没有实现锁机制)
测试结果
实时性
对于有优先级的ucosii和embassy_preempt 我们设置按键中断为最高优先级
ucosii: delay时间(设置50ms),实际结果(单位ms) | ucosii: 任务被调度等待时间(单位 us) | embassy_preempt(rust_uc): delay时间(设置50ms),实际结果(单位ms) | embassy_preempt(rust_uc):任务被调度等待时间(单位 us) | embassy: delay时间(设置50ms),实际结果(单位ms) | embassy: 任务被调度等待时间(单位 ms) |
49.574 | 10 | 50.17 | 12 | 107.212 | 67.361 |
49.452 | 10 | 50.12 | 12.5 | 85.775 | 91.294 |
49.287 | 10 | 50.13 | 12.5 | 107.212 | 105.108 |
49.528 | 12 | 50.12 | 12 | 85.774 | 93.332 |
49.055 | 9 | 50.07 | 12.208 | 85.776 | 47.646 |
49.566 | 10 | 50.09 | 12.167 | 85.768 | 80.193 |
49.905 | 10 | 50.12 | 12.208 | 107.214 | 69.887 |
49.453 | 10 | 50.13 | 12.167 | 107.212 | 105.108 |
49.383 | 9.167 | 50.11 | 12.167 | 107.218 | 72.472 |
49.046 | 9.167 | 50.12 | 12.167 | 85.774 | 47.897 |
49.399 | 9.167 | 50.12 | 12.167 | 85.773 | 68.355 |
49.833 | 9.167 | 50.11 | 12.208 | 107.212 | 119.003 |
49.185 | 9.125 | 50.17 | 12.208 | 85.774 | 47.897 |
49.046 | 9.167 | 50.13 | 12.167 | 85.773 | 103.280 |
avg:49.408 | avg:9.7114 us | avg: 50.12214 | avg:12.20243 us | avg: 94.96193 | avg: 79.9166 ms |
偏差比例:1.184%(能接受) | ㅤ | 偏差比例:0.244%(能接受) | ㅤ | 偏差比例:89.924%(完全不能接受) | ㅤ |
关于调度时间,我们进行了重新测试(单位均为us):
ㅤ | ucosii | embassy_preempt线程模式 | embassy_preempt协程模式 |
1 | 7.00 | 7.375 | 10.125 |
2 | 7.042 | 7.375 | 10.167 |
3 | 7.00 | 7.375 | 10.167 |
4 | 7.042 | 8.333 | 10.125 |
5 | 7.00 | 8.333 | 10.167 |
6 | 7.00 | 7.375 | 10.083 |
7 | 7.042 | 7.375 | 10.083 |
8 | 7.00 | 8.333 | 10.167 |
9 | 7.00 | 7.375 | 10.125 |
10 | 7.00 | 8.333 | 10.167 |
11 | 7.042 | 7.375 | 10.167 |
12 | 7.00 | 7.375 | 10.167 |
13 | 7.00 | 8.333 | 10.083 |
14 | 7.00 | 7.375 | 10.125 |
15 | 7.042 | 7.375 | 10.083 |
平均值 | 7.014 | 7.6943 | 10.1334 |
数据分析:
- 解释8.333的问题(也是分析线程模式和ucosii时间差距的关键),是因为当当前是idle任务时,如果发生抢占,我们不希望idle任务保存上下文,所以如果切换到的任务有栈,那么就会将原来的程序栈释放掉,设置新的程序栈为有栈的任务的栈,从而回收栈,所以这就导致了回收栈的额外开销(≈1us—对应84条指令的样子),经过测试,在任务执行频率很高的情况下,或者协程任务比较多的情况下,这种情况就几乎不会发生,所以embassy_preempt协程模式的数据区别不是很大
- 而协程模式之所以会多2.5us的样子,是因为几个地方的开销:
- 栈分配:这里的栈分配指的是我们已经优化了任务抢占idle的情况的。在有协程存在的情况下,当一个处于协程状态的任务抢占了另一个非idle的任务,栈分配是不可避免的。
- 模拟压栈:由于每次分配栈以及进行栈转移(即协程任务抢占idle之后,将idle栈转移给该任务)之后都需要进行模拟压栈以便后续PendSV进行现场恢复。
- poll执行:poll函数的执行为协程任务唤醒的必备流程,是无法优化的
空间利用
- 前提条件:对ucosii而言,将所有的任务栈大小都设置为512B
经过测试,对我们设置的任务大小,ucosii的爆栈边缘为72B,但是在实际的应用过程中,任务需要的栈大小将远大于72B,因此在我们的测试中,我们将任务栈的大小设置为512B作为ucosii运行时的所有任务的栈的平均值,用于估计63个任务的整体RAM开销
- 动态空间大小:
- Rust-uC(通过爆栈的方式来测出接近的动态栈开销,配置RAM为24KB)
- Embassy(通过爆栈的方式来测出接近的动态栈开销,配置RAM为12KB)
- ucosii(动态ram开销和静态一致)
bss段的主要开销在于arena的开销,因为future创建需要ram空间,我们使用arena静态分配了一个10K大小的池子用于future内存的放置。剩下14K左右用于栈空间,程序栈以2K为单位分配,异常栈使用2K,测出低于14K时发生爆栈(测试时长30min)(不低于14K时正常执行),所以在64个任务存在的情况下,峰值情况最多有6个任务拥有自己的堆栈。也就是最大栈开销在14K
解释一下这里为什么设置2K大小的程序栈,是因为我们的栈是动态分配,不可能精确的知道每个任务需要多少栈然后在抢占时分配,并且这也不方便使用我们快速的分配算法(block-allocation),所以我们这里进行最坏的假设测试,给出2K的最大栈开销,保证不可能有正常任务执行时使用开销超过分配给他的栈,这样的话任务的栈开销也就只有最坏情况的5倍
由于在embassy的运行过程中有且仅有一个程序栈,因此在测试embassy的栈开销时,我们只能不断缩小RAM区直到出现栈溢出的情况
当RAM空间开到12KB、arena大小为10K时,63个简单任务刚好能运行,总共耗费栈的峰值在2KB。
在ucosii中,bss段的主要开销为任务栈以及各种OS需要的数据结构(如TCB等)
在每一个任务栈平均值为512B时,ucosii运行63个任务的RAM开销(消费栈以及一些数据结构(占少数))大约为512B*64(包含了一个idle)=32KB
63个任务运行下,不同RTOS的RAM开销
ㅤ | embassy | embassy_preempt | ucosii |
栈空间 | 2k | 12k | 32k |
RAM开销 | 12k | 24k | ≈40k |
debug
opt汇编对比
好像发现问题:
优化前大小:
优化前大小:
空间甚至变小了。