用例目标
- 测什么:申请一块匿名内存的耗时 ——
touch模式 = mmap + 首触缺页(纯申请);memset模式 = mmap + 整块写入(申请 + 写入)。 - 怎么对比:轻载与重载的唯一差异是
-b ballast参数;重载时 harness 自动 建 ballast → 等压力稳定 → 测量 → 自动释放,全程同一套命令。 - 关键约束:每轮测量前先快照系统内存,申请耗时与申请时刻前的内存统计值逐轮配对成
一条
REC记录 —— 耗时永远可以溯源到"当时系统处于什么状态"。 - 跨平台:iOS(越狱)与 Linux 用完全相同的步骤;harness 为纯 POSIX sh (bash 3.2 / zsh / dash / Ubuntu sh 实测通过),不依赖 awk/bc/python。
环境准备
| 平台 | 编译 | 部署要点 |
|---|---|---|
| Linux | make | 产物与 run_case.sh 放同一目录即可 |
| macOS (调试用) | make | 同上;与 iOS 共用 Darwin 分支 (vm_stat) |
| iOS (越狱) | make ios | scp 到设备后设备端 /usr/bin/ldid -S<ents.plist> 签名 + chmod +x(Mac 侧签名会 SIGKILL) |
运行依赖:sh、date、uname、kill、
内存源(Darwin: vm_stat + sysctl;Linux: /proc/meminfo)。
用例步骤(标准序列)
标准四步size 按需替换,此处 200M
# ① 轻载基线(系统空闲态)
sh ./run_case.sh -s 200M -m touch -r 5 > case-light-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5 > case-light-memset.txt
# ② 重载(自动: 建 ballast → 等稳定 → 测量 → 释放)
sh ./run_case.sh -s 200M -m touch -r 5 -b 3600 > case-heavy-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5 -b 3600 > case-heavy-memset.txt
# iOS 远程执行(例):
ssh -p 2222 root@localhost 'cd /var/mobile/ios_memslice && sh ./run_case.sh -s 200M -m touch -r 5 -b 3600'
参数
| 参数 | 默认 | 含义 |
|---|---|---|
-s | 200M | 申请大小,仅整 MiB(如 64M / 200M / 500M) |
-m | touch | touch = 纯申请(首触缺页);memset = 申请 + 写入 |
-r | 5 | 轮数;每轮独立配对一次申请前内存快照 |
-b | 0 | ballast(MiB);>0 时自动建压/释放(mem_stress -P 50) |
-w | 20 | ballast 压力稳定等待秒数 |
-t | 900 | ballast 死开关(秒)—— SSH 失联也能自愈 |
ballast 取值:物理内存的 45%~60%(6GB 手机 ≈ 2700~3600)。
必须用 -P 50 半随机填充(压缩比 ~2:1,压缩器有余量)——不可压缩的全随机填充会塞满
XNU 压缩器导致 watchdog 内核 panic(实测事故见主页实验报告)。
内置安全机制:①
-t 死开关到期自动释放;② 稳定期 free < 16MiB 自动中止(exit 3);
③ ballast 进程异常退出立即中止;④ trap EXIT/INT/TERM 保证任何路径下释放 ballast。
输出格式约束
输出为行式记录:每行一类,行首是记录类型标签,后接空格分隔的 key=value 令牌。
除 ERROR 行可含自由文本外,所有值不含空格。
记录类型
| 行类型 | 时机 | 作用 |
|---|---|---|
CASE | 每次调用开头,恰好一行 | 用例元数据(平台/内核/规模/模式/轮数/ballast) |
PHASE | 阶段切换 | 生命周期与阶段级内存快照(pre_;释放用 post_) |
REC | 每轮测量一行 | 申请耗时 + 缺页次数 + 申请前/驻留时内存统计(核心记录) |
SUMMARY | 测量结束 | fill_ms 的 avg/min/max(可由 REC 重算) |
ERROR | 异常 | ERROR code=<名称> ...;退出码 2=参数/环境,3=ballast 安全中止,4=测量/解析失败 |
REC 字段
| key | 单位 | 含义 |
|---|---|---|
run | — | 轮次,1..runs |
size_mib / ballast_mib | MiB | 申请大小 / 当前 ballast 总量(轻载=0) |
mode | — | touch / memset |
mmap_us | µs | 建映射耗时(XNU 预留免费,与压力无关,用于交叉验证) |
fill_ms | ms | 申请耗时主体:touch=首触全部页;memset=整块写入 |
unit + unit_label | — | touch → ns/page(单页首触成本);memset → MiB/s(写入吞吐) |
unmap_ms | ms | munmap 释放耗时 |
rss_mib | MiB | 本轮峰值驻留(应为 size+页表开销) |
faults | 次 | 申请窗口内总缺页次数(mmap→fill→读回全程;Darwin 取 task_events_info,其余 getrusage);-1=平台不可用 |
majflt | 次 | 其中需磁盘 I/O 的主缺页(匿名首触通常全为 0;出现非零说明已在换页) |
pre_* | MiB | 申请时刻前的系统内存快照(见下表) |
post_* | MiB | 申请驻留时快照:fill 完成后 mem_bench 保持驻留 3s(不计入耗时),驻留期采样;与 pre_* 同键集 |
pre_* / post_* 内存指标统一口径(同键集,平台映射)
| 统一 key | 含义 | Darwin (vm_stat) | Linux (/proc/meminfo) |
|---|---|---|---|
pre_total | 物理内存总量 | sysctl hw.memsize | MemTotal |
pre_free | 空闲 | Pages free | MemFree |
pre_avail | 可分配(含可回收) | -1 | MemAvailable |
pre_free_spec | 空闲+投机页 | Pages free + speculative | -1 |
pre_file | 文件缓存 | File-backed pages | Cached |
pre_anon | 匿名驻留 | Anonymous pages | AnonPages |
pre_swap_comp | 换出/已压缩数据量 | Pages stored in compressor | SwapTotal − SwapFree |
pre_comp_occ | 压缩器物理占用 | Pages occupied by compressor | -1 |
不变量(消费方可以依赖)
- 配对语义:每条 REC 的 pre_* 为该轮 mem_bench 启动之前的快照(间隔 ≤ 一次进程 spawn);post_* 为 fill 后驻留期(约启动后 1~1.5s)的快照;轻重视对比只需按
ballast_mib分组。 - 缺页恒等:touch/memset 模式下
faults ≈ size/页大小(每页恰好 1 次首触缺页;实测 200M/16K 页 = 精确 12800);明显超出即存在非本申请的缺页噪声,可作数据质量校验。post_anon − pre_anon ≈ size(驻留落账)。 - 生命周期顺序:
case_start → [ballast_start → ballast_settled | ballast_abort] → REC×runs → [ballast_released];ballast_* 仅在 -b>0 时出现,released 行用post_前缀。 - 键完整:平台无此指标的键恒为
-1,不会缺键;所有内存值均为整 MiB。 - 可重算:SUMMARY 的 avg/min/max 可由全部 REC 的 fill_ms 重建(毫单位整数算术)。
- 单位固定:mmap_us=µs;fill_ms/unmap_ms=ms;内存=MiB;unit 按 unit_label 解释。
消费示例
parse.py逐轮提取 耗时 + 申请前 free
import sys
for line in open(sys.argv[1]):
if line.startswith("REC "):
rec = dict(kv.split("=", 1) for kv in line.split()[1:])
print(rec["run"], rec["fill_ms"], rec["pre_free"], rec["unit"], rec["unit_label"])
# 重载首触成本:
# awk: grep '^REC' case-heavy-touch.txt | tr ' ' '\n' | grep -A0 '^unit='
从网页到对比表 —— 完整走一遍
核心逻辑:格式由 run_case.sh 约束,不依赖设备 —— 任何人在任何 iOS / Linux 机器上执行第②步,
得到的
.txt 都是同一 key=value 格式;对比 = 把任意两份 case 文件喂给
compare_cases.py(第一个文件自动作为基准)。文件可任意组合:轻 vs 重、
iOS vs Linux、改动前后、不同内核版本。
① 取件(用例全部材料,均可在本页"脚本源码"区下载)
| 材料 | 来源 | 去向 |
|---|---|---|
mem_stress.c / mem_bench.c / Makefile | 本页 · 脚本源码区 | 编译(见环境准备),产物与脚本同目录 |
run_case.sh | 本页 · 脚本源码区(下载按钮) | 被测设备任意目录,chmod +x |
compare_cases.py | 本页 · 脚本源码区(下载按钮) | 任意电脑(对比在采集侧做,不要求设备有 Python) |
② 采集(被测设备上执行,生成四个 case 文件)
在被测设备上iOS 与 Linux 命令完全相同
sh ./run_case.sh -s 200M -m touch -r 5 > case-light-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5 > case-light-memset.txt
sh ./run_case.sh -s 200M -m touch -r 5 -b 3600 > case-heavy-touch.txt
sh ./run_case.sh -s 200M -m memset -r 5 -b 3600 > case-heavy-memset.txt
# iOS 远程执行同一命令, 例:
# ssh -p 2222 root@device 'cd /var/mobile/ios_memslice && sh ./run_case.sh -s 200M -m touch -r 5 -b 3600'
# 采集完把 .txt 拷回电脑 (scp / sftp), 或在设备上直接查看
③ 对比(电脑上执行)
compare_cases.py第一个文件 = 基准
# 同平台 轻载 vs 重载 (纯申请):
python3 compare_cases.py case-light-touch.txt case-heavy-touch.txt
# 同平台 轻载 vs 重载 (申请+写入):
python3 compare_cases.py case-light-memset.txt case-heavy-memset.txt
# 跨平台 同状态对比 (重命名区分平台后):
python3 compare_cases.py case-ios-light-touch.txt case-linux-light-touch.txt
④ 输出(以下为真实数据,非演示)
iOS · 轻载 vs 重载(touch,200M) —— 耗时表(含缺页) + pre/post 状态表 + 驻留开销表 + DELTA 汇总行:
compare-example-touch.txt
下载
COMPARE files=2 base=case-light-touch.txt size_mib=200 mode=touch
[耗时表]
file ballast runs fill_ms avg/min/max unit mmap_us unmap_ms faults
----------------------------------------------------------------------------------------------------------------
case-light-touch.txt 0 5 36.42 / 35.25 / 37.62 2845.5 ns/page 12.8 10.5 12800
case-heavy-touch.txt 3600 5 40.46 / 37.81 / 42.56 3160.8 ns/page 12.2 14.1 12800
[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)
指标 case-light-touch.txt case-heavy-touch.txt Δ
-------------------------------------------------------------------
total 5735.0 5735.0 +0.0%
free 3651.6 277.8 -92.4%
avail N/A N/A -
free_spec 3890.8 311.8 -92.0%
file 710.4 515.8 -27.4%
anon 451.0 4049.2 +797.8%
swap_comp 329.0 325.8 -1.0%
comp_occ 117.0 113.0 -3.4%
[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)
指标 case-light-touch.txt case-heavy-touch.txt Δ
-------------------------------------------------------------------
total 5735.0 5735.0 +0.0%
free 3462.4 93.2 -97.3%
avail N/A N/A -
free_spec 3701.0 113.0 -96.9%
file 709.8 501.6 -29.3%
anon 651.2 4248.0 +552.3%
swap_comp 329.0 325.8 -1.0%
comp_occ 117.0 113.0 -3.4%
[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)
指标 case-light-touch.txt case-heavy-touch.txt
---------------------------------------------------------
total +0.0 +0.0
free -189.2 -184.6
avail - -
free_spec -189.8 -198.8
file -0.6 -14.2
anon +200.2 +198.8
swap_comp +0.0 +0.0
comp_occ +0.0 +0.0
DELTA case-heavy-touch.txt vs base: fill_ms=+11.1% unit=+11.1% unmap_ms=+34.2% faults=+0.0% pre_free=-92.4%
# 机器可读行 (key=value, 与用例记录同风格):
ROW file=case-light-touch.txt size_mib=200 mode=touch ballast_mib=0 runs=5 fill_ms_avg=36.423 fill_ms_min=35.252 fill_ms_max=37.617 unit_avg=2845.5 unit_label=ns/page mmap_us_avg=12.8 unmap_ms_avg=10.475 faults_avg=12800.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=3651.6 pre_avail_avg=-1 pre_free_spec_avg=3890.8 pre_file_avg=710.4 pre_anon_avg=451.0 pre_swap_comp_avg=329.0 pre_comp_occ_avg=117.0 post_total_avg=5735.0 post_free_avg=3462.4 post_avail_avg=-1 post_free_spec_avg=3701.0 post_file_avg=709.8 post_anon_avg=651.2 post_swap_comp_avg=329.0 post_comp_occ_avg=117.0
ROW file=case-heavy-touch.txt size_mib=200 mode=touch ballast_mib=3600 runs=5 fill_ms_avg=40.459 fill_ms_min=37.807 fill_ms_max=42.564 unit_avg=3160.8 unit_label=ns/page mmap_us_avg=12.2 unmap_ms_avg=14.060 faults_avg=12800.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=277.8 pre_avail_avg=-1 pre_free_spec_avg=311.8 pre_file_avg=515.8 pre_anon_avg=4049.2 pre_swap_comp_avg=325.8 pre_comp_occ_avg=113.0 post_total_avg=5735.0 post_free_avg=93.2 post_avail_avg=-1 post_free_spec_avg=113.0 post_file_avg=501.6 post_anon_avg=4248.0 post_swap_comp_avg=325.8 post_comp_occ_avg=113.0
DELTA file=case-heavy-touch.txt vs=case-light-touch.txt fill_ms_pct=+11.1% unit_pct=+11.1% unmap_ms_pct=+34.2% faults_pct=+0.0% pre_total_pct=+0.0% pre_free_pct=-92.4% pre_free_spec_pct=-92.0% pre_file_pct=-27.4% pre_anon_pct=+797.8% pre_swap_comp_pct=-1.0% pre_comp_occ_pct=-3.4% post_total_pct=+0.0% post_free_pct=-97.3% post_free_spec_pct=-96.9% post_file_pct=-29.3% post_anon_pct=+552.3% post_swap_comp_pct=-1.0% post_comp_occ_pct=-3.4%
跨平台 · iOS vs Linux(同为轻载 touch,200M):
compare-example-cross.txt
下载
COMPARE files=2 base=case-ios-light-touch.txt size_mib=200 mode=touch
[耗时表]
file ballast runs fill_ms avg/min/max unit mmap_us unmap_ms faults
----------------------------------------------------------------------------------------------------------------
case-ios-light-touch.txt 0 5 36.42 / 35.25 / 37.62 2845.5 ns/page 12.8 10.5 12800
case-linux-light-touch.txt 0 5 210.46 / 166.24 / 282.39 4110.6 ns/page 7.4 32.4 51202
[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)
指标 case-ios-light-touch.txt case-linux-light-touch.txt Δ
-----------------------------------------------------------------------------
total 5735.0 4895.0 -14.6%
free 3651.6 3050.8 -16.5%
avail N/A 4326.6 -
free_spec 3890.8 N/A -
file 710.4 1316.0 +85.2%
anon 451.0 89.0 -80.3%
swap_comp 329.0 50.0 -84.8%
comp_occ 117.0 N/A -
[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)
指标 case-ios-light-touch.txt case-linux-light-touch.txt Δ
-----------------------------------------------------------------------------
total 5735.0 4895.0 -14.6%
free 3462.4 2871.8 -17.1%
avail N/A 4147.8 -
free_spec 3701.0 N/A -
file 709.8 1316.0 +85.4%
anon 651.2 289.0 -55.6%
swap_comp 329.0 50.0 -84.8%
comp_occ 117.0 N/A -
[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)
指标 case-ios-light-touch.txt case-linux-light-touch.txt
-------------------------------------------------------------------
total +0.0 +0.0
free -189.2 -179.0
avail - -178.8
free_spec -189.8 -
file -0.6 +0.0
anon +200.2 +200.0
swap_comp +0.0 +0.0
comp_occ +0.0 -
DELTA case-linux-light-touch.txt vs base: fill_ms=+477.8% unit=+44.5% unmap_ms=+209.8% faults=+300.0% pre_free=-16.5%
# 机器可读行 (key=value, 与用例记录同风格):
ROW file=case-ios-light-touch.txt size_mib=200 mode=touch ballast_mib=0 runs=5 fill_ms_avg=36.423 fill_ms_min=35.252 fill_ms_max=37.617 unit_avg=2845.5 unit_label=ns/page mmap_us_avg=12.8 unmap_ms_avg=10.475 faults_avg=12800.0 majflt_avg=0.0 pre_total_avg=5735.0 pre_free_avg=3651.6 pre_avail_avg=-1 pre_free_spec_avg=3890.8 pre_file_avg=710.4 pre_anon_avg=451.0 pre_swap_comp_avg=329.0 pre_comp_occ_avg=117.0 post_total_avg=5735.0 post_free_avg=3462.4 post_avail_avg=-1 post_free_spec_avg=3701.0 post_file_avg=709.8 post_anon_avg=651.2 post_swap_comp_avg=329.0 post_comp_occ_avg=117.0
ROW file=case-linux-light-touch.txt size_mib=200 mode=touch ballast_mib=0 runs=5 fill_ms_avg=210.465 fill_ms_min=166.240 fill_ms_max=282.386 unit_avg=4110.6 unit_label=ns/page mmap_us_avg=7.4 unmap_ms_avg=32.448 faults_avg=51202.0 majflt_avg=0.0 pre_total_avg=4895.0 pre_free_avg=3050.8 pre_avail_avg=4326.6 pre_free_spec_avg=-1 pre_file_avg=1316.0 pre_anon_avg=89.0 pre_swap_comp_avg=50.0 pre_comp_occ_avg=-1 post_total_avg=4895.0 post_free_avg=2871.8 post_avail_avg=4147.8 post_free_spec_avg=-1 post_file_avg=1316.0 post_anon_avg=289.0 post_swap_comp_avg=50.0 post_comp_occ_avg=-1
DELTA file=case-linux-light-touch.txt vs=case-ios-light-touch.txt fill_ms_pct=+477.8% unit_pct=+44.5% unmap_ms_pct=+209.8% faults_pct=+300.0% pre_total_pct=-14.6% pre_free_pct=-16.5% pre_file_pct=+85.2% pre_anon_pct=-80.3% pre_swap_comp_pct=-84.8% post_total_pct=-14.6% post_free_pct=-17.1% post_file_pct=+85.4% post_anon_pct=-55.6% post_swap_comp_pct=-84.8%
怎么读对比表
- fill_ms 是主指标:Δ +11.2% = 重载下"申请 200M"慢了 11%;touch 用 ns/page、memset 用 MiB/s 互为印证。
- 状态表全量呈现 8 项 pre_*(申请前)与 post_*(申请驻留时):pre_free 3652→278(-92%)证明两种系统状态;post_anon−pre_anon ≈ +200 = 驻留落账。跨平台对比时 N/A 列恰好互补(iOS 有 free_spec/comp_occ,Linux 有 avail)。
- 驻留开销表(post−pre)是"申请 200M 的净代价":轻载 anon+200 / free−189 / file−0.6(纯落账,无挤出);重载 file−14.2(内核驱逐文件页腾地)。挤出/压缩行为只在 post 里可见。
- faults 列是数据质量交叉验证:两种状态下都精确 12800(=200M/16K 页),说明 fill 耗时差异全部来自每缺页成本,而非缺页数量变化;majflt≠0 则说明已在换页。
- unmap_ms 变贵(+87.6%)是压力下的次生现象,同样被记录。
- Linux 示例的巨大方差(min 35ms / max 471ms,实验内核虚拟机)恰好展示格式价值:每个离群轮次都能在对应 REC 的 pre_* 里找到当时系统状态,min 更接近真实冷性能基线。
三平台真实输出(均为实测,非演示数据)
iOS — iPhone 12 Pro · iOS 14.8.1 · /bin/sh=dash · touch 64M ×2 · ballast 1200M
case-sample-ios.txt
下载
CASE ts=2026-10-09T17:14:55+0800 platform=Darwin kernel=Darwin-20.6.0 arch=iPhone13,3 size_mib=64 mode=touch runs=2 ballast_mib=1200 tool=mem_bench
PHASE name=case_start ts=2026-10-09T17:14:55+0800 pre_total=5735 pre_free=3926 pre_avail=-1 pre_free_spec=3932 pre_file=476 pre_anon=404 pre_swap_comp=338 pre_comp_occ=118
PHASE name=ballast_start ts=2026-10-09T17:14:55+0800 ballast_mib=1200 hold_s=900 pre_total=5735 pre_free=3926 pre_avail=-1 pre_free_spec=3932 pre_file=476 pre_anon=404 pre_swap_comp=338 pre_comp_occ=118
PHASE name=ballast_settled ts=2026-10-09T17:15:16+0800 waited_s=20 pid=16728 pre_total=5735 pre_free=2723 pre_avail=-1 pre_free_spec=2729 pre_file=476 pre_anon=1609 pre_swap_comp=338 pre_comp_occ=118
REC run=1 size_mib=64 mode=touch ballast_mib=1200 mmap_us=12.0 fill_ms=11.662 unit=2847.2 unit_label=ns/page unmap_ms=14.397 rss_mib=66.45 faults=4096 majflt=0 pre_total=5735 pre_free=2725 pre_avail=-1 pre_free_spec=2731 pre_file=476 pre_anon=1607 pre_swap_comp=338 pre_comp_occ=118 post_total=5735 post_free=2660 post_avail=-1 post_free_spec=2666 post_file=476 post_anon=1672 post_swap_comp=338 post_comp_occ=118
REC run=2 size_mib=64 mode=touch ballast_mib=1200 mmap_us=11.0 fill_ms=13.069 unit=3190.7 unit_label=ns/page unmap_ms=4.736 rss_mib=66.36 faults=4096 majflt=0 pre_total=5735 pre_free=2776 pre_avail=-1 pre_free_spec=2782 pre_file=477 pre_anon=1608 pre_swap_comp=338 pre_comp_occ=118 post_total=5735 post_free=2711 post_avail=-1 post_free_spec=2718 post_file=477 post_anon=1672 post_swap_comp=338 post_comp_occ=118
SUMMARY runs=2 fill_ms_avg=12.365 fill_ms_min=11.662 fill_ms_max=13.069
PHASE name=ballast_released ts=2026-10-09T17:15:23+0800 post_total=5735 post_free=3975 post_avail=-1 post_free_spec=3981 post_file=477 post_anon=409 post_swap_comp=338 post_comp_occ=118
Linux — Ubuntu · kernel 7.0.0 · aarch64 · touch 64M ×2 · ballast 2048M
case-sample-linux.txt
下载
CASE ts=2026-10-09T17:15:52+0800 platform=Linux kernel=Linux-7.0.0-34-generic arch=aarch64 size_mib=64 mode=touch runs=2 ballast_mib=2048 tool=mem_bench
PHASE name=case_start ts=2026-10-09T17:15:52+0800 pre_total=4895 pre_free=3108 pre_avail=4384 pre_free_spec=-1 pre_file=1316 pre_anon=90 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_start ts=2026-10-09T17:15:52+0800 ballast_mib=2048 hold_s=900 pre_total=4895 pre_free=3108 pre_avail=4384 pre_free_spec=-1 pre_file=1316 pre_anon=90 pre_swap_comp=50 pre_comp_occ=-1
PHASE name=ballast_settled ts=2026-10-09T17:16:12+0800 waited_s=20 pid=163555 pre_total=4895 pre_free=1047 pre_avail=2323 pre_free_spec=-1 pre_file=1316 pre_anon=2137 pre_swap_comp=50 pre_comp_occ=-1
REC run=1 size_mib=64 mode=touch ballast_mib=2048 mmap_us=4.4 fill_ms=62.781 unit=3831.9 unit_label=ns/page unmap_ms=6.593 rss_mib=65.40 faults=16386 majflt=0 pre_total=4895 pre_free=1048 pre_avail=2324 pre_free_spec=-1 pre_file=1316 pre_anon=2137 pre_swap_comp=50 pre_comp_occ=-1 post_total=4895 post_free=990 post_avail=2266 post_free_spec=-1 post_file=1316 post_anon=2202 post_swap_comp=50 post_comp_occ=-1
REC run=2 size_mib=64 mode=touch ballast_mib=2048 mmap_us=30.6 fill_ms=32.351 unit=1974.5 unit_label=ns/page unmap_ms=6.777 rss_mib=65.40 faults=16386 majflt=0 pre_total=4895 pre_free=1024 pre_avail=2299 pre_free_spec=-1 pre_file=1316 pre_anon=2137 pre_swap_comp=50 pre_comp_occ=-1 post_total=4895 post_free=964 post_avail=2240 post_free_spec=-1 post_file=1316 post_anon=2201 post_swap_comp=50 post_comp_occ=-1
SUMMARY runs=2 fill_ms_avg=47.566 fill_ms_min=32.351 fill_ms_max=62.781
PHASE name=ballast_released ts=2026-10-09T17:16:19+0800 post_total=4895 post_free=3057 post_avail=4333 post_free_spec=-1 post_file=1316 post_anon=89 post_swap_comp=50 post_comp_occ=-1
macOS — Darwin 25.4 · arm64 · touch 64M ×2 · ballast 1024M
case-sample-macos.txt
下载
CASE ts=2026-10-09T17:15:24+0800 platform=Darwin kernel=Darwin-25.4.0 arch=arm64 size_mib=64 mode=touch runs=2 ballast_mib=1024 tool=mem_bench
PHASE name=case_start ts=2026-10-09T17:15:24+0800 pre_total=8192 pre_free=102 pre_avail=-1 pre_free_spec=181 pre_file=668 pre_anon=1119 pre_swap_comp=32190 pre_comp_occ=2679
PHASE name=ballast_start ts=2026-10-09T17:15:24+0800 ballast_mib=1024 hold_s=900 pre_total=8192 pre_free=102 pre_avail=-1 pre_free_spec=181 pre_file=668 pre_anon=1119 pre_swap_comp=32190 pre_comp_occ=2679
PHASE name=ballast_settled ts=2026-10-09T17:15:44+0800 waited_s=20 pid=94046 pre_total=8192 pre_free=66 pre_avail=-1 pre_free_spec=81 pre_file=633 pre_anon=1164 pre_swap_comp=33170 pre_comp_occ=2684
REC run=1 size_mib=64 mode=touch ballast_mib=1024 mmap_us=0.0 fill_ms=6.133 unit=1497.3 unit_label=ns/page unmap_ms=1.626 rss_mib=65.30 faults=4096 majflt=0 pre_total=8192 pre_free=63 pre_avail=-1 pre_free_spec=78 pre_file=633 pre_anon=1164 pre_swap_comp=33170 pre_comp_occ=2684 post_total=8192 post_free=80 post_avail=-1 post_free_spec=96 post_file=634 post_anon=1146 post_swap_comp=33261 post_comp_occ=2713
REC run=2 size_mib=64 mode=touch ballast_mib=1024 mmap_us=1.0 fill_ms=5.214 unit=1272.9 unit_label=ns/page unmap_ms=2.985 rss_mib=65.30 faults=4096 majflt=0 pre_total=8192 pre_free=74 pre_avail=-1 pre_free_spec=140 pre_file=674 pre_anon=1103 pre_swap_comp=33235 pre_comp_occ=2715 post_total=8192 post_free=60 post_avail=-1 post_free_spec=119 post_file=667 post_anon=1218 post_swap_comp=33182 post_comp_occ=2622
SUMMARY runs=2 fill_ms_avg=5.673 fill_ms_min=5.214 fill_ms_max=6.133
PHASE name=ballast_released ts=2026-10-09T17:15:51+0800 post_total=8192 post_free=699 post_avail=-1 post_free_spec=710 post_file=621 post_anon=1249 post_swap_comp=32064 post_comp_occ=2007
验证矩阵:iOS (iPhone13,3, dash, 设备端 ldid 签名) ✓ ·
Linux (Ubuntu aarch64, gcc 15.2) ✓ · macOS (bash 3.2 /bin/sh + zsh) ✓ ——
三平台的 ballast 生命周期、双模式解析、平台差异键(-1)均逐项核对通过。
脚本源码(用例全量归档)
用例涉及的 5 个文件全部在此,按用例数据流排列: run_case.sh(采集 harness)调用 mem_bench(测量工具)与 mem_stress(重载 ballast 工具), 由 Makefile 编译后两者,采集文件交给 compare_cases.py(对比工具)。本页自包含,无需跳转。
#!/bin/sh
# run_case.sh — 轻重载"申请耗时"标准用例 harness (iOS / macOS / Linux 通用)
#
# 用法:
# ./run_case.sh -s <N M> -m <touch|memset> -r <runs> [-b <ballast_mib>] [-w settle_s] [-t hold_s]
# 示例:
# ./run_case.sh -s 200M -m touch -r 5 # 轻载: 200M 纯申请 x 5 轮
# ./run_case.sh -s 200M -m memset -r 5 # 轻载: 200M 申请+写入 x 5 轮
# ./run_case.sh -s 200M -m touch -r 5 -b 3600 # 重载: 自动建 3600MiB ballast, 测完自动释放
#
# 输出: 约束格式记录行 CASE/PHASE/REC/SUMMARY (key=value 令牌, 见 protocol.html)
# REC = 耗时 (mmap_us/fill_ms/unmap_ms) + 缺页 (faults/majflt) + rss
# + 申请前 (pre_*) 与申请驻留时 (post_*) 的内存统计
# post_*: 每轮 fill 后 mem_bench 保持驻留 HOLD_FILL 秒, 驻留期采样
#
# 依赖: 同目录 mem_bench / mem_stress; 内存源 (Darwin: vm_stat+sysctl / Linux: /proc/meminfo)
# 约束: 纯 POSIX sh (dash/ash/bash/zsh), 不依赖 awk/bc/python — iOS 精简用户态可跑
SIZE=200M; MODE=touch; RUNS=5; BALLAST=0; SETTLE=20; HOLD=900
HOLD_FILL=3 # 每轮 fill 后驻留保持秒数 (post_* 采样窗口)
DIR=$(dirname "$0")
BENCH="$DIR/mem_bench"; STRESS="$DIR/mem_stress"
BPID=""; BCPID=""
TMPB=/tmp/.run_case.bench.$$
usage() { sed -n '3,15p' "$0"; exit 2; }
while getopts "s:m:r:b:w:t:h" opt 2>/dev/null; do
case $opt in
s) SIZE=$OPTARG ;; m) MODE=$OPTARG ;; r) RUNS=$OPTARG ;;
b) BALLAST=$OPTARG ;; w) SETTLE=$OPTARG ;; t) HOLD=$OPTARG ;;
h) usage ;; *) usage ;;
esac
done
# ---- 参数校验 (size 仅支持整 MiB) ----
case $SIZE in ''|*[!0-9M]*) echo "ERROR code=bad_size size=$SIZE (仅整 MiB, 如 200M)"; exit 2 ;; esac
SIZE_MIB=${SIZE%M}
case $SIZE_MIB in ''|*[!0-9]*) echo "ERROR code=bad_size size=$SIZE"; exit 2 ;; esac
case $MODE in touch|memset) ;; *) echo "ERROR code=bad_mode mode=$MODE (touch|memset)"; exit 2 ;; esac
case $RUNS in ''|*[!0-9]*|0) echo "ERROR code=bad_runs runs=$RUNS"; exit 2 ;; esac
case $BALLAST in ''|*[!0-9]*) echo "ERROR code=bad_ballast ballast=$BALLAST"; exit 2 ;; esac
[ -x "$BENCH" ] || { echo "ERROR code=no_bench path=$BENCH"; exit 2; }
if [ "$BALLAST" -gt 0 ]; then [ -x "$STRESS" ] || { echo "ERROR code=no_stress path=$STRESS"; exit 2; }; fi
ts() { date +%Y-%m-%dT%H:%M:%S%z; }
# ---- 平台检测 ----
KERNEL=$(uname -s)
case $KERNEL in Darwin|Linux) ;; *) echo "ERROR code=unsupported_kernel kernel=$KERNEL"; exit 2 ;; esac
ARCH=$(uname -m)
PAGE_BYTES=0; TOTAL_MIB=-1
if [ "$KERNEL" = "Darwin" ]; then
# vm_stat 首行固定为 "Mach Virtual Memory Statistics: (page size of NNNN bytes)"
_hdr=$(vm_stat 2>/dev/null | { IFS= read -r _l; echo "$_l"; })
PAGE_BYTES=0
for _t in $_hdr; do
case $_t in
[0-9]*) PAGE_BYTES=$_t ;;
esac
done
[ "$PAGE_BYTES" -gt 0 ] 2>/dev/null || PAGE_BYTES=16384
_ms=$(sysctl -n hw.memsize 2>/dev/null) && TOTAL_MIB=$((_ms / 1048576))
fi
kib_pages_to_mib() { echo $(( $1 * PAGE_BYTES / 1048576 )); }
# ---- 统一内存快照 -> R_* (整 MiB, -1 = 平台无此指标) ----
snap_mem() {
R_TOTAL=$TOTAL_MIB; R_AVAIL=-1; R_FREE_SPEC=-1; R_COMP_OCC=-1
if [ "$KERNEL" = "Linux" ]; then
R_FREE=-1; R_FILE=-1; R_ANON=-1; R_SWAP_COMP=-1
[ -r /proc/meminfo ] || return 0
_t=0 _f=0 _a=0 _c=0 _an=0 _st=0 _sf=0
while IFS=' :' read -r k v rest; do
case $k in
MemTotal) _t=$v ;; MemFree) _f=$v ;; MemAvailable) _a=$v ;;
Cached) _c=$v ;; AnonPages) _an=$v ;;
SwapTotal) _st=$v ;; SwapFree) _sf=$v ;;
esac
done < /proc/meminfo
R_TOTAL=$((_t / 1024)); R_FREE=$((_f / 1024)); R_AVAIL=$((_a / 1024))
R_FILE=$((_c / 1024)); R_ANON=$((_an / 1024))
R_SWAP_COMP=$(( (_st - _sf) / 1024 ))
else
vm_stat > /tmp/.run_case.$$ 2>/dev/null || { R_FREE=-1; R_FILE=-1; R_ANON=-1; R_SWAP_COMP=-1; return 0; }
_pf=0 _psp=0 _pfb=0 _pan=0 _psc=0 _poc=0
while IFS= read -r line; do
case $line in
"Pages free:"*) set -- $line; _pf=${3%.} ;;
"Pages speculative:"*) set -- $line; _psp=${3%.} ;;
"File-backed pages:"*) set -- $line; _pfb=${3%.} ;;
"Anonymous pages:"*) set -- $line; _pan=${3%.} ;;
"Pages stored in compressor:"*) set -- $line; _psc=${5%.} ;;
"Pages occupied by compressor:"*) set -- $line; _poc=${5%.} ;;
esac
done < /tmp/.run_case.$$
rm -f /tmp/.run_case.$$
R_FREE=$(kib_pages_to_mib $_pf)
R_FREE_SPEC=$(kib_pages_to_mib $((_pf + _psp)))
R_FILE=$(kib_pages_to_mib $_pfb)
R_ANON=$(kib_pages_to_mib $_pan)
R_SWAP_COMP=$(kib_pages_to_mib $_psc)
R_COMP_OCC=$(kib_pages_to_mib $_poc)
fi
}
mem_tokens() { # $1=前缀(pre/post)
printf '%s_total=%s %s_free=%s %s_avail=%s %s_free_spec=%s %s_file=%s %s_anon=%s %s_swap_comp=%s %s_comp_occ=%s' \
"$1" "$R_TOTAL" "$1" "$R_FREE" "$1" "$R_AVAIL" "$1" "$R_FREE_SPEC" \
"$1" "$R_FILE" "$1" "$R_ANON" "$1" "$R_SWAP_COMP" "$1" "$R_COMP_OCC"
}
cleanup() {
rc=$?
trap - EXIT INT TERM
if [ -n "$BCPID" ]; then
kill "$BCPID" 2>/dev/null; wait "$BCPID" 2>/dev/null; BCPID=""
fi
if [ -n "$BPID" ]; then
kill "$BPID" 2>/dev/null; wait "$BPID" 2>/dev/null; BPID=""
snap_mem
printf 'PHASE name=ballast_released ts=%s ' "$(ts)"; mem_tokens post; echo
fi
rm -f /tmp/.run_case.$$ "$TMPB"
exit "$rc"
}
trap cleanup EXIT INT TERM
# 浮点(<=3位小数) -> 毫单位整数: "35.096"->35096, "12.0"->12000
to_milli() {
_i=${1%%.*}; _f=${1#*.}
[ "$_f" = "$1" ] && _f=""
case $_f in
"") _f=000 ;; ?) _f=${_f}00 ;; ??) _f=${_f}0 ;; ???) ;; *) _f=${_f%${_f#???}} ;;
esac
printf '%s%s' "$_i" "$_f"
}
fmt3() { printf '%d.%03d' $(($1 / 1000)) $(($1 % 1000)); }
# ---- 开始 ----
printf 'CASE ts=%s platform=%s kernel=%s arch=%s size_mib=%s mode=%s runs=%s ballast_mib=%s tool=mem_bench\n' \
"$(ts)" "$KERNEL" "$KERNEL-$(uname -r)" "$ARCH" "$SIZE_MIB" "$MODE" "$RUNS" "$BALLAST"
snap_mem
printf 'PHASE name=case_start ts=%s ' "$(ts)"; mem_tokens pre; echo
# ---- 重载: 建 ballast (-P 50 半随机 ≈ 真实数据压缩比, 防 watchdog panic) ----
if [ "$BALLAST" -gt 0 ]; then
printf 'PHASE name=ballast_start ts=%s ballast_mib=%s hold_s=%s ' "$(ts)" "$BALLAST" "$HOLD"; mem_tokens pre; echo
nohup "$STRESS" "${BALLAST}M" -P 50 -q -t "$HOLD" >/dev/null 2>&1 &
BPID=$!
_waited=0
while [ "$_waited" -lt "$SETTLE" ]; do
sleep 2; _waited=$((_waited + 2))
snap_mem
if [ "$R_FREE" -lt 16 ]; then
echo "PHASE name=ballast_abort ts=$(ts) reason=free_below_16MiB free_mib=$R_FREE"; exit 3
fi
kill -0 "$BPID" 2>/dev/null || { echo "PHASE name=ballast_abort ts=$(ts) reason=ballast_died"; exit 3; }
done
printf 'PHASE name=ballast_settled ts=%s waited_s=%s pid=%s ' "$(ts)" "$_waited" "$BPID"; mem_tokens pre; echo
fi
# ---- 测量循环: 每轮 pre 快照 -> 单轮 bench(fill 后驻留 HOLD_FILL 秒) -> 驻留期 post 快照 -> 解析配对 ----
_sum=0; _min=999999999; _max=0; _n=0
i=1
while [ "$i" -le "$RUNS" ]; do
snap_mem
pre_tok=$(mem_tokens pre)
# bench 后台跑: fill(数十~数百 ms) 后保持驻留 3s; shell 在 ~1s 处采 post 快照
"$BENCH" "$SIZE" -r 1 -m "$MODE" -H "$HOLD_FILL" >"$TMPB" 2>&1 &
BCPID=$!
sleep 1
snap_mem
post_tok=$(mem_tokens post)
wait "$BCPID"; brc=$?
BCPID=""
if [ "$brc" -ne 0 ]; then
echo "ERROR code=bench_failed run=$i rc=$brc"
cat "$TMPB"
exit 4
fi
out=$(cat "$TMPB")
mmap_v=""; fill_v=""; unit_v=""; unit_l=""; unmap_v=""; rss_v=""; faults_v=""; majflt_v=""; found=0
while IFS= read -r line; do
case $line in
"run 1/1:"*)
found=1; _st=""
set -- $line
for t in "$@"; do
case $_st in
mmap) mmap_v=$t ;;
fill) fill_v=$t ;;
unit) unit_v=$t; _st=unit_l; continue ;;
unit_l) unit_l=${t%)}; _st="" ;;
unmap) unmap_v=$t ;;
rss) rss_v=$t ;;
esac
case $t in
mmap=) _st=mmap ;;
')=') _st=fill ;;
'fill(memset)=') _st=fill ;;
'(') _st=unit ;;
unmap=) _st=unmap ;;
rss=) _st=rss ;;
faults=*) faults_v=${t#faults=} ;;
majflt=*) majflt_v=${t#majflt=} ;;
*) case $_st in mmap|fill|unmap|rss) _st="" ;; esac ;;
esac
done
;;
esac
done <<EOF
$out
EOF
[ "$found" = 1 ] || { echo "ERROR code=parse_failed run=$i"; printf '%s\n' "$out"; exit 4; }
[ -n "$faults_v" ] || faults_v=-1
[ -n "$majflt_v" ] || majflt_v=-1
printf 'REC run=%s size_mib=%s mode=%s ballast_mib=%s mmap_us=%s fill_ms=%s unit=%s unit_label=%s unmap_ms=%s rss_mib=%s faults=%s majflt=%s %s %s\n' \
"$i" "$SIZE_MIB" "$MODE" "$BALLAST" "$mmap_v" "$fill_v" "$unit_v" "$unit_l" "$unmap_v" "$rss_v" "$faults_v" "$majflt_v" "$pre_tok" "$post_tok"
_m=$(to_milli "$fill_v")
_sum=$((_sum + _m)); _n=$((_n + 1))
[ "$_m" -lt "$_min" ] && _min=$_m
[ "$_m" -gt "$_max" ] && _max=$_m
i=$((i + 1))
[ "$i" -le "$RUNS" ] && sleep 1
done
if [ "$_n" -gt 0 ]; then
printf 'SUMMARY runs=%s fill_ms_avg=%s fill_ms_min=%s fill_ms_max=%s\n' \
"$_n" "$(fmt3 $((_sum / _n)))" "$(fmt3 $_min)" "$(fmt3 $_max)"
fi
/*
* mem_bench.c — 匿名页申请耗时基准 (iOS / HarmonyOS / macOS / Linux 通用)
*
* 功能:
* 按入参大小反复 mmap 一块匿名私有内存, 分别统计:
* - mmap 耗时 (建立映射本身, 通常只有 µs 级)
* - fill 耗时 (memset 整块填充 —— 真正触发缺页 + 写入的主成本;
* 或 touch 模式: 每页写 1 字节, 专门测首次触页缺页成本)
* - unmap 耗时 (munmap 释放)
* - faults 缺页次数 (本轮申请窗口内的总缺页 + 需 I/O 的主缺页;
* Darwin 用 task_events_info, 其余 getrusage)
* 每轮结束后做轻量读回校验 (防止填充被优化掉, 并确认内存真实可读),
* 最后输出 avg/min/max 汇总与吞吐率。
* -H <sec>: fill 之后、unmap 之前保持驻留 N 秒 —— 供外部 (run_case.sh)
* 在驻留期间采集"申请后"的内存快照 (post_*); 保持期不计入任何耗时。
*
* 用法:
* mem_bench [-r n] [-m memset|touch|none] [-H sec] [-q] <size>[K|M|G|T]
*
* 构建:
* host : cc -O2 -o mem_bench mem_bench.c
* iOS : xcrun -sdk iphoneos clang -arch arm64 -O2 \
* -miphoneos-version-min=12.0 -o mem_bench mem_bench.c
* HarmonyOS : <OHOS_NDK>/llvm/bin/clang --target=aarch64-linux-ohos \
* --sysroot=<OHOS_NDK>/sysroot -O2 -o mem_bench mem_bench.c
*
* 说明:
* - 纯 POSIX C, 无第三方依赖; 仅 RSS 读取按平台分支。
* - 单位为二进制: K=1024, M=1024^2, G=1024^3, T=1024^4。
* - 每轮 memset 使用不同填充值 (0x11/0x22/...), 规避任何零页/去重优化。
* - iOS 为 16K 页, Linux/鸿蒙通常为 4K 页; touch 模式的 ns/page 指标
* 可直接对比两平台的单缺页成本。
*/
#if !defined(__APPLE__)
#define _POSIX_C_SOURCE 200809L
#endif
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <mach/mach.h>
#endif
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
enum fill_mode { FILL_MEMSET, FILL_TOUCH, FILL_NONE };
static volatile sig_atomic_t g_stop = 0;
static volatile uint64_t g_sink = 0; /* 读回校验累加器, volatile 防优化 */
static size_t g_page = 4096;
/* ---------------- 公共小工具 ---------------- */
static void on_signal(int sig)
{
(void)sig;
g_stop = 1;
}
static double now_sec(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static double to_mib(unsigned long long bytes)
{
return (double)bytes / 1048576.0;
}
/* 解析大小参数 (与 mem_stress 相同: 二进制单位, 兼容 B/iB 后缀) */
static int parse_size(const char *s, unsigned long long *out)
{
if (s == NULL || *s == '\0' || *s == '-' || *s == '+')
return -1;
errno = 0;
char *end = NULL;
unsigned long long v = strtoull(s, &end, 10);
if (errno != 0 || end == s)
return -1;
const char *u = end;
unsigned long long mult = 1ULL;
if (*u == 'K' || *u == 'k' || *u == 'M' || *u == 'm' ||
*u == 'G' || *u == 'g' || *u == 'T' || *u == 't') {
switch (*u | 0x20) {
case 'k': mult = 1ULL << 10; break;
case 'm': mult = 1ULL << 20; break;
case 'g': mult = 1ULL << 30; break;
case 't': mult = 1ULL << 40; break;
}
u++;
if (*u == 'i')
u++;
if (*u == 'B' || *u == 'b')
u++;
} else if (*u == 'B' || *u == 'b') {
u++;
}
if (*u != '\0')
return -1;
if (v == 0)
return -1;
if (v > ULLONG_MAX / mult)
return -1;
*out = v * mult;
return 0;
}
/* 当前驻留集 (字节); 取不到返回 0 (调用方按 0 跳过显示) */
static unsigned long long get_rss_bytes(void)
{
#if defined(__APPLE__)
struct task_vm_info vm;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vm, &count) != KERN_SUCCESS)
return 0;
return (unsigned long long)vm.resident_size;
#else
FILE *f = fopen("/proc/self/statm", "r");
if (f == NULL)
return 0;
unsigned long long total = 0, resident = 0;
int n = fscanf(f, "%llu %llu", &total, &resident);
fclose(f);
if (n != 2)
return 0;
return resident * (unsigned long long)g_page;
#endif
}
/* 峰值驻留集 (字节); Darwin 的 ru_maxrss 单位是字节, Linux 系是 KiB */
static unsigned long long get_peak_rss_bytes(void)
{
struct rusage ru;
if (getrusage(RUSAGE_SELF, &ru) != 0)
return 0;
#if defined(__APPLE__)
return (unsigned long long)ru.ru_maxrss;
#else
return (unsigned long long)ru.ru_maxrss * 1024ULL;
#endif
}
/*
* 当前累计缺页计数 (本进程)。
* Darwin: task_events_info 的 faults=总缺页, pageins=需 I/O 的主缺页 (最可靠);
* 其他平台: getrusage 的 ru_minflt/ru_majflt。
* 取不到时 *ok=0 (调用方以 -1 表示不可用)。
*/
static void get_faults_now(unsigned long long *flt, unsigned long long *majflt,
int *ok)
{
#if defined(__APPLE__)
struct task_events_info te;
mach_msg_type_number_t cnt = TASK_EVENTS_INFO_COUNT;
if (task_info(mach_task_self(), TASK_EVENTS_INFO,
(task_info_t)&te, &cnt) == KERN_SUCCESS) {
*flt = (unsigned long long)te.faults;
*majflt = (unsigned long long)te.pageins;
*ok = 1;
return;
}
#endif
struct rusage ru;
if (getrusage(RUSAGE_SELF, &ru) == 0) {
*flt = (unsigned long long)ru.ru_minflt + (unsigned long long)ru.ru_majflt;
*majflt = (unsigned long long)ru.ru_majflt;
*ok = 1;
return;
}
*ok = 0;
}
/*
* 严格的整数解析 (拒绝垃圾输入)。注: 不用 getopt —— BSD/musl 的 getopt
* 不重排 argv, "size 在前、选项在后"的写法会解析失败; 手写解析保证
* 各平台行为一致。
*/
static int parse_int(const char *s, long *out)
{
if (s == NULL || *s == '\0')
return -1;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
if (errno != 0 || end == s || *end != '\0')
return -1;
*out = v;
return 0;
}
static const char *fill_mode_name(enum fill_mode m)
{
switch (m) {
case FILL_MEMSET: return "memset";
case FILL_TOUCH: return "touch ";
case FILL_NONE: return "none ";
}
return "?";
}
static void usage(FILE *out, const char *prog)
{
fprintf(out,
"mem_bench - anonymous mmap/memset timing benchmark (iOS/HarmonyOS/macOS/Linux)\n"
"\n"
"usage: %s [-r n] [-m mode] [-H sec] [-q] <size>\n"
"\n"
" <size> bytes of anonymous memory per run\n"
" e.g. 268435456 | 100K | 512M | 2G (binary units, B/iB suffix ok)\n"
" -r n repeat runs (default 3)\n"
" -m mode fill mode after mmap (default memset):\n"
" memset memset the whole region (faults + bandwidth)\n"
" touch write 1 byte per page (first-touch fault cost, ns/page)\n"
" none mmap only (mapping setup/teardown cost)\n"
" -H sec hold mapping resident N sec between fill and unmap\n"
" (for external post-allocation snapshots; not timed)\n"
" -q quiet: per-run lines suppressed\n"
"\n"
"per-run line also reports faults=<total> majflt=<io> (page faults in\n"
"the allocation window; -1 = unavailable on this platform).\n"
"\n"
"examples:\n"
" %s 512M -r 5 time mmap + memset of 512M, 5 runs\n"
" %s 512M -m touch time first-touch page faults (ns/page)\n"
" %s 512M -H 3 hold 3s after fill for external snapshot\n",
prog, prog, prog, prog);
}
/* 汇总打印一行: name avg/min/max (value 数组单位为秒, 乘 scale 换显示单位) */
static void print_stats(const char *name, const double *v, int n,
double scale, const char *unit)
{
double sum = 0.0, mn = v[0], mx = v[0];
for (int i = 0; i < n; i++) {
sum += v[i];
if (v[i] < mn) mn = v[i];
if (v[i] > mx) mx = v[i];
}
printf("%-7s: avg %10.3f %s | min %10.3f %s | max %10.3f %s\n",
name, sum / (double)n * scale, unit,
mn * scale, unit, mx * scale, unit);
}
int main(int argc, char **argv)
{
int repeats = 3;
int quiet = 0;
long hold_s = 0;
enum fill_mode mode = FILL_MEMSET;
const char *size_arg = NULL;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-q") == 0) {
quiet = 1;
} else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage(stdout, argv[0]);
return 0;
} else if (strcmp(a, "-H") == 0) {
long v = 0;
if (i + 1 >= argc || parse_int(argv[i + 1], &v) != 0 ||
v < 0 || v > 3600) {
fprintf(stderr, "error: -H expects seconds in 0..3600\n");
return 2;
}
i++;
hold_s = v;
} else if (strcmp(a, "-r") == 0) {
long v = 0;
if (i + 1 >= argc || parse_int(argv[i + 1], &v) != 0 ||
v < 1 || v > 10000) {
fprintf(stderr, "error: -r expects an integer in 1..10000\n");
return 2;
}
i++;
repeats = (int)v;
} else if (strcmp(a, "-m") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "error: -m expects a mode (memset|touch|none)\n");
return 2;
}
i++;
if (strcmp(argv[i], "memset") == 0) mode = FILL_MEMSET;
else if (strcmp(argv[i], "touch") == 0) mode = FILL_TOUCH;
else if (strcmp(argv[i], "none") == 0) mode = FILL_NONE;
else {
fprintf(stderr, "error: unknown mode '%s' (memset|touch|none)\n",
argv[i]);
return 2;
}
} else if (a[0] == '-' && a[1] != '\0') {
fprintf(stderr, "error: unknown option '%s'\n", a);
usage(stderr, argv[0]);
return 2;
} else {
if (size_arg != NULL) {
fprintf(stderr, "error: multiple sizes given ('%s' and '%s')\n",
size_arg, a);
return 2;
}
size_arg = a;
}
}
if (size_arg == NULL) {
usage(stderr, argv[0]);
return 2;
}
unsigned long long req = 0;
if (parse_size(size_arg, &req) != 0) {
fprintf(stderr, "error: bad size '%s' (try 512M / 2G / 65536K)\n",
size_arg);
return 2;
}
long ps = sysconf(_SC_PAGESIZE);
if (ps > 0)
g_page = (size_t)ps;
if (req > (unsigned long long)(SIZE_MAX - (g_page - 1))) {
fprintf(stderr, "error: size %llu bytes exceeds this platform's address space\n",
req);
return 2;
}
size_t size = ((size_t)req + g_page - 1) & ~((size_t)g_page - 1);
unsigned long long pages = (unsigned long long)(size / g_page);
setvbuf(stdout, NULL, _IOLBF, 0);
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
double *t_mmap = malloc((size_t)repeats * sizeof(double));
double *t_fill = malloc((size_t)repeats * sizeof(double));
double *t_unmap = malloc((size_t)repeats * sizeof(double));
if (t_mmap == NULL || t_fill == NULL || t_unmap == NULL) {
fprintf(stderr, "error: out of memory\n");
return 1;
}
printf("target : %llu bytes (%.2f MiB), %llu page(s) x %zu B, "
"mode=%s, runs=%d\n",
(unsigned long long)size, to_mib(size), pages, g_page,
fill_mode_name(mode), repeats);
int completed = 0;
for (int i = 0; i < repeats && !g_stop; i++) {
/* --- 申请窗口缺页计数: 起点采样 --- */
unsigned long long f0 = 0, f0m = 0;
int fok0 = 0;
get_faults_now(&f0, &f0m, &fok0);
/* --- mmap --- */
double t0 = now_sec();
char *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
double t1 = now_sec();
if (p == MAP_FAILED) {
fprintf(stderr, "error: mmap %llu bytes failed on run %d: %s\n",
(unsigned long long)size, i + 1, strerror(errno));
#if defined(__APPLE__)
fprintf(stderr, "hint: iOS caps a single process's anonymous "
"reservation (~4GiB observed on 14.8); "
"try a smaller size.\n");
#endif
break;
}
t_mmap[i] = t1 - t0;
/* --- fill --- */
unsigned char pat = (unsigned char)(0x11 * (unsigned)(i + 1)); /* 0x11,0x22,... */
t0 = now_sec();
if (mode == FILL_MEMSET) {
memset(p, pat, size);
} else if (mode == FILL_TOUCH) {
for (size_t off = 0; off < size && !g_stop; off += g_page)
p[off] = (char)pat;
}
t1 = now_sec();
t_fill[i] = t1 - t0;
/* 轻量读回校验: 每页读首字节累加, 防止填充/映射被优化掉 */
if (mode != FILL_NONE) {
uint64_t sum = 0;
for (size_t off = 0; off < size; off += g_page)
sum += (uint64_t)(unsigned char)p[off];
g_sink = sum;
}
unsigned long long rss = get_rss_bytes();
/* --- 申请窗口缺页计数: 终点采样 (fill+读回全部计入) --- */
unsigned long long f1 = 0, f1m = 0;
int fok1 = 0;
get_faults_now(&f1, &f1m, &fok1);
long long flt = -1, flt_maj = -1;
if (fok0 && fok1) {
flt = (long long)(f1 - f0);
flt_maj = (long long)(f1m - f0m);
}
/* --- 驻留保持 (供外部采"申请后"快照; 不计入任何耗时) --- */
if (hold_s > 0 && !g_stop) {
struct timespec hts = { hold_s, 0 };
nanosleep(&hts, NULL);
}
/* --- unmap --- */
t0 = now_sec();
munmap(p, size);
t_unmap[i] = now_sec() - t0;
completed = i + 1;
if (!quiet) {
printf("run %d/%d: mmap=%9.1f us | fill(%s)=%10.3f ms",
i + 1, repeats, t_mmap[i] * 1e6,
mode == FILL_MEMSET ? "memset" :
mode == FILL_TOUCH ? "touch " : "none ",
t_fill[i] * 1e3);
if (mode == FILL_MEMSET && t_fill[i] > 0.0)
printf(" (%8.1f MiB/s)", to_mib(size) / t_fill[i]);
if (mode == FILL_TOUCH && t_fill[i] > 0.0 && pages > 0)
printf(" (%8.1f ns/page)",
t_fill[i] * 1e9 / (double)pages);
printf(" | unmap=%8.3f ms", t_unmap[i] * 1e3);
if (rss)
printf(" | rss=%8.2f MiB", to_mib(rss));
printf(" | faults=%lld majflt=%lld", flt, flt_maj);
if (hold_s > 0)
printf(" | held=%lds", hold_s);
printf("\n");
}
if (i + 1 < repeats) {
struct timespec ts = { 0, 100 * 1000 * 1000 }; /* 100ms 间隔 */
nanosleep(&ts, NULL);
}
}
if (completed == 0) {
fprintf(stderr, "error: no run completed\n");
free(t_mmap); free(t_fill); free(t_unmap);
return 1;
}
/* ---- 汇总 ---- */
printf("---- summary (%d run%s, %.2f MiB, %s mode) ----\n",
completed, completed == 1 ? "" : "s", to_mib(size),
mode == FILL_MEMSET ? "memset" :
mode == FILL_TOUCH ? "touch" : "none");
print_stats("mmap", t_mmap, completed, 1e6, "us");
if (mode != FILL_NONE)
print_stats("fill", t_fill, completed, 1e3, "ms");
print_stats("unmap", t_unmap, completed, 1e3, "ms");
if (mode == FILL_MEMSET) {
double tp_sum = 0.0;
int tp_n = 0;
for (int i = 0; i < completed; i++) {
if (t_fill[i] > 0.0) {
tp_sum += to_mib(size) / t_fill[i];
tp_n++;
}
}
if (tp_n > 0)
printf("memset throughput: avg %.1f MiB/s over %d run(s)\n",
tp_sum / (double)tp_n, tp_n);
}
if (mode == FILL_TOUCH && pages > 0) {
double fp_sum = 0.0;
int fp_n = 0;
for (int i = 0; i < completed; i++) {
if (t_fill[i] > 0.0) {
fp_sum += t_fill[i] * 1e9 / (double)pages;
fp_n++;
}
}
if (fp_n > 0)
printf("first-touch fault: avg %.1f ns/page over %d run(s)\n",
fp_sum / (double)fp_n, fp_n);
}
printf("peak rss: %.2f MiB\n", to_mib(get_peak_rss_bytes()));
(void)g_sink;
free(t_mmap); free(t_fill); free(t_unmap);
return 0;
}
/*
* mem_stress.c — 匿名页内存加压工具 (iOS / HarmonyOS / macOS / Linux 通用)
*
* 功能:
* 按入参大小 mmap 一块匿名私有内存 (MAP_PRIVATE|MAP_ANONYMOUS), 并逐页写入
* 触页 (touch) 提交物理页, 形成真实内存压力; 之后按参数保持一段时间
* (或直到收到 SIGINT/SIGTERM) 再释放。
*
* 可选 -k 周期性重触: 每个周期向所有页写入新值, 迫使被 XNU compressor /
* Linux zram 压缩回收的页重新解压驻留, 并把干净页重新弄脏, 维持压力。
*
* 用法:
* mem_stress [-f|-R] [-k sec] [-t sec] [-qv] <size>[K|M|G|T]
* -R = 随机字节填充 (不可压缩, 模拟真实负载; 默认触页/0xA5 模式会被
* compressor 几乎零成本吸收, 无法形成持续压力)
*
* 构建:
* host : cc -O2 -o mem_stress mem_stress.c
* iOS : xcrun -sdk iphoneos clang -arch arm64 -O2 \
* -miphoneos-version-min=12.0 -o mem_stress mem_stress.c
* HarmonyOS : <OHOS_NDK>/llvm/bin/clang --target=aarch64-linux-ohos \
* --sysroot=<OHOS_NDK>/sysroot -O2 -o mem_stress mem_stress.c
*
* 说明:
* - 纯 POSIX C, 无第三方依赖; 仅 RSS/footprint 读取按平台分支。
* - 单位为二进制: K=1024, M=1024^2, G=1024^3, T=1024^4。
* - iOS 上单进程匿名保留有 ~4GiB 上限 (14.8 实测), 超限 mmap 直接失败;
* 更常见的是压力过大先被 jetsam 以 SIGKILL 终止 —— 属预期行为。
*/
#if !defined(__APPLE__)
#define _POSIX_C_SOURCE 200809L
#endif
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <time.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <mach/mach.h>
#endif
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
static volatile sig_atomic_t g_stop = 0;
static size_t g_page = 4096;
/* ---------------- 公共小工具 ---------------- */
static void on_signal(int sig)
{
(void)sig;
g_stop = 1;
}
static double now_sec(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static double to_mib(unsigned long long bytes)
{
return (double)bytes / 1048576.0;
}
/*
* 解析大小参数: "1024" "100K" "512M" "2G" "1T", 兼容 B/iB 后缀与大小写。
* 二进制单位 (K=1024)。成功返回 0, 失败返回 -1。
*/
static int parse_size(const char *s, unsigned long long *out)
{
if (s == NULL || *s == '\0' || *s == '-' || *s == '+')
return -1;
errno = 0;
char *end = NULL;
unsigned long long v = strtoull(s, &end, 10);
if (errno != 0 || end == s)
return -1;
const char *u = end;
unsigned long long mult = 1ULL;
if (*u == 'K' || *u == 'k' || *u == 'M' || *u == 'm' ||
*u == 'G' || *u == 'g' || *u == 'T' || *u == 't') {
switch (*u | 0x20) {
case 'k': mult = 1ULL << 10; break;
case 'm': mult = 1ULL << 20; break;
case 'g': mult = 1ULL << 30; break;
case 't': mult = 1ULL << 40; break;
}
u++;
if (*u == 'i') /* KiB / MiB / GiB ... */
u++;
if (*u == 'B' || *u == 'b')
u++;
} else if (*u == 'B' || *u == 'b') {
u++; /* "100B" = 100 字节 */
}
if (*u != '\0')
return -1;
if (v == 0)
return -1;
if (v > ULLONG_MAX / mult)
return -1;
*out = v * mult;
return 0;
}
/* 当前驻留集 (字节); 取不到返回 0 (调用方按 0 跳过显示) */
static unsigned long long get_rss_bytes(void)
{
#if defined(__APPLE__)
struct task_vm_info vm;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vm, &count) != KERN_SUCCESS)
return 0;
return (unsigned long long)vm.resident_size;
#else
/* Linux / OpenHarmony(musl): /proc/self/statm 第二列 = 驻留页数 */
FILE *f = fopen("/proc/self/statm", "r");
if (f == NULL)
return 0;
unsigned long long total = 0, resident = 0;
int n = fscanf(f, "%llu %llu", &total, &resident);
fclose(f);
if (n != 2)
return 0;
return resident * (unsigned long long)g_page;
#endif
}
#if defined(__APPLE__)
/* iOS jetsam 的判定口径: phys_footprint = 驻留脏页 + 已压缩脏页 + IOKit 映射 */
static unsigned long long get_footprint_bytes(void)
{
struct task_vm_info vm;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vm, &count) != KERN_SUCCESS)
return 0;
return (unsigned long long)vm.phys_footprint;
}
#endif
/* 峰值驻留集 (字节); Darwin 的 ru_maxrss 单位是字节, Linux 系是 KiB */
static unsigned long long get_peak_rss_bytes(void)
{
struct rusage ru;
if (getrusage(RUSAGE_SELF, &ru) != 0)
return 0;
#if defined(__APPLE__)
return (unsigned long long)ru.ru_maxrss;
#else
return (unsigned long long)ru.ru_maxrss * 1024ULL;
#endif
}
/* xorshift64* 伪随机数: 填充不可压缩 ballast (随机数据 WKdm 压不动,
* 与 0xA5 模式填充不同, 无法被 compressor 几乎零成本地吸收) */
static uint64_t g_prng = 0;
static void prng_seed(uint64_t s)
{
g_prng = s ? s : 0x9E3779B97F4A7C15ULL;
}
static uint64_t prng_next(void)
{
uint64_t x = g_prng;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
g_prng = x;
return x * 0x2545F4914F6CDD1DULL;
}
/* 整块填充随机字节, 密度 pct (1..100): 每页前 pct% 为随机字节, 其余清零。
* pct=100 即全随机 (WKdm ~1:1, compressor 无净收益); pct=50 约 2:1 ——
* 接近真实 App 数据的压缩比, 可形成"压缩器仍有回收余量"的可持续重载。 */
static void fill_random(char *buf, size_t size, unsigned cycle, int pct)
{
prng_seed(0x9E3779B97F4A7C15ULL * (uint64_t)(cycle + 1) ^
(uint64_t)time(NULL) ^ (uint64_t)(uintptr_t)buf);
if (pct >= 100) {
size_t n64 = size / 8;
uint64_t *q = (uint64_t *)(void *)buf; /* mmap 区页对齐, 安全 */
for (size_t i = 0; i < n64; i++)
q[i] = prng_next();
for (size_t i = n64 * 8; i < size; i++)
buf[i] = (char)prng_next();
return;
}
/* 部分密度: 每页 [0, rand_bytes) 随机, 其余 0 */
size_t rand_bytes = (size_t)((unsigned long long)g_page * (unsigned)pct / 100);
if (rand_bytes == 0)
rand_bytes = 1;
memset(buf, 0, size);
for (size_t off = 0; off < size; off += g_page) {
size_t remain = rand_bytes;
if (off + remain > size)
remain = size - off;
size_t n64 = remain / 8;
uint64_t *q = (uint64_t *)(void *)(buf + off);
for (size_t i = 0; i < n64; i++)
q[i] = prng_next();
for (size_t i = n64 * 8; i < remain; i++)
buf[off + i] = (char)prng_next();
}
}
/*
* 解析秒数参数 (strtod 严格校验, 拒绝垃圾输入)。成功返回 0。
* 注: 不用 getopt —— BSD/musl 的 getopt 不重排 argv, "size 在前、选项在后"
* 的写法会解析失败; 手写解析保证各平台行为一致。
*/
static int parse_seconds(const char *s, double *out)
{
if (s == NULL || *s == '\0')
return -1;
errno = 0;
char *end = NULL;
double v = strtod(s, &end);
if (errno != 0 || end == s || *end != '\0' || v < 0.0)
return -1;
*out = v;
return 0;
}
static void usage(FILE *out, const char *prog)
{
fprintf(out,
"mem_stress - anonymous memory pressure tool (iOS/HarmonyOS/macOS/Linux)\n"
"\n"
"usage: %s [-f] [-k sec] [-t sec] [-qv] <size>\n"
"\n"
" <size> bytes of anonymous memory to commit\n"
" e.g. 268435456 | 100K | 512M | 1500M | 2G | 1T\n"
" (binary units: K=1024, M=1024^2, G=1024^3; B/iB suffix ok)\n"
" -f full fill: memset the whole region (default: 1 byte per page)\n"
" -R fill with pseudo-random bytes: incompressible ballast that\n"
" the compressor cannot absorb (realistic heavy load)\n"
" -P n partial random fill: first n%% of each page random, rest zero\n"
" (n=50 approximates real app data, ~2:1 WKdm ratio)\n"
" -k sec re-touch all pages every <sec> while holding, to defeat\n"
" compressor/zram reclaim (0 = off, default off)\n"
" -t sec hold duration after touch, then release\n"
" (default: hold until SIGINT/SIGTERM)\n"
" -q quiet (suppress progress lines)\n"
" -v verbose progress\n"
"\n"
"examples:\n"
" %s 512M -t 30 commit 512M, hold 30s, release\n"
" %s 1500M -t 60 -k 5 hold 60s, re-touch every 5s against compression\n"
" %s 2000M -R -t 900 incompressible ballast (heavy load emulation)\n",
prog, prog, prog, prog);
}
int main(int argc, char **argv)
{
int full_fill = 0, random_fill = 0, random_pct = 0, verbose = 0, quiet = 0;
double keep_interval = 0.0;
double hold_secs = -1.0; /* -1 = 一直持有直到信号 */
const char *size_arg = NULL;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-f") == 0) {
full_fill = 1;
} else if (strcmp(a, "-R") == 0) {
random_fill = 1;
random_pct = 100;
} else if (strcmp(a, "-P") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "error: -P expects a percentage 1..100\n");
return 2;
}
i++;
long v = strtol(argv[i], NULL, 10);
if (v < 1 || v > 100 || argv[i][0] == '\0' ||
(strchr(argv[i], '-') != NULL)) {
fprintf(stderr, "error: -P expects a percentage 1..100\n");
return 2;
}
random_fill = 1;
random_pct = (int)v;
} else if (strcmp(a, "-q") == 0) {
quiet = 1;
} else if (strcmp(a, "-v") == 0) {
verbose = 1;
} else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
usage(stdout, argv[0]);
return 0;
} else if (strcmp(a, "-k") == 0 || strcmp(a, "-t") == 0) {
double v = 0.0;
if (i + 1 >= argc || parse_seconds(argv[i + 1], &v) != 0) {
fprintf(stderr, "error: %s expects a non-negative number of seconds\n", a);
return 2;
}
i++;
if (a[1] == 'k')
keep_interval = v;
else
hold_secs = v;
} else if (a[0] == '-' && a[1] != '\0') {
fprintf(stderr, "error: unknown option '%s'\n", a);
usage(stderr, argv[0]);
return 2;
} else {
if (size_arg != NULL) {
fprintf(stderr, "error: multiple sizes given ('%s' and '%s')\n",
size_arg, a);
return 2;
}
size_arg = a;
}
}
if (size_arg == NULL) {
usage(stderr, argv[0]);
return 2;
}
unsigned long long req = 0;
if (parse_size(size_arg, &req) != 0) {
fprintf(stderr, "error: bad size '%s' (try 512M / 2G / 65536K)\n",
size_arg);
return 2;
}
long ps = sysconf(_SC_PAGESIZE);
if (ps > 0)
g_page = (size_t)ps;
if (req > (unsigned long long)(SIZE_MAX - (g_page - 1))) {
fprintf(stderr, "error: size %llu bytes exceeds this platform's address space\n",
req);
return 2;
}
size_t size = ((size_t)req + g_page - 1) & ~((size_t)g_page - 1);
unsigned long long pages = (unsigned long long)(size / g_page);
setvbuf(stdout, NULL, _IOLBF, 0); /* 管道/ssh 下进度实时可见 */
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* 不重启: nanosleep 被信号打断立刻返回 */
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
double t_start = now_sec();
/* ---- 申请匿名区 ---- */
double t0 = now_sec();
char *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
double map_s = now_sec() - t0;
if (mem == MAP_FAILED) {
fprintf(stderr, "error: mmap %llu bytes failed: %s\n",
(unsigned long long)size, strerror(errno));
#if defined(__APPLE__)
fprintf(stderr, "hint: iOS caps a single process's anonymous reservation "
"(~4GiB observed on 14.8); try a smaller size.\n");
#endif
return 1;
}
if (!quiet)
printf("alloc : %llu bytes (%.2f MiB), %llu page(s) x %zu B, "
"mmap %.3f ms\n",
(unsigned long long)size, to_mib(size), pages, g_page,
map_s * 1e3);
/* ---- 触页提交物理内存 ---- */
unsigned long long done = 0; /* 已触页数 (page-touch 模式) */
t0 = now_sec();
if (random_fill) {
fill_random(mem, size, 0, random_pct); /* 密度见 -P/-R */
} else if (full_fill) {
memset(mem, 0xA5, size); /* 整块填充 (不可中途打断) */
} else {
/* 每页首字节写一次即触发缺页提交; 默认每 10% 汇报, -v 每 1% */
unsigned long long report_every = pages / (verbose ? 100 : 10);
if (report_every == 0)
report_every = 1;
for (size_t off = 0; off < size; off += g_page) {
if (g_stop)
break;
mem[off] = (char)0xA5;
done++;
if (!quiet && (done % report_every) == 0)
printf("touch : %3llu%% (%8.2f MiB) elapsed %.2f s\n",
done * 100ULL / pages,
to_mib(done * (unsigned long long)g_page),
now_sec() - t0);
}
}
double touch_s = now_sec() - t0;
if (g_stop && !full_fill && !random_fill) {
printf("touch : interrupted at %.2f MiB (%llu/%llu pages)\n",
to_mib(done * (unsigned long long)g_page), done, pages);
} else {
printf("touch : %s %llu pages (%.2f MiB) in %.3f s",
(random_fill || full_fill) ? "filled" : "touched",
pages, to_mib(size), touch_s);
if (random_fill)
printf(" [random %d%%/page]", random_pct);
if (touch_s > 0.0)
printf(" [%.1f MiB/s, %.1fK pages/s]",
to_mib(size) / touch_s,
(double)pages / touch_s / 1000.0);
printf("\n");
}
{
unsigned long long rss = get_rss_bytes();
#if defined(__APPLE__)
unsigned long long fp = get_footprint_bytes();
if (rss || fp)
printf("rss : resident %.2f MiB, phys_footprint %.2f MiB\n",
to_mib(rss), to_mib(fp));
#else
if (rss)
printf("rss : resident %.2f MiB\n", to_mib(rss));
#endif
}
/* ---- 保持 (可选周期重触) ---- */
if (!g_stop) {
if (hold_secs >= 0.0) {
if (!quiet)
printf("hold : %.3f s%s\n", hold_secs,
keep_interval > 0.0 ? ", re-touch on" : "");
} else if (!quiet) {
printf("hold : until SIGINT/SIGTERM (press Ctrl-C to release)\n");
}
double hold0 = now_sec();
double deadline = (hold_secs >= 0.0) ? hold0 + hold_secs : -1.0;
double next_cycle = (keep_interval > 0.0) ? hold0 + keep_interval : -1.0;
unsigned cycle = 0;
while (!g_stop) {
double now = now_sec();
if (deadline > 0.0 && now >= deadline)
break;
if (next_cycle > 0.0 && now >= next_cycle) {
cycle++;
/* 写入与上一周期不同的数据: 强制压缩页解压回 RAM、干净页重新变脏;
* -R 模式每轮换随机数据, 保持不可压缩 */
unsigned char pat = (unsigned char)(0xA0 | (cycle & 0x0F));
if (random_fill) {
fill_random(mem, size, cycle, random_pct);
} else if (full_fill) {
memset(mem, pat, size);
} else {
for (size_t off = 0; off < size && !g_stop; off += g_page)
mem[off] = (char)pat;
}
if (!quiet) {
printf("cycle : %u re-touched %.2f MiB", cycle, to_mib(size));
unsigned long long r2 = get_rss_bytes();
if (r2)
printf(", rss %.2f MiB", to_mib(r2));
printf("\n");
}
next_cycle += keep_interval;
continue;
}
struct timespec ts = { 0, 20 * 1000 * 1000 }; /* 20ms 轮询 */
nanosleep(&ts, NULL);
}
}
/* ---- 释放 ---- */
munmap(mem, size);
printf("release: munmap ok, total %.3f s, peak rss %.2f MiB\n",
now_sec() - t_start, to_mib(get_peak_rss_bytes()));
return 0;
}
# mem_alloc_bench — 跨平台构建
#
# make 本机 (macOS/Linux) 编译 mem_stress + mem_bench
# make ios 交叉编译 iOS arm64 二进制 (输出到 ios/)
# make ohos 交叉编译 HarmonyOS arm64 二进制 (输出到 ohos/, 需 OHOS NDK)
# make clean
#
# OHOS NDK 路径按实际安装位置覆盖, 例如:
# make ohos OHOS_NATIVE=/path/to/openharmony/native
CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra -std=c11
BINS = mem_stress mem_bench
# ---- iOS (Xcode 自带工具链) ----
IOS_CC := xcrun -sdk iphoneos clang
IOS_CFLAGS := -arch arm64 -O2 -Wall -Wextra -std=c11 -miphoneos-version-min=12.0
# ---- HarmonyOS (DevEco Studio 的 OHOS NDK, musl libc) ----
OHOS_NATIVE ?= /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native
OHOS_CC := $(OHOS_NATIVE)/llvm/bin/clang
# 真机 arm64 用 aarch64-linux-ohos; 模拟器改 x86_64-linux-ohos
OHOS_TRIPLE ?= aarch64-linux-ohos
OHOS_CFLAGS := --target=$(OHOS_TRIPLE) --sysroot=$(OHOS_NATIVE)/sysroot \
-O2 -Wall -Wextra -std=c11
all: $(BINS)
mem_stress: mem_stress.c
$(CC) $(CFLAGS) -o $@ $<
mem_bench: mem_bench.c
$(CC) $(CFLAGS) -o $@ $<
ios: ios/mem_stress ios/mem_bench
ios/%: %.c
@mkdir -p ios
$(IOS_CC) $(IOS_CFLAGS) -o $@ $<
ohos: ohos/mem_stress ohos/mem_bench
ohos/%: %.c
@mkdir -p ohos
$(OHOS_CC) $(OHOS_CFLAGS) -o $@ $<
clean:
rm -rf $(BINS) ios ohos
.PHONY: all ios ohos clean
#!/usr/bin/env python3
# compare_cases.py — 标准用例数据对比 (消费 run_case.sh 生成的 case-*.txt)
#
# 用法:
# python3 compare_cases.py case-light-touch.txt case-heavy-touch.txt [...] # 第一个文件为基准
#
# 输出: ① 耗时表 — 每文件一行 (fill/mmap/unmap/faults 计时与缺页指标)
# ② 状态表 — pre_* (申请前) 与 post_* (申请驻留时) 全量指标
# (转置: 指标为行, 文件为列, 逐项 Δ vs 基准)
# ③ 驻留开销 — post−pre 逐项差值 (申请 200M 对系统状态的净影响)
# ④ Δ 汇总行 — 主指标相对基准变化
# ⑤ 机器可读 key=value 行 (ROW/DELTA, 含全部 pre_*/post_*_avg/_pct 与 faults)
# 约定: pre_*/post_* 单位 MiB; -1/N/A = 平台无此指标 (如 iOS 无 pre_avail, Linux 无 pre_free_spec)
# 旧格式 case 文件 (无 post_*/faults) 仍可解析: 对应列显示 "-", post 块自动省略
# 仅依赖 Python3 标准库; 对比在采集侧(电脑)进行, 不要求 iOS 上有 python。
import sys
METRICS = ["total", "free", "avail", "free_spec", "file", "anon",
"swap_comp", "comp_occ"] # pre_<m> / post_<m>
def parse(path):
case, recs = {}, []
for line in open(path, encoding="utf-8", errors="replace"):
line = line.strip()
if not line:
continue
toks = line.split()
if toks[0] not in ("CASE", "PHASE", "REC", "SUMMARY", "ERROR"):
continue
d = dict(t.split("=", 1) for t in toks[1:] if "=" in t)
if toks[0] == "CASE":
case = d
elif toks[0] == "REC":
recs.append(d)
return case, recs
def num(d, key):
try:
return float(d[key])
except (KeyError, ValueError):
return None
def mean(xs):
xs = [x for x in xs if x is not None]
return sum(xs) / len(xs) if xs else None
def avg_key(recs, key):
"""REC 令牌求均值; 全 -1 (平台无此指标) -> None"""
vals = [num(r, key) for r in recs]
vals = [v for v in vals if v is not None and v != -1.0]
return mean(vals) if vals else None
def pct(new, old):
if new is None or old is None or old == 0:
return None
return (new - old) / old * 100.0
def fmt_pct(v):
return ("%+.1f%%" % v) if v is not None else "-"
def fmt_int(v):
return ("%d" % round(v)) if v is not None else "-"
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(2)
stats = []
for p in sys.argv[1:]:
case, recs = parse(p)
if not recs:
print("ERROR no REC in %s" % p, file=sys.stderr)
sys.exit(3)
pre = {("pre_" + m): avg_key(recs, "pre_" + m)
for m in METRICS if ("pre_" + m) in recs[0]}
post = {("post_" + m): avg_key(recs, "post_" + m)
for m in METRICS if ("post_" + m) in recs[0]}
stats.append({
"file": p,
"size_mib": case.get("size_mib", "?"),
"mode": case.get("mode", "?"),
"ballast": case.get("ballast_mib", "?"),
"runs": len(recs),
"fill_avg": mean([num(r, "fill_ms") for r in recs]),
"fill_min": min(num(r, "fill_ms") for r in recs),
"fill_max": max(num(r, "fill_ms") for r in recs),
"unit_avg": mean([num(r, "unit") for r in recs]),
"unit_label": recs[0].get("unit_label", "?"),
"mmap_avg": mean([num(r, "mmap_us") for r in recs]),
"unmap_avg": mean([num(r, "unmap_ms") for r in recs]),
"faults": avg_key(recs, "faults"),
"majflt": avg_key(recs, "majflt"),
"pre": pre, "post": post,
"has_post": bool(post),
})
base = stats[0]
any_post = any(s["has_post"] for s in stats)
print("COMPARE files=%d base=%s size_mib=%s mode=%s" %
(len(stats), base["file"], base["size_mib"], base["mode"]))
print()
# ① 耗时表
hdr = "%-28s %8s %4s %-24s %16s %8s %8s %8s" % (
"file", "ballast", "runs", "fill_ms avg/min/max",
"unit", "mmap_us", "unmap_ms", "faults")
print("[耗时表]")
print(hdr)
print("-" * len(hdr))
for s in stats:
print("%-28s %8s %4d %6.2f / %6.2f / %6.2f %10.1f %-7s %8.1f %8.1f %8s" % (
s["file"], s["ballast"], s["runs"],
s["fill_avg"], s["fill_min"], s["fill_max"],
s["unit_avg"], s["unit_label"], s["mmap_avg"], s["unmap_avg"],
fmt_int(s["faults"])))
print()
# ②③ 状态表 + 驻留开销
def state_block(title, key_prefix, delta_vs_base):
col_w = [max(len(s["file"]), 8) for s in stats]
if delta_vs_base and len(stats) > 1:
dlab = ["Δ"] if len(stats) == 2 else \
["Δ:%s" % s["file"][:12] for s in stats[1:]]
d_w = [max(len(l), 8) for l in dlab]
print(title)
print("%-13s" % "指标" +
"".join("%*s" % (w + 2, s["file"]) for w, s in zip(col_w, stats)) +
("".join("%*s" % (w + 2, l) for w, l in zip(d_w, dlab))
if delta_vs_base and len(stats) > 1 else ""))
print("-" * (13 + sum(w + 2 for w in col_w) +
(sum(w + 2 for w in d_w) if delta_vs_base and len(stats) > 1 else 0)))
for m in METRICS:
k = key_prefix + m
if not any(k in s[key_prefix[:-1]] for s in stats if key_prefix[:-1] in s):
continue
row = "%-13s" % m
for s, w in zip(stats, col_w):
v = s[key_prefix[:-1]].get(k)
row += "%*s" % (w + 2, "N/A" if v is None else "%.1f" % v)
if delta_vs_base and len(stats) > 1:
for s, w in zip(stats[1:], d_w):
row += "%*s" % (w + 2, fmt_pct(
pct(s[key_prefix[:-1]].get(k), base[key_prefix[:-1]].get(k))))
print(row)
print()
state_block("[pre_* 申请前快照] (每轮申请时刻前均值, MiB; N/A=平台无此指标)",
"pre_", True)
if any_post:
state_block("[post_* 申请驻留时快照] (fill 后驻留期均值, MiB)", "post_", True)
# 驻留开销 post - pre
col_w = [max(len(s["file"]), 8) for s in stats]
print("[驻留开销 post−pre] (申请 SIZE 对系统状态的净影响, MiB)")
print("%-13s" % "指标" +
"".join("%*s" % (w + 2, s["file"]) for w, s in zip(col_w, stats)))
print("-" * (13 + sum(w + 2 for w in col_w)))
for m in METRICS:
kp, ks = "pre_" + m, "post_" + m
if not any(s["has_post"] for s in stats):
continue
row = "%-13s" % m
for s, w in zip(stats, col_w):
a, b = s["pre"].get(kp), s["post"].get(ks)
row += "%*s" % (w + 2, "-" if (a is None or b is None)
else "%+.1f" % (b - a))
print(row)
print()
# ④ Δ 汇总
if len(stats) > 1:
for s in stats[1:]:
print("DELTA %-28s vs base: fill_ms=%s unit=%s unmap_ms=%s faults=%s pre_free=%s" % (
s["file"],
fmt_pct(pct(s["fill_avg"], base["fill_avg"])),
fmt_pct(pct(s["unit_avg"], base["unit_avg"])),
fmt_pct(pct(s["unmap_avg"], base["unmap_avg"])),
fmt_pct(pct(s["faults"], base["faults"])),
fmt_pct(pct(s["pre"].get("pre_free"), base["pre"].get("pre_free")))))
print()
# ⑤ 机器可读行
print("# 机器可读行 (key=value, 与用例记录同风格):")
for s in stats:
row = ("ROW file=%s size_mib=%s mode=%s ballast_mib=%s runs=%d "
"fill_ms_avg=%.3f fill_ms_min=%.3f fill_ms_max=%.3f "
"unit_avg=%.1f unit_label=%s mmap_us_avg=%.1f unmap_ms_avg=%.3f "
"faults_avg=%s majflt_avg=%s" % (
s["file"], s["size_mib"], s["mode"], s["ballast"], s["runs"],
s["fill_avg"], s["fill_min"], s["fill_max"],
s["unit_avg"], s["unit_label"], s["mmap_avg"], s["unmap_avg"],
"-1" if s["faults"] is None else "%.1f" % s["faults"],
"-1" if s["majflt"] is None else "%.1f" % s["majflt"]))
for m in METRICS:
v = s["pre"].get("pre_" + m)
row += " pre_%s_avg=%s" % (m, "-1" if v is None else "%.1f" % v)
for m in METRICS:
if s["has_post"]:
v = s["post"].get("post_" + m)
row += " post_%s_avg=%s" % (m, "-1" if v is None else "%.1f" % v)
print(row)
for s in stats[1:]:
row = ("DELTA file=%s vs=%s fill_ms_pct=%s unit_pct=%s unmap_ms_pct=%s faults_pct=%s" % (
s["file"], base["file"],
fmt_pct(pct(s["fill_avg"], base["fill_avg"])),
fmt_pct(pct(s["unit_avg"], base["unit_avg"])),
fmt_pct(pct(s["unmap_avg"], base["unmap_avg"])),
fmt_pct(pct(s["faults"], base["faults"]))))
for m in METRICS:
p = pct(s["pre"].get("pre_" + m), base["pre"].get("pre_" + m))
if p is not None:
row += " pre_%s_pct=%s" % (m, fmt_pct(p))
if s["has_post"] and base["has_post"]:
for m in METRICS:
p = pct(s["post"].get("post_" + m), base["post"].get("post_" + m))
if p is not None:
row += " post_%s_pct=%s" % (m, fmt_pct(p))
print(row)
if __name__ == "__main__":
main()