Core RAM 使用分布与裁剪指南

文档范围

  • 本文面向 EmbeddedGUI core/framework 层,说明当前 RAM 的主要组成、热点分布和裁剪规则。

  • 当前示例数据来自 HelloPerformance + PORT=qemu + CPU_ARCH=cortex-m3,用于帮助定位 RAM 大头,不代表所有应用都完全一致。

  • PFB 会单独列出,但它属于用户配置项,不计入框架 RAM 优化成果。

  • 业务层自定义 view、应用资源、端口私有缓冲区,以及链接脚本保留的 stack/heap 区域,同样会占用同一片 SRAM;本文重点解释 core 侧的占用方式和裁剪顺序。

RAM 三类

建议把 EmbeddedGUI 的 RAM 按下面三类来审视:

类别

典型位置

典型内容

规则

静态 RAM

.bss / .data

核心状态机、定时器、少量固定元数据、PFB

只保留与尺寸无关、上界稳定、收益明确的小对象

跟随尺寸走的 buffer

运行时 scratch / cache

跟随字体大小、图片大小、屏幕尺寸、PFB 尺寸变化的 buffer

必须走 heap,不能为了回避 heap 改成宏固定 RAM、静态数组或大栈数组

Heap

单次绘制 / 单帧 / 跨帧缓存

图像解码 row cache、文字变换 scratch、外部字体 scratch、图片 resize scratch

优先短生命周期;如需常驻,必须记录 owner、lifetime、bytes 和收益

额外需要单独评估的一类是 stack

  • stack 不属于上面三类,但它和 .bss/.data/heap 竞争同一片 SRAM。

  • 大局部数组同样可能把系统栈顶爆。

  • 任何新引入的大栈对象,都必须说明必要性,并用 -fstack-usage 复核。

额外的 HelloPerformance 硬约束:

  • 不接受整个图像尺寸的 resident heap,即使它能换来明显性能收益。

  • 尺寸相关 heap 的上限只接受两类:

    • 不超过 1 * PFB 字节;

    • 或最多 2 行 / 2 列图像、屏幕尺寸相关 scratch/cache。

  • 如果后续确实需要更高性能版本,也必须在这个上限内做“双版本”选择,不能靠 whole-image cache。

当前 HelloPerformance / QEMU 快照

当前 HelloPerformance/QEMU 的 RAM 观测值如下:

项目

当前值

说明

text

2135636B

代码和只读数据

data

52B

已初始化静态 RAM

bss

2356B

llvm-size 汇总值,包含链接脚本保留区

.bss

1920B

实际未初始化静态符号区

._user_heap_stack

436B

当前链接脚本保留区,不是 core 固定对象本身

固定静态 RAM 小计

1972B

.data + .bss,不含链接脚本保留区

Heap 峰值

5008B

QEMU_HEAP_MEASURE=1 实测峰值

Heap 空闲 current

0B

当前默认低 RAM 方案下,尺寸相关 scratch 最终都会释放

alloc/free

5434 / 5434

完整录制流程结束后配平

编译期最大栈帧

1200B

egui_view_heart_rate_on_draw(),不在当前录制热路径

当前活跃路径最大栈帧

432B

egui_canvas_draw_thick_line_scan()

说明:

  • llvm-sizebss=2356B 里包含了 ._user_heap_stack=436B,看总表时不要把它全部当成 core 固定静态 RAM。

  • 从 core 固定对象角度看,当前真正长期常驻的 .data + .bss 只有 1972B

  • 其中 egui_pfb=1536B 是当前应用配置下的 PFB,这是用户自己选择的空间换时间项,不应被当成框架裁剪成果。

  • heap peak=5008B 是运行时峰值,不是 idle 常驻占用;当前默认示例结束后 current heap 会回到 0B

  • 如需继续追峰值归因,可在测量构建里额外打开 QEMU_HEAP_TRACE_ACTIONS=1,让 QEMU 按录制 action 输出 HEAP_ACTION:<idx>:current/peak/allocs/frees,默认关闭时不会改变当前 RAM 口径。

  • HelloPerformance 现在把 QEMU 链接脚本保留栈压到 432B,所以静态 RAM headline 已反映这一调整。

  • 2026-03-30 又做过一次继续下压到 0x0180384B)的 A/B,但该 probe 已明确拒绝且没有保留。原因不是 egui_canvas_draw_thick_line_scan() 不能再降,而是 fresh clean -fstack-usage / main.map / llvm-nm 交叉复核后,新的 linked ceiling 会立刻转移到 egui_canvas_draw_circle_fill_gradient (424B),后面还有 egui_canvas_draw_rectangle_fill_gradient (416B)egui_canvas_draw_polygon_fill_gradient (408B)egui_canvas_draw_polygon_fill (408B)egui_image_rle_draw_image (392B);在“几个字节的不管”这条规则下,这意味着当前默认 reserve 没必要继续为了 <=24B 的静态 RAM 去冒险。

  • 当前正常 QEMU 构建口径是 text=2135636data=52bss=2356static RAM=2408;buffered rotated-text 路径已于 2026-03-31 删除,相关 alpha8 visible-cache / packed4 fallback heap 口径不再属于当前 shipped path;当前 whole-run heap headline 仍由 codec 场景决定。

  • Core 仍保留默认关闭的 global logical PFB probe 作为测量工具,但 HelloPerformance 现在已经接受一个 shipped 的按场景 width hint:egui_core_get_logical_pfb_target_width_hint() 对当前高 heap 的 codec hotspot 返回 96,其他场景返回 0。因此非热点场景仍走原始 48x16,被选中的 codec 场景改为逻辑 96x8,而物理 PFB 字节数完全不变。

  • QOI/RLE 解码状态已经从固定 .bss 挪到按帧 heap,而当前 shipped 的按场景 logical 96x8 walk 继续把 whole-run heap headline 压到 5008B。当前第一峰值 owner 是 IMAGE_TILED_QOI_565_8,已验证的热点梯队是 IMAGE_TILED_QOI_565_8 5008BIMAGE_TILED_RLE_565_8 4816BIMAGE_RLE_565_8 / EXTERN_IMAGE_RLE_565_8 3760B,以及 IMAGE_QOI_565_8 / EXTERN_IMAGE_QOI_565_8 / MASK_IMAGE_QOI_8_ROUND_RECT / EXTERN_MASK_IMAGE_QOI_8_ROUND_RECT 3664B

  • EGUI_CONFIG_CORE_LOGICAL_PFB_PROBE_ENABLEEGUI_CONFIG_CORE_LOGICAL_PFB_PROBE_TARGET_WIDTH 和弱符号 egui_core_get_logical_pfb_target_width_hint() 仍主要用于手工 A/B;core 默认返回 0,而当前 shipped path 只是在 HelloPerformance 里额外覆写了一个按场景 hint:当前高 heap codec hotspot 返回 96,其他场景仍返回 0IMAGE_TILED_RLE_565_0 由于会带来 +13.10% 回归,明确不进入这组 hint。

  • 2026-03-30 的 probe follow-up 还顺手清掉了 resize src_x_map heap 路径里遗留的 count <= EGUI_CONFIG_PFB_WIDTH 断言。这个断言会把逻辑 tile 宽于物理 48px 的 probe build 直接卡死,但对默认 shipped path 的 48x16 几何、RAM headline 和性能结果都没有任何变化。

  • 历史上的 EGUI_CONFIG_IMAGE_STD_ROUND_RECT_FAST_ROW_CACHE_ENABLEEGUI_CONFIG_MASK_CIRCLE_FRAME_ROW_CACHE_ENABLE 已收回实现私有并保持默认关闭;这两个 PFB_HEIGHT 相关行缓存如果以后重新评估,也仍然必须走 heap,不能回退为静态 RAM 或大栈数组。

  • 当前默认 external raw-image shared row cache 也已收紧到 2 行上限:EGUI_CONFIG_IMAGE_EXTERNAL_DATA_CACHE_MAX_BYTES=960EGUI_CONFIG_IMAGE_EXTERNAL_ALPHA_CACHE_MAX_BYTES=480。这不会改变压缩图主导的 whole-run headline,只是当前 headline 现在是 5008B;对应 240px 外部 RGB565+alpha 场景的 scene-local heap 从旧的 2880B 压到 1440B,resize 场景则是 1536B

静态 RAM 分布

下面是当前示例里最主要的固定静态符号:

符号

字节

分类

说明

egui_pfb

1536B

用户配置项

当前 PFB 像素缓冲区

egui_core

120B

core 元数据

核心运行时状态

g_text_transform_prepare_cache

60B

core 元数据

旋转文字小型 affine prepare cache,不是尺寸相关 scratch

test_view

56B

app 示例对象

HelloPerformance 场景对象

canvas_data

32B

core 元数据

canvas 运行时状态

ui_timer

20B

app 示例对象

app 定时器

port_display_driver

16B

port 元数据

QEMU 显示驱动状态

g_font_std_code_lookup_cache

12B

core 元数据

字体 code lookup 缓存元数据

egui_image_decode_cache_state

12B

core 元数据

解码 cache 元数据,记录 tail-row cache 窗口信息

g_text_transform_visible_tile_cache

8B

heap handle 元数据

只保存 heap 缓冲句柄/容量

qoi_state_ptr

4B

core 元数据

QOI 解码器按帧 heap 状态句柄

rle_state_ptr

4B

core 元数据

RLE 解码器按帧 heap 状态句柄

s_qemu_resource_handle

4B

port 元数据

QEMU 资源句柄

判断原则:

  • 可以留在静态区的,应该是与字体大小、图片大小、屏幕尺寸、PFB 尺寸无关的小型状态对象。

  • 如果静态对象只是某个 heap buffer 的 owner / pointer / capacity 元数据,通常可以接受,因为真正的大块数据已经不再常驻 .bss

  • PFB 必须单独看。它通常是静态 RAM 里最大的一项,但这是应用配置,不是 core 算法残留。

  • QOI/RLE 解码器也改到了这一条线上:当前静态区只保留 qoi_state_ptr/rle_state_ptr 两个 4B 句柄,真正的 208B / 16B 解码状态只在当前帧解码期间上 heap,并在 egui_image_std_release_frame_cache() 释放。

  • 最新 external text/image transform 优化仍遵守这条线:只新增了流式读取和 chunk 对齐控制代码,没有把任何 glyph 或 image 尺寸相关 scratch 落回静态区。

跟随尺寸走的 buffer

以下这类 buffer 不允许为了“heap 看起来更小”而回退成静态数组或大栈数组:

  • 跟随字体大小变化

  • 跟随图片大小变化

  • 跟随屏幕尺寸变化

  • 跟随 PFB 尺寸变化

当前 core 中典型的尺寸相关 buffer 如下:

模块

owner

尺寸驱动因素

当前放置

生命周期

压缩图单行解码 scratch

egui_image_decode_*

图片宽度、像素格式、alpha 行宽

heap

单次解码 / 当前帧

压缩图 row-band / tail-row cache

egui_image_decode_*

row_count * image_width * bytes_per_pixel,受 PFB_HEIGHT 和图片宽度影响

heap

当前 refresh walk

masked QOI visible-span scratch

egui_image_qoi_draw_image()

当前可见列数 visible_col_count * (RGB565 + alpha8)

heap

单次 masked alpha 行 / 当前绘制

旋转文字 visible alpha8 tile

egui_canvas_draw_text_transform()

可见 glyph 变换后包围盒

heap

当前 refresh walk

rotated text layout scratch

egui_canvas_draw_text_transform()

glyph 数、line 数、布局结果

heap

单次绘制

rotated text tile scratch

egui_canvas_draw_text_transform()

可见 glyph 数、line 数

heap

单次绘制

图片 resize src_x_map

egui_image_std_*

当前绘制宽度

heap

单次绘制

round-rect 图片快速路径 row cache

egui_image_std_*

min(PFB_HEIGHT, image_height)

默认关闭;重开时仍走 heap

当前 refresh walk(启用时)

circle mask row cache

egui_mask_circle_*

3 * PFB_HEIGHT * sizeof(egui_dim_t)

默认关闭;重开时仍走 heap

当前帧(启用时)

外部字体 glyph / row scratch

egui_font_std_*egui_canvas_transform_*

字体大小、glyph bitmap 尺寸

heap

单次绘制 / 当前 refresh walk,visible-tile 外部旋转文字按 14 行 chunk 流式读取

关于宏的使用边界:

  • 宏可以表达“是否启用某个能力”或“某个路径的预算上限”。

  • 但不能用宏把尺寸相关 buffer 直接落成固定 .bss、静态局部数组或大栈数组。

  • 宏可以决定“需不需要这块 buffer”或“允许用多少 heap”;真正的大块存储仍然必须走 heap

当前 Heap 分布

当前默认示例的 heap 以短生命周期 scratch 为主,峰值 5008B,空闲 current 回到 0B

默认低 RAM 路径的大头

类型

典型 owner

特征

备注

压缩图 tail-row cache

egui_image_qoi_draw_image() / egui_image_rle_draw_image()

当前默认 heap 峰值主因

QOI 已去掉首个可见 alpha 段的 heap scratch,RLE alpha 仍保留可见段瞬时 scratch;后续都只保留横向 tail 列

rotated text scratch

egui_canvas_draw_text_transform()

随文本布局结果变化

已改为按 glyph/line 数动态申请

图片 resize / round-rect scratch

egui_image_std_*

随绘制宽度、图片高度、PFB_HEIGHT 变化

resize scratch 默认启用并用完立即释放;round-rect 行缓存默认关闭,若重开仍必须按需从 heap 申请并释放

circle mask frame scratch

egui_mask_circle_*

PFB_HEIGHT 变化

默认关闭;若重开,仍必须在当前帧内按需从 heap 申请并在帧结束释放

外部字体 scratch

egui_font_std_* / egui_canvas_transform_*

随 glyph bitmap 尺寸变化

用完或 frame 结束释放;visible-tile 外部旋转文字改为 14 行 chunk heap scratch

默认路径的单场景 heap 热点

场景

interaction total peak

说明

IMAGE_TILED_QOI_565_8

5008B

当前 whole-run 的第一峰值 owner;扩展后的 logical 96x8 shipped 路径已覆盖这类高 heap codec hotspot

IMAGE_TILED_RLE_565_8

4816B

logical 96x8 后的 tiled RLE alpha 场景

IMAGE_RLE_565_8

3760B

logical 96x8 后的 RLE alpha 场景

EXTERN_IMAGE_RLE_565_8

3760B

同上,外部资源版本

IMAGE_QOI_565_8

3664B

logical 96x8 后的 QOI alpha 场景

EXTERN_IMAGE_QOI_565_8

3664B

同上,外部资源版本

MASK_IMAGE_QOI_8_ROUND_RECT

3664B

QOI alpha8 round-rect 场景,已经明显低于当前 tiled codec 热点

EXTERN_MASK_IMAGE_QOI_8_ROUND_RECT

3664B

同上,外部资源版本

EXTERN_IMAGE_RESIZE_565_8

1536B

2 行 external shared RGB565+alpha row cache + resize src_x_map

EXTERN_IMAGE_565_8

1440B

2 行 external shared RGB565+alpha row cache

EXTERN_IMAGE_ROTATE_565_8

1440B

2 行 external transform-side shared RGB565+alpha row cache

补充说明:

  • 当前 whole-run heap peak 仍然由压缩图场景决定,而不是 text 场景;当前 headline 是 5008B,第一峰值 owner 为 IMAGE_TILED_QOI_565_8

  • 所有尺寸相关 scratch 最终都会回到 0B current heap;默认路径没有常驻 heap 大块缓存。

  • 当前压缩图热点可以按 shipped 默认分成三档:tiled codec hotspot 5008B / 4816B,普通 QOI/RLE alpha 3760B / 3664B,以及 buffered text 3544B / 3334B。这些数值都来自当前 shipped 的按场景 logical 96x8 路径;它不是被拒绝的“全场景 logical PFB probe”,而是只对当前高 heap codec hotspot 生效的定向 hint,且明确排除了 IMAGE_TILED_RLE_565_0

  • 2026-03-29 对 EGUI_CONFIG_IMAGE_CODEC_TAIL_ROW_CACHE_MAX_COLS 做过 A/B:14496 都会让 QOI/RLE alpha 场景退化到数倍,说明在当前 240px 屏宽、PFB_WIDTH=48、横向逐 tile refresh walk 下,192 尾列已经是默认单次解码路径的硬下限,而不是随手还能继续压的余量。

  • 2026-03-29 第二轮 A/B 又测了 176184 两档 tail cap,并配合 EGUI_CONFIG_IMAGE_QOI_CHECKPOINT_COUNT=1/2EGUI_CONFIG_IMAGE_RLE_CHECKPOINT_ENABLE=1。相对当前 d3d37bf 基线(IMAGE_QOI_565_8 2.258EXTERN_IMAGE_QOI_565_8 2.734IMAGE_RLE_565_8 1.561EXTERN_IMAGE_RLE_565_8 2.385),四组组合仍然退化 +45% ~ +66%,因此不再继续补 heap 数据,直接判定默认路径拒绝。

  • 这背后的几何约束已经比较清楚:在 240px 图宽、48px 水平 PFB walk 下,首个可见 tile 覆盖 0..47,后续横向邻居需要的列并集是 48..239,正好就是 192 列。只要单次 tail cache 小于 192,最后一个 tile 就必然 miss,进而触发额外 row-band 重解;checkpoint 只能把解码状态恢复到 row-band 起点,并不能改变这个覆盖条件。这里是根据当前 walk 顺序做的推导,上面的实测数据正好印证了这个结论。

  • 2026-03-29 还做过 tail alpha sparse A/B:尝试只保留 raw RGB565 tail-row cache,把 alpha8 改成 sparse row table(-DEGUI_CONFIG_IMAGE_CODEC_TAIL_ALPHA_SPARSE_ENABLE=1)。这一轮测量发生在后续 qoi_state/rle_state -> frame heap 之前,所以它只覆盖 row-cache 本体,不含现在额外的 208B/16B 解码状态。单场景 heap 当时确实从 9360B 降到 7512B,即 -1848B,但 IMAGE_QOI_565_8 / EXTERN_IMAGE_QOI_565_8 / IMAGE_RLE_565_8 / EXTERN_IMAGE_RLE_565_8 分别退化到 8.988 / 9.464 / 8.296 / 9.119 ms,相对当时默认 2.264 / 2.740 / 1.572 / 2.395 ms+245% ~ +428%。同时 alloc/free 也从 32 / 32 跳到 376 / 376,说明 sparse pack/expand 的运行时成本远超当前 RAM 收益,因此这一方向同样明确拒绝,不进入默认实现。

配置

IMAGE_QOI_565_8

EXTERN_IMAGE_QOI_565_8

IMAGE_RLE_565_8

EXTERN_IMAGE_RLE_565_8

结论

基线(d3d37bf

2.258

2.734

1.561

2.385

后续 frame-heap 状态调整前的默认基线

tail=176, qoi_cp=1, rle_cp=1

3.616 (+60.14%)

4.536 (+65.91%)

2.270 (+45.42%)

3.911 (+63.98%)

拒绝

tail=176, qoi_cp=2, rle_cp=1

3.617 (+60.19%)

4.537 (+65.95%)

2.270 (+45.42%)

3.911 (+63.98%)

拒绝

tail=184, qoi_cp=1, rle_cp=1

3.629 (+60.72%)

4.544 (+66.20%)

2.284 (+46.32%)

3.927 (+64.65%)

拒绝

tail=184, qoi_cp=2, rle_cp=1

3.630 (+60.76%)

4.544 (+66.20%)

2.284 (+46.32%)

3.927 (+64.65%)

拒绝

  • 按同样的几何关系推算,176184 其实也只是在 192 列 tail 上少了 16 / 8 列,checkpoint 开销扣除前的 raw tail 节省不过 768B / 384B。在已经实测到 +45% ~ +66% 退化的前提下,这条线没有继续做默认路径 heap 复测的必要。

  • 这组 buffered rotated-text heap/perf A/B 已经转入历史记录:当前代码已删除整条路径,后续默认口径不再单独跟踪这组 alpha8 visible-cache / packed4 fallback 指标。

  • 当前默认 external raw-image 路径也已经强制遵守 <=2 行 / 列尺寸相关 heap 约束:shared row cache 从旧的 1920/960 收紧到 960/480,因此代表性 scene-local heap 变为 EXTERN_IMAGE_565_8 1440BEXTERN_IMAGE_RESIZE_565_8 1536BEXTERN_IMAGE_ROTATE_565_8 1440B

  • 2026-03-29 又补做过 1 行 external raw-image row cache A/B:把 shared row cache 再压到 480/240 后,代表性 scene-local heap 会继续降到 EXTERN_IMAGE_565_8 720BEXTERN_IMAGE_RESIZE_565_8 816BEXTERN_IMAGE_ROTATE_565_8 720B,但 whole-run heap headline 仍然不变,还是被 codec 场景钉在当前的 5008B

  • 同一轮实测里,1 行方案在关键 direct-draw / rotate 场景退化超出当前 >500B => 10% 默认门线:EXTERN_IMAGE_565_1 1.458 -> 1.795 (+23.11%)EXTERN_IMAGE_565_8 1.630 -> 1.967 (+20.67%)EXTERN_IMAGE_ROTATE_565_1 13.474 -> 15.161 (+12.52%)EXTERN_IMAGE_ROTATE_565_8 15.875 -> 19.655 (+23.81%),因此当前正式默认值仍保持 2 行,1 行只保留为测量用 override,不进入默认实现。

  • 这一轮属于“按 RAM 规则收口”的默认低 RAM 路径,而不是 perf-neutral 优化:旧的 4 行 external raw-image 默认 cache 已经超出当前 HelloPerformance 上限,因此现在接受一部分外部原始图片场景变慢,最差实测为 EXTERN_IMAGE_565_1 +13.11%EXTERN_IMAGE_ROTATE_565_1 +12.56%EXTERN_IMAGE_ROTATE_TILED_565_8 +12.26%

  • 最新 external transform row-cache 对齐则是在同样 2 行 cache 上追回 rotate 性能,不改变 1440B / 1536B scene-local heap:EXTERN_IMAGE_ROTATE_565_1 14.696 -> 13.474 (-8.32%)EXTERN_IMAGE_ROTATE_565_2 15.508 -> 14.106 (-9.04%)EXTERN_IMAGE_ROTATE_TILED_565_0 11.707 -> 10.630 (-9.20%)EXTERN_IMAGE_ROTATE_TILED_565_8 17.633 -> 16.029 (-9.10%),而 direct draw / resize 保持 0.00%

  • 只要收益不足阈值,就优先保 RAM:<=500B RAM 变化用 5% 线,>500B RAM 变化用 10% 线。

  • 如果后续还要继续压缩这一段 heap,手段就不能再是“简单缩 tail 列数”,而需要新的解码/渲染架构,例如改变 PFB walk 顺序,或引入更细粒度的 decoder checkpoint。

Rejected Global Logical PFB Probe A/B

2026-03-30 在 core 里补了默认关闭的 logical PFB probe,只用于测量,不改变物理 PFB 字节数。后续 shipped path 接受的并不是“全局 probe 开启”,而是 HelloPerformance 自己提供的按场景 width hint:仅当前高 heap codec hotspot 返回 96,其他场景仍返回 0。下面这些 global / all-scenes probe 仍然全部拒绝。

实验

Heap 变化

性能结果

结论

全 codec 场景逻辑 tile 改为 64x12

9568B -> 6736B (-2832B)

QOI alpha 仍有 +2.11% / +6.23% / +1.46% / +6.98%,但内部 RLE 热点超出 >500B => 10% 门线:IMAGE_RLE_565 +16.26%IMAGE_RLE_565_8 +12.92%

拒绝

仅 QOI 场景逻辑 tile 改为 64x12

9568B -> 9376B (-192B)

节省只有 192B,但 IMAGE_QOI_565_8 / EXTERN_IMAGE_QOI_565_8 仍退化约 +6% ~ +7%,超过 <=500B => 5% 门线

拒绝

全 codec 64x12 + EGUI_CONFIG_IMAGE_RLE_CHECKPOINT_ENABLE=1

9568B -> 6736B (-2832B)

RLE checkpoint 对这条实验线几乎没有恢复作用,整体结果与上一行基本相同

拒绝

  • MASK_IMAGE_TEST_PERF_ROUND_RECT 等局部圆角遮罩场景在更宽逻辑 tile 下确实更快,但 shipped default 必须看整套场景是否满足当前 RAM/perf 规则,不能因为单个热点收益就带进默认路径。

  • python scripts/perf_analysis/main.pyPASSED 只表示当前跑通,不代表通过历史结果对比;这三组 probe 实验都是手工对照 doc/source/performance/perf_report.md 后判定拒绝。

  • 2026-03-30 的 follow-up 把 probe-only 的 resize 断言补齐后,96x8 / 128x6 / 192x4 这些更宽 shape 终于可以完整跑完;但它们依然没有一个能通过 shipped default 的全场景门线。

实验

Heap 变化

性能结果

结论

全场景 logical 96x8 tiles

未继续重跑 heap

codec 热点本身大多仍在可接受区间:IMAGE_QOI_565_8 -5.32%EXTERN_IMAGE_QOI_565_8 -4.51%IMAGE_RLE_565 +8.80%IMAGE_RLE_565_8 +1.91%;但一旦看完整 benchmark,非 codec 场景大面积退化:TEXT +17.51%CIRCLE_FILL +25.29%ANIMATION_TRANSLATE +15.65%IMAGE_TILED_565_0 +10.06%

拒绝

更宽 logical 128x6 tiles spot check

未继续重跑 heap

更宽 shape 的 targeted spot check 已经跨过门线:IMAGE_QOI_565_8 -10.22%EXTERN_IMAGE_QOI_565_8 -8.63%IMAGE_RLE_565 +11.07%MASK_IMAGE_QOI_8_ROUND_RECT +6.65%EXTERN_MASK_IMAGE_QOI_8_ROUND_RECT +8.98%

拒绝

最宽 logical 192x4 tiles spot check

未继续重跑 heap

最激进宽度方向更差:IMAGE_RLE_565 +25.52%IMAGE_RLE_565_8 +8.68%EXTERN_IMAGE_RLE_565 +9.27%MASK_IMAGE_TEST_PERF_ROUND_RECT +14.11%MASK_IMAGE_QOI_8_ROUND_RECT +17.83%EXTERN_MASK_IMAGE_QOI_8_ROUND_RECT +20.21%

拒绝

  • 这三组更宽 shape 都是在性能已经明显失败后就停止,没有继续补新的 whole-run heap headline,因为 shipped default 的前提是先过完整 RAM/perf 门线,而不是只看 codec 局部热点。

  • 被接受的 shipped 结论比上面的 rejected global probe 更窄:只对当前高 heap codec hotspot 复用 logical 96x8,并且显式排除 IMAGE_TILED_RLE_565_0。最新 follow-up 进一步把 whole-run heap 从 6448B 降到 5008B,同时整套 benchmark 仍满足 >500B => 10% 门线。这个 accepted path 不是上面被拒绝的“全场景 96x8”。

不接受的方向

下列方案即使性能收益明显,也不再接受:

方向

原因

whole-image resident cache

直接按整图尺寸申请 heap,违反 HelloPerformance 的 RAM 上限规则

用宏上限把整图或整屏 scratch 固定进 .bss / stack

本质仍是尺寸相关 buffer,只是把风险从 heap 转移到静态区或栈

因此,后续允许保留的高性能变体只能是:

  • 行缓存、列缓存、PFB 级 scratch;

  • 最多 2 行 / 2 列图像、屏幕尺寸相关 heap;

  • 且必须和默认低 RAM 方案分开记录。

栈使用评估

stack 不是本文的主分类,但必须同步审计。当前 -fstack-usage 结果中,较大的栈帧如下:

函数

栈帧

是否在当前 HelloPerformance 活跃路径

说明

egui_view_heart_rate_on_draw()

1200B

否(仅编译进入,已被当前镜像 GC 丢弃)

大局部数组热点;output/main.map 只在 Discarded input sections 中出现

egui_view_virtual_tree_walk_internal()

1112B

否(仅编译进入,已被当前镜像 GC 丢弃)

大局部 frames[EGUI_VIEW_VIRTUAL_TREE_MAX_DEPTH];仅出现在 Discarded input sections

line_hq_draw_polyline_segment()

976B

否(仅编译进入,已被当前镜像 GC 丢弃)

polyline helper 没有链接进当前 HelloPerformance benchmark 镜像

egui_canvas_draw_thick_line_scan()

432B

当前最大的 linked HelloPerformance 栈帧;局部 scratch 为标量状态,不跟随尺寸变化

egui_canvas_draw_circle_fill_gradient()

424B

当前第二大的 linked HelloPerformance 栈帧;固定 LUT 仍然与尺寸无关

egui_canvas_draw_polygon_fill_gradient()

408B

packed edge/intersection scratch 把原来的 536B 热点再压低 128B

egui_canvas_draw_polygon_fill()

408B

与渐变 polygon 路径使用同样的 packed scratch 思路

egui_view_gauge_on_draw()

88B

ring gradient / center dot / value text 拆到小 helper 后,app 侧 draw 帧已不再是热点

egui_canvas_draw_triangle_fill()

248B

函数级 optimize("Os") 把原来的 504B frame 压到 248B

egui_canvas_draw_image_transform()

344B

外部 whole-image cache 探测已撤掉,函数级 optimize("Os") 后进一步压缩

egui_canvas_draw_text_transform()

368B

函数级 optimize("Os") 后由 760B 降到 376B,最新 visible-list scratch 简化又轻微降到 368B

egui_canvas_draw_line_round_cap_hq()

280B

函数级 optimize("Os") 后由旧的高栈帧降到 280B

egui_canvas_draw_line_hq()

288B

函数级 optimize("Os") 后由 824B 降到 288B,不再是当前 linked 栈热点

egui_shadow_draw_corner()

176B

函数级 optimize("Os") 后明显缩小,且 SHADOW_ROUND 实测更快

栈侧规则:

  • 新增局部 buffer 之前,先判断它是否属于尺寸相关 buffer;如果是,直接走 heap

  • 即使不是尺寸相关 buffer,只要局部数组达到几百字节,也应该评估是否会把热路径 stack 顶高。

  • 提交前建议至少对热路径跑一次 -fstack-usage,确认没有新的异常膨胀。

  • Map 交叉复核结果:.su 里的 1200B1112B976B 这三个大栈帧,在当前 HelloPerformance 镜像里都只出现在 Discarded input sections,并未进入最终链接结果。

  • 当前最终链接镜像里的最大单函数栈帧是 432B,由 egui_canvas_draw_thick_line_scan 持有;其后是 egui_canvas_draw_circle_fill_gradient (424B)egui_canvas_draw_rectangle_fill_gradient (416B)egui_canvas_draw_polygon_fill_gradient (408B)egui_canvas_draw_polygon_fill (408B)egui_image_rle_draw_image (392B)egui_canvas_draw_text_transform (368B)egui_canvas_draw_image_transform (344B)egui_canvas_draw_line_hq (288B),最终镜像中仍然没有 >=1KB 的链接热点。

  • logical PFB 相关路径也已经做过 -fstack-usage 复核:core 侧 egui_core_get_logical_pfb_target_width_hint=4Begui_core_get_logical_pfb_probe_width=32Begui_core_apply_logical_pfb_probe_shape=32B,而 HelloPerformance 新增的 app 侧 helper egui_view_test_performance_uses_logical_pfb_96_hint=16B、override egui_core_get_logical_pfb_target_width_hint=8B,都没有引入新的大栈对象。

  • HelloPerformance 现在使用 __qemu_min_stack_size__=0x01b0,即 432B QEMU 预留栈;当前口径已由最新 .su/main.map 交叉复核、runtime 223/223、unit test 649/649python scripts/perf_analysis/main.py --profile cortex-m3 --threshold 10 共同支撑。

  • 这也说明当前 stack 方向已经接近收尾:后续如果只是把某一个 432B424B 热点单独压下去,而没有一起带动 416B/408B/392B 这组 active linked frame 下移,就不会带来值得保留的静态 RAM headline 变化。

建议的裁剪顺序

当用户继续压缩 RAM 时,建议按下面顺序判断:

  1. 先把 PFB 当作应用配置项单独评估,不要把减小 PFB 误记成 core 优化成果。

  2. 先排查是否还有尺寸相关 buffer 被放在 .bssstack

  3. 再看是否存在“只带来轻微性能收益”的常驻 heap 或静态 cache。

  4. 最后再考虑压缩少量固定元数据,因为这类收益通常是几十字节级别。

通常最值得优先关注的是:

  • 图像解码 row cache / single-row scratch

  • 外部图像 row cache

  • 文字变换 scratch

  • 外部字体 glyph scratch

  • 跟随 PFB_HEIGHT 的 mask / round-rect row cache

  • 跟随绘制宽度变化的 resize map

建议的记录口径

后续所有 RAM 相关提交,建议至少同步记录以下数据:

维度

建议记录内容

静态 RAM

llvm-sizedata/bss,以及 llvm-size -A 拆开的 .bss、链接脚本保留区

固定静态大头

llvm-nm -S --size-sort --print-size 的主要符号

Heap

peak/current、alloc/free、常驻 heap 的 owner/lifetime/bytes

Stack

-fstack-usage 的热点函数,区分“当前活跃路径”和“仅编译进入”

结论

是否有新的尺寸相关 buffer 落回 .bssstack

建议复测命令:

make clean
make all APP=HelloPerformance PORT=qemu CPU_ARCH=cortex-m3
llvm-size output/main.elf
llvm-size -A output/main.elf
llvm-nm -S --size-sort --print-size output/main.elf

Heap 观测:

make clean
make all APP=HelloPerformance PORT=qemu CPU_ARCH=cortex-m3 USER_CFLAGS="-DQEMU_HEAP_MEASURE=1 -DQEMU_HEAP_ACTIONS_APP_RECORDING=1 -DEGUI_CONFIG_FUNCTION_RECORDING_TEST=1"

按 action 追踪 heap 峰值归因:

make clean
make all APP=HelloPerformance PORT=qemu CPU_ARCH=cortex-m3 USER_CFLAGS="-DQEMU_HEAP_MEASURE=1 -DQEMU_HEAP_ACTIONS_APP_RECORDING=1 -DQEMU_HEAP_TRACE_ACTIONS=1 -DEGUI_CONFIG_FUNCTION_RECORDING_TEST=1"

Stack 观测:

make clean
make all APP=HelloPerformance PORT=qemu CPU_ARCH=cortex-m3 USER_CFLAGS="-fstack-usage"

同时建议每次同步更新:

  • example/HelloPerformance/ram_tracking.md

小结

当前 EmbeddedGUI core 的 RAM 使用可以概括为:

  • 固定静态 RAM 已经比较小,当前示例真正常驻的 .data + .bss1972B,其中 PFB 就占了 1536B

  • 需要跟随字体、图片、屏幕、PFB 尺寸变化的 buffer,必须继续保持 heap 化,不能为了追求“heap=0”回退到静态区或大栈。

  • 当前默认 heap 峰值 5008B 主要来自运行时 scratch,空间可回到 0B;当前第一峰值 owner 是 IMAGE_TILED_QOI_565_8 5008B,随后是 IMAGE_TILED_RLE_565_8 4816BIMAGE_RLE_565_8 / EXTERN_IMAGE_RLE_565_8 3760BIMAGE_QOI_565_8 / EXTERN_IMAGE_QOI_565_8 / MASK_IMAGE_QOI_8_ROUND_RECT / EXTERN_MASK_IMAGE_QOI_8_ROUND_RECT 3664B;若后续引入常驻 heap,必须明确记录 owner、lifetime、bytes。

  • 默认关闭的 core logical PFB probe 仍然保留在树里,作为后续架构 A/B 的测量工具;但 HelloPerformance 现在已经接受一个更窄的 shipped 版本,即仅对当前高 heap codec hotspot 启用 logical 96x8 hint,并明确排除 IMAGE_TILED_RLE_565_0。任何更宽或更全局的 shape 仍然必须先通过整套 RAM/perf 门线,不能直接当成正式优化。

  • 对于高性能变体,也只能在 1 * PFB2 行 / 列尺寸相关 heap 的约束内做选择,不能回到 whole-image cache。