mem_alloc_bench

两个纯 POSIX C 的匿名内存工具 —— 同一份源码在 iOS (越狱) / HarmonyOS / macOS / Linux 上编译运行

POSIX C11单文件 · 零依赖arm64 / x86_64 16K / 4K 页自适应iOS jetsam 口径 phys_footprint

简介

mem_stress.c — 内存加压

按入参大小 mmap 一块匿名私有页 (MAP_PRIVATE|MAP_ANONYMOUS),逐页写入触页提交物理内存,形成真实内存压力。
  • 默认每页写 1 字节提交;-f 整块 memset
  • -t sec 保持时长(默认直到 SIGINT/SIGTERM)
  • -k sec 周期重触,对抗 XNU compressor / 鸿蒙 zram 压缩回收
  • iOS/macOS 额外打印 phys_footprint(jetsam 判定口径)

mem_bench.c — mmap/memset 耗时统计

按入参大小分轮申请匿名页,分别计时 mmap / 填充 / munmap,输出 avg/min/max 与吞吐。
  • -m memset(默认):整块填充 + MiB/s 吞吐
  • -m touch:每页写 1 字节,输出 ns/page 首触缺页成本
  • -m none:仅 mmap/munmap(映射建立成本)
  • 每轮换填充值 0x11/0x22… 并读回校验,防零页/去重优化
设计要点:纯 POSIX C、单文件、无第三方依赖,仅 RSS 读取按平台分支(Apple 走 task_vm_info, Linux/鸿蒙走 /proc/self/statm)。大小参数支持 512M / 2G / 100K / KiB 二进制单位。 参数为手写解析(BSD/musl 的 getopt 不做 GNU 重排,"size 在前选项在后"会解析失败,已规避)。

用法与实测输出

mem_stress — 内存加压

# 提交 512M 匿名页, 保持 2s 后释放; 长时间加压建议加 -k 5 对抗压缩
./mem_stress 512M -t 2
./mem_stress 1500M -t 60 -k 5

macOS (Apple Silicon, 16K 页) 实测:

alloc  : 536870912 bytes (512.00 MiB), 32768 page(s) x 16384 B, mmap 0.040 ms
touch  :  10% (    51.20 MiB) elapsed 0.01 s
...
touch  :  99% (   506.88 MiB) elapsed 0.09 s
touch  : touched 32768 pages (512.00 MiB) in 0.092 s [5580.1 MiB/s, 357.1K pages/s]
rss    : resident 229.52 MiB, phys_footprint 513.22 MiB
hold   : 2.000 s
release: munmap ok, total 2.120 s, peak rss 233.16 MiB
注意上面 rss 229 MiB < phys_footprint 513 MiB —— 触页完成后压缩器已吃掉约一半驻留页, 而 phys_footprint 把已压缩脏页一并计入,才是 jetsam 眼中的真实压力。加压实验请看 footprint。

mem_bench — mmap / memset 耗时统计

# 256M 重复 3 轮, 统计 mmap/memset/unmap 耗时; -m touch 看单缺页成本
./mem_bench 256M -r 3
./mem_bench 256M -m touch

macOS 实测(memset 模式):

target : 268435456 bytes (256.00 MiB), 16384 page(s) x 16384 B, mode=memset, runs=3
run 1/3: mmap=      1.0 us | fill(memset)=    65.330 ms (  3918.6 MiB/s) | unmap=   3.712 ms | rss=  256.56 MiB
run 2/3: mmap=     10.0 us | fill(memset)=    33.109 ms (  7732.0 MiB/s) | unmap=   6.100 ms | rss=  256.80 MiB
run 3/3: mmap=     10.0 us | fill(memset)=    35.269 ms (  7258.5 MiB/s) | unmap=   3.870 ms | rss=  256.80 MiB
---- summary (3 runs, 256.00 MiB, memset mode) ----
mmap   : avg      7.000 us | min      0.999 us | max     10.001 us
fill   : avg     44.569 ms | min     33.109 ms | max     65.330 ms
unmap  : avg      4.561 ms | min      3.712 ms | max      6.100 ms
memset throughput: avg 6303.0 MiB/s over 3 run(s)
peak rss: 256.80 MiB

首触缺页成本对比(-m touch):

macOS (16K 页)   : first-touch fault: avg 1492.7 ns/page over 3 run(s)
Linux musl (4K页): first-touch fault: avg 1103.2 ns/page over 3 run(s)

mmap 本身只是建立映射(µs 级),真正的成本在填充阶段的缺页 + 写入 —— 两者分开计时,恰好回答"申请"与"提交"各自的耗时。

编译与部署

平台命令产物
本机 (macOS/Linux)makemem_stress / mem_bench
iOS (越狱)make iosios/*.arm64 Mach-O
HarmonyOSmake ohos(需 OHOS NDK)ohos/*.musl ELF
iOS — 编译 + 越狱设备部署
make ios
# 等价: xcrun -sdk iphoneos clang -arch arm64 -O2 -miphoneos-version-min=12.0 -o ios/mem_stress mem_stress.c

# 部署 (iproxy 2222 → 设备 22, root/alpine)
scp -P 2222 ios/mem_stress root@localhost:/var/mobile/ios_memslice/
ssh -p 2222 root@localhost \
  '/usr/bin/ldid -S /var/mobile/ios_memslice/mem_stress && chmod +x /var/mobile/ios_memslice/mem_stress'
ssh -p 2222 root@localhost '/var/mobile/ios_memslice/mem_stress 200M -t 10'
  • 必须设备端 ldid -S 签名:Mac 侧签完传上去会被 SIGKILL (rc=137)
  • 覆盖二进制先 rm 再传,直接覆盖同样触发 rc=137
  • sftp/scp 上传后必须 chmod +x;工具仅需普通 mmap,无需 entitlements
  • 非越狱 iOS 无法直接跑 CLI 二进制,同一份源码可嵌进 App target 编译
HarmonyOS — OHOS NDK 编译 + hdc 部署
make ohos
# 等价 (OHOS_NATIVE 按实际安装路径调整):
OHOS_NATIVE=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native
$OHOS_NATIVE/llvm/bin/clang --target=aarch64-linux-ohos \
    --sysroot=$OHOS_NATIVE/sysroot -O2 -o ohos/mem_stress mem_stress.c

# 部署 (设备需开发者模式)
hdc file send ohos/mem_stress /data/local/tmp/mem_stress
hdc shell "chmod +x /data/local/tmp/mem_stress"
hdc shell "/data/local/tmp/mem_stress 200M -t 10"
  • 必须用 OHOS NDK 的 clang(链接 musl,动态链接器 /lib/ld-musl-aarch64.so.1);glibc 交叉链产物设备上无法运行
  • 模拟器 (x86_64):make ohos OHOS_TRIPLE=x86_64-linux-ohos
  • 鸿蒙存在 zram 压缩回收,长时间加压建议 -k 5 重触

验证矩阵(2026-10 实测)

平台编译运行
macOS (Apple Silicon, 16K 页)✅ -Wall -Wextra 零警告✅ 全模式 + SIGTERM 干净退出 + 压缩行为观察
iOS arm64 (xcrun -sdk iphoneos clang)✅ 零警告, 纯 libSystem 符号✅ 真机全模式验证 + 轻重载实验 (见实验报告)
Linux aarch64 glibc✅ 零警告✅ 全模式(4K 页, /proc RSS)
Linux aarch64 musl(鸿蒙同族 libc)✅ 零警告✅ 全模式
HarmonyOS 真机命令已给出(本机无 NDK/hdc)待设备

实验报告:轻重载内存压力下申请 200M / 500M 耗时对比

实验设计

  • 设备: iPhone 12 Pro (iPhone13,3) · iOS 14.8.1 越狱 · 6GB · 16K 页 (200M = 12800 页)
  • 自变量: mem_stress -P 50 ballast 阶梯加压 (0 → 3200M → 3600M → 4600M),模拟真实负载的半随机可压缩数据
  • 因变量: mem_bench 200M -r 5 的 mmap / 首触缺页 (touch) / memset 耗时
  • 控制: 轻载与重载在同一启动会话内测量;ballast 全程带 -t 死开关;vm_stat 实时监控 free 页;测后释放并复测基线排除热漂移
ballast 模式WKdm 压缩比系统行为
默认触页 / -f 模式填充~1000:1被 compressor 零成本吸收(2400M 只占 991 页),形不成压力
-P 50 半随机(本实验采用)~2:1~2.5:1真实负载模型:压缩器持续工作但有余量,系统优雅降级
-R 全随机~1:1压缩器被塞满 → 守护进程饿死 → watchdog 内核 panic

结果:申请 200M(12800 页)耗时对比

状态ballastfree 页mmap 耗时*touch 首触 (纯申请)vs 轻载memset 耗时vs 轻载memset 吞吐
轻载基线011053515.2 µs35.9 ms (2804 ns/页)—45.4 ms—4411 MiB/s
重载-13200M959615.3 µs43.0 ms (3358 ns/页)+19.8%47.2 ms+4.1%4234 MiB/s
重载-23600M669018.3 µs43.8 ms (3424 ns/页)+22.1%47.8 ms+5.3%4190 MiB/s
重载-34600M775315.4 µs52.6 ms (4111 ns/页)+46.6%51.5 ms+13.6%3885 MiB/s
恢复复测024079013.0 µs36.4 ms (2844 ns/页)+1.4%43.9 ms-3.2%4561 MiB/s

* mmap 均值取 touch/memset 两种模式各 5 轮共 10 次测量; 全部 50+ 次测量落在 8~27µs,与压力水平无关。unmap 释放 5.3~20.5ms(重载下偶发尖峰)。

冷首轮尖峰:压力态下第一笔分配最贵 —— 重载-2 首轮 4356 ns/页 (+55%), 重载-3 首轮 6995 ns/页 (+150%);后续轮次系统已提前回收,回落到 3000~3600。 mmap 建映射耗时恒定 9~27µs(与压力无关,XNU 预留免费,成本全在首触缺页)。

补测:申请 500M(32000 页)轻重载对比

同协议重测(重载 = 3600M+1000M -P 50 双实例,测量起点 free=4398 页,略深于 200M 重载-3 的 7753):

状态ballastfree 页mmap 耗时*touch 首触 (纯申请)vs 轻载memset 耗时vs 轻载memset 吞吐
轻载基线021331717.9 µs91.1 ms (2847 ns/页)—114.1 ms—4382 MiB/s
重载4600M439823.5 µs122.2 ms (3820 ns/页)+34.2%117.6 ms+3.0%4257 MiB/s
恢复复测026058915.2 µs93.9 ms (2935 ns/页)+3.1%115.5 ms+1.2%4330 MiB/s

* mmap 两模式均值,仍与压力无关;冷首轮 6518 ns/页 (208.6ms, +129%), run2~5 回落 2988~3349;测量期间 compressions +53195 次(200M 实验同期约 +15k)。

跨尺寸对比:200M vs 500M

指标200M (12800 页)500M (32000 页)解读
轻载单页成本2804 ns/页2847 ns/页完全一致 —— 申请成本线性,无尺寸惩罚
重载均值增幅+46.6%+34.2%500M 填充时长 2.5 倍,pageout 有更多"边填边回收"重叠时间,稳态反而更低
重载冷首轮+150% (6995)+129% (6518)第一轮付清全部回收欠账,尖峰比例相当
memset 重载增幅+13.6%+3.0%流式写与回收重叠,带宽几乎不受影响
unmap 释放~7.7 ms~18 ms~38µs/MiB 线性,与压力基本无关

关键发现

⚠ 实测事故(已成为设计准则):实验早期用 -R 全随机 ballast 加压至 4600M, 10:11:13 内核 panic:userspace watchdog timeout: jailbreakd BSOD handler failed … no successful checkins from com.apple.mediaserverd in 180 seconds(panic 时 free=2867 页、 compressorSize=291717 页=4.45GiB)。教训:压力实验用 -P 50 + 死开关 + free>2000 监控。
完整原始实验记录(508 行,含每一步 vm_stat 快照)
ios-200m-light-vs-heavy.txt轻载 vs 重载实验完整日志 下载
# 实验: 轻载 vs 重载内存压力下, 申请 200M 匿名页的耗时对比
# 设备: iPhone 12 Pro (iPhone13,3) iOS 14.8 Taurine, 6GB, 16K 页, 总页数 367104
# 工具: mem_bench 200M (touch=纯申请/首触缺页, memset=申请+整块写入)
# 协议: 轻载基线 -> ballast 阶梯加压(free 1000-2000 页区间) -> 重载测量 -> 释放恢复复测
# 时间: 2026-10-09 09:56:17

########## Phase A: 轻载基线 ##########
----- vm_stat (轻载, before) -----
Pages free:                               86949.
Pages speculative:                        58176.
File-backed pages:                        90245.
Anonymous pages:                          83116.
Pages stored in compressor:                   0.
Pages occupied by compressor:                 0.
Decompressions:                               0.
Compressions:                                 0.

----- mem_bench 200M -r 5 -m touch (轻载) -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     10.0 us | fill(touch )=    38.013 ms (  2969.8 ns/page) | unmap=   7.208 ms | rss=  202.44 MiB
run 2/5: mmap=     20.0 us | fill(touch )=    32.744 ms (  2558.1 ns/page) | unmap=   7.053 ms | rss=  202.47 MiB
run 3/5: mmap=     15.0 us | fill(touch )=    37.463 ms (  2926.8 ns/page) | unmap=   7.943 ms | rss=  202.47 MiB
run 4/5: mmap=     13.0 us | fill(touch )=    37.458 ms (  2926.4 ns/page) | unmap=   7.808 ms | rss=  202.47 MiB
run 5/5: mmap=     14.0 us | fill(touch )=    37.731 ms (  2947.7 ns/page) | unmap=   7.415 ms | rss=  202.47 MiB
---- summary (5 runs, 200.00 MiB, touch mode) ----
mmap   : avg     14.400 us | min     10.000 us | max     20.000 us
fill   : avg     36.682 ms | min     32.744 ms | max     38.013 ms
unmap  : avg      7.485 ms | min      7.053 ms | max      7.943 ms
first-touch fault: avg 2865.8 ns/page over 5 run(s)
peak rss: 202.47 MiB

----- mem_bench 200M -r 5 -m memset (轻载) -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     11.0 us | fill(memset)=    47.219 ms (  4235.6 MiB/s) | unmap=   8.542 ms | rss=  202.38 MiB
run 2/5: mmap=     19.0 us | fill(memset)=    43.310 ms (  4617.9 MiB/s) | unmap=   5.603 ms | rss=  202.41 MiB
run 3/5: mmap=     24.0 us | fill(memset)=    44.220 ms (  4522.8 MiB/s) | unmap=   7.398 ms | rss=  202.41 MiB
run 4/5: mmap=     23.0 us | fill(memset)=    46.967 ms (  4258.3 MiB/s) | unmap=   8.162 ms | rss=  202.41 MiB
run 5/5: mmap=     26.0 us | fill(memset)=    46.333 ms (  4316.6 MiB/s) | unmap=   8.050 ms | rss=  202.41 MiB
---- summary (5 runs, 200.00 MiB, memset mode) ----
mmap   : avg     20.600 us | min     11.000 us | max     26.000 us
fill   : avg     45.610 ms | min     43.310 ms | max     47.219 ms
unmap  : avg      7.551 ms | min      5.603 ms | max      8.542 ms
memset throughput: avg 4390.2 MiB/s over 5 run(s)
peak rss: 202.41 MiB

----- vm_stat (轻载, after) -----
Pages free:                               87049.
Pages speculative:                        58022.
File-backed pages:                        90091.
Anonymous pages:                          83272.
Pages stored in compressor:                   0.
Pages occupied by compressor:                 0.
Decompressions:                               0.
Compressions:                                 0.

########## Phase B: 阶梯加压 ##########
----- B1: ballast 2400M 启动 -----
PID=1516
Pages free:                                6267.
Pages speculative:                          342.
File-backed pages:                        74790.
Anonymous pages:                         232740.
Pages stored in compressor:                  29.
Pages occupied by compressor:                16.
Compressions:                                37.
 1516 /var/mobile/ios_memslice/mem_stress
----- B2: ballast +1200M (累计 3600M) -----
PID=1516
PID=1551
Pages free:                                8993.
Pages speculative:                          115.
File-backed pages:                        74228.
Anonymous pages:                         233087.
Pages stored in compressor:               75055.
Pages occupied by compressor:               991.
Decompressions:                              32.
Compressions:                             75087.
 1516 /var/mobile/ios_memslice/mem_stress
 1551 /var/mobile/ios_memslice/mem_stress

----- B3: 压缩性 A/B 验证 (512M 模式 vs 512M 随机, 各保持 6s) -----
--- pattern 512M (每页1字节 0xA5) ---
touch  : touched 32768 pages (512.00 MiB) in 0.116 s [4408.4 MiB/s, 282.1K pages/s]
rss    : resident 514.39 MiB, phys_footprint 513.30 MiB
release: munmap ok, total 6.168 s, peak rss 514.41 MiB
--- random 512M (-R) ---
touch  : filled(random) 32768 pages (512.00 MiB) in 0.372 s [1377.9 MiB/s, 88.2K pages/s]
rss    : resident 514.39 MiB, phys_footprint 513.30 MiB
release: munmap ok, total 6.409 s, peak rss 514.41 MiB

----- B4: 清场 + 随机 ballast 2000M 起步 (-R 不可压缩) -----
Pages free:                               41558.
File-backed pages:                        74194.
Pages stored in compressor:              110720.
PID=1765
Pages free:                                5651.
Pages speculative:                           97.
File-backed pages:                        74185.
Anonymous pages:                         232525.
Pages stored in compressor:              206406.
Pages occupied by compressor:              1529.
Decompressions:                              84.
Compressions:                            206574.
 1516 /var/mobile/ios_memslice/mem_stress
 1551 /var/mobile/ios_memslice/mem_stress
 1765 /var/mobile/ios_memslice/mem_stress
----- B5: 清掉旧 pattern ballast + 随机 ballast +600M (累计 2600M) -----
 1765 /var/mobile/ios_memslice/mem_stress
1811
Pages free:                                5175.
Pages speculative:                          113.
File-backed pages:                        74207.
Anonymous pages:                          63779.
Pages stored in compressor:              178792.
Pages occupied by compressor:            170441.
Decompressions:                             578.
Compressions:                            383746.
----- B6: 随机 ballast +800M (累计 3400M) -----
1865
Pages free:                                5756.
Pages speculative:                         3989.
File-backed pages:                        49673.
Anonymous pages:                          44451.
Pages stored in compressor:              243078.
Pages occupied by compressor:            216900.
Decompressions:                            4828.
Compressions:                            452505.
----- B7: 随机 ballast +800M (累计 4200M) -----
1895
Pages free:                                7303.
Pages speculative:                          495.
File-backed pages:                        18031.
Anonymous pages:                          14997.
Pages stored in compressor:              285747.
Pages occupied by compressor:            275337.
Decompressions:                           21682.
Compressions:                            539908.
----- B8: 随机 ballast +400M (累计 4600M) -----
2189
Exception (client): Error reading SSH protocol banner
Traceback (most recent call last):
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 2213, in _check_banner
    buf = self.packetizer.readline(timeout)
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/packet.py", line 395, in readline
    buf += self._read_timeout(timeout)
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/packet.py", line 665, in _read_timeout
    raise EOFError()
EOFError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 2029, in run
    self._check_banner()
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 2217, in _check_banner
    raise SSHException(
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner

Traceback (most recent call last):
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 2213, in _check_banner
    buf = self.packetizer.readline(timeout)
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/packet.py", line 395, in readline
    buf += self._read_timeout(timeout)
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/packet.py", line 665, in _read_timeout
    raise EOFError()
EOFError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/sched/Documents/workspace/mem_alloc_bench/run_on_device.py", line 43, in <module>
    c = connect()
  File "/Users/sched/Documents/workspace/mem_alloc_bench/run_on_device.py", line 16, in connect
    c.connect("localhost", port=2222, username="root", password="alpine",
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/client.py", line 443, in connect
    t.start_client(timeout=timeout)
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 701, in start_client
    raise e
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 2029, in run
    self._check_banner()
  File "/Users/sched/Library/Python/3.9/lib/python/site-packages/paramiko/transport.py", line 2217, in _check_banner
    raise SSHException(
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner

########## [重启后正式实验 10:5x 起, panic 后同一启动会话] ##########
----- 前置: 系统状态 -----
 10:58:04  up   0:47,  0 users,  load average: 2.21, 5.08, 4.17
Pages free:                              113701.
File-backed pages:                        81616.
Pages stored in compressor:                   0.
Compressions:                              1781.

----- Phase A': 轻载基线 mem_bench 200M -r 5 -m touch -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     12.0 us | fill(touch )=    35.096 ms (  2741.9 ns/page) | unmap=   7.420 ms | rss=  202.34 MiB
run 2/5: mmap=     15.0 us | fill(touch )=    35.847 ms (  2800.5 ns/page) | unmap=   7.354 ms | rss=  202.38 MiB
run 3/5: mmap=     15.0 us | fill(touch )=    35.055 ms (  2738.7 ns/page) | unmap=   7.656 ms | rss=  202.38 MiB
run 4/5: mmap=     18.0 us | fill(touch )=    36.559 ms (  2856.2 ns/page) | unmap=   7.690 ms | rss=  202.38 MiB
run 5/5: mmap=     20.0 us | fill(touch )=    36.880 ms (  2881.3 ns/page) | unmap=   8.315 ms | rss=  202.38 MiB
---- summary (5 runs, 200.00 MiB, touch mode) ----
mmap   : avg     16.000 us | min     12.000 us | max     20.000 us
fill   : avg     35.887 ms | min     35.055 ms | max     36.880 ms
unmap  : avg      7.687 ms | min      7.354 ms | max      8.315 ms
first-touch fault: avg 2803.7 ns/page over 5 run(s)
peak rss: 202.38 MiB

----- Phase A': 轻载基线 mem_bench 200M -r 5 -m memset -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     11.0 us | fill(memset)=    45.899 ms (  4357.4 MiB/s) | unmap=   8.661 ms | rss=  202.41 MiB
run 2/5: mmap=     15.0 us | fill(memset)=    46.542 ms (  4297.2 MiB/s) | unmap=   8.125 ms | rss=  202.44 MiB
run 3/5: mmap=     18.0 us | fill(memset)=    44.581 ms (  4486.2 MiB/s) | unmap=   8.013 ms | rss=  202.44 MiB
run 4/5: mmap=      9.0 us | fill(memset)=    44.412 ms (  4503.3 MiB/s) | unmap=   7.889 ms | rss=  202.44 MiB
run 5/5: mmap=     19.0 us | fill(memset)=    45.345 ms (  4410.6 MiB/s) | unmap=   7.734 ms | rss=  202.44 MiB
---- summary (5 runs, 200.00 MiB, memset mode) ----
mmap   : avg     14.400 us | min      9.000 us | max     19.000 us
fill   : avg     45.356 ms | min     44.412 ms | max     46.542 ms
unmap  : avg      8.084 ms | min      7.734 ms | max      8.661 ms
memset throughput: avg 4410.9 MiB/s over 5 run(s)
peak rss: 202.44 MiB

----- vm_stat (轻载基线后) -----
Pages free:                              110535.
File-backed pages:                        81526.
Pages stored in compressor:                   0.
Compressions:                              1781.

----- B'1: ballast 1600M -P 50 (累计 1600M) -----
2090
Pages free:                               14192.
Pages speculative:                        49480.
File-backed pages:                        79309.
Anonymous pages:                         171566.
Pages stored in compressor:                   0.
Pages occupied by compressor:                32.
Decompressions:                             302.
Compressions:                              1781.
----- B'2: ballast +400M (累计 2000M) -----
2120
Pages free:                                9939.
Pages speculative:                        40712.
File-backed pages:                        69471.
Anonymous pages:                         194349.
Pages stored in compressor:                   0.
Pages occupied by compressor:                32.
Decompressions:                             302.
Compressions:                              1781.
----- B'3: ballast +400M (累计 2400M) -----
2161
Pages free:                                7525.
Pages speculative:                        15316.
File-backed pages:                        68406.
Anonymous pages:                         220178.
Pages stored in compressor:                   0.
Pages occupied by compressor:                32.
Decompressions:                             302.
Compressions:                              1781.
----- B'4: ballast +400M (累计 2800M) -----
2199
Pages free:                                8021.
Pages speculative:                          376.
File-backed pages:                        72520.
Anonymous pages:                         228919.
Pages stored in compressor:               16209.
Pages occupied by compressor:              8373.
Decompressions:                             340.
Compressions:                             18030.
----- B'5: ballast +400M (累计 3200M, 上限) -----
2242
Pages free:                                6670.
Pages speculative:                          740.
File-backed pages:                        72180.
Anonymous pages:                         194349.
Pages stored in compressor:               75869.
Pages occupied by compressor:             41306.
Decompressions:                             399.
Compressions:                             77858.
5

########## Phase C': 重载(3200M ballast)下测量 ##########
----- vm_stat (重载, before) -----
Pages free:                                9596.
Pages speculative:                          856.
File-backed pages:                        72311.
Anonymous pages:                         194653.
Pages stored in compressor:               75721.
Pages occupied by compressor:             41234.
Decompressions:                             486.
Compressions:                             77858.

----- mem_bench 200M -r 5 -m touch (重载) -----
error: multiple sizes given ('200M' and '600')

----- vm_stat (touch 测量后) -----
Pages free:                                9515.
Pages stored in compressor:               75721.
Decompressions:                             486.
Compressions:                             77858.

----- mem_bench 200M -r 5 -m memset (重载) -----
error: multiple sizes given ('200M' and '600')

----- vm_stat (重载, after) -----
Pages free:                                6164.
File-backed pages:                        72317.
Pages stored in compressor:               75721.
Pages occupied by compressor:             41234.
Decompressions:                             486.
Compressions:                             77858.
----- mem_bench 200M -r 5 -m touch (重载, 修正) -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     14.0 us | fill(touch )=    45.172 ms (  3529.1 ns/page) | unmap=  20.535 ms | rss=  202.31 MiB
run 2/5: mmap=     14.0 us | fill(touch )=    40.470 ms (  3161.7 ns/page) | unmap=   6.474 ms | rss=  202.34 MiB
run 3/5: mmap=     16.0 us | fill(touch )=    43.704 ms (  3414.4 ns/page) | unmap=   7.773 ms | rss=  202.34 MiB
run 4/5: mmap=     16.0 us | fill(touch )=    38.578 ms (  3013.9 ns/page) | unmap=   6.597 ms | rss=  202.34 MiB
run 5/5: mmap=     10.0 us | fill(touch )=    46.995 ms (  3671.5 ns/page) | unmap=   7.320 ms | rss=  202.34 MiB
---- summary (5 runs, 200.00 MiB, touch mode) ----
mmap   : avg     14.000 us | min     10.000 us | max     16.000 us
fill   : avg     42.984 ms | min     38.578 ms | max     46.995 ms
unmap  : avg      9.740 ms | min      6.474 ms | max     20.535 ms
first-touch fault: avg 3358.1 ns/page over 5 run(s)
peak rss: 202.34 MiB

----- mem_bench 200M -r 5 -m memset (重载, 修正) -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     10.0 us | fill(memset)=    47.213 ms (  4236.1 MiB/s) | unmap=   7.066 ms | rss=  202.38 MiB
run 2/5: mmap=     13.0 us | fill(memset)=    46.587 ms (  4293.0 MiB/s) | unmap=   6.873 ms | rss=  202.44 MiB
run 3/5: mmap=     21.0 us | fill(memset)=    47.779 ms (  4185.9 MiB/s) | unmap=   7.179 ms | rss=  202.44 MiB
run 4/5: mmap=     23.0 us | fill(memset)=    47.336 ms (  4225.1 MiB/s) | unmap=   7.643 ms | rss=  202.44 MiB
run 5/5: mmap=     16.0 us | fill(memset)=    47.274 ms (  4230.7 MiB/s) | unmap=   8.002 ms | rss=  202.44 MiB
---- summary (5 runs, 200.00 MiB, memset mode) ----
mmap   : avg     16.600 us | min     10.000 us | max     23.000 us
fill   : avg     47.238 ms | min     46.587 ms | max     47.779 ms
unmap  : avg      7.353 ms | min      6.873 ms | max      8.002 ms
memset throughput: avg 4234.2 MiB/s over 5 run(s)
peak rss: 202.44 MiB

----- vm_stat (重载测量后) -----
Pages free:                               14876.
File-backed pages:                        71159.
Pages stored in compressor:               84584.
Pages occupied by compressor:             46117.
Decompressions:                             487.
Compressions:                             86725.

----- B'6: 重启 ballast 为单实例 3600M -P 50 -t 600 -----
Pages free:                              182382.
Pages stored in compressor:                2140.
--- 启动 3600M 单实例 ---
2380
Pages free:                                6690.
Pages speculative:                          869.
File-backed pages:                        49623.
Anonymous pages:                         241395.
Pages stored in compressor:               49426.
Pages occupied by compressor:             19671.
Decompressions:                           88609.
Compressions:                            222194.

########## Phase C'2: 重载-2 (3600M ballast) 下测量 ##########
----- mem_bench 200M -r 5 -m touch -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     10.0 us | fill(touch )=    55.757 ms (  4356.0 ns/page) | unmap=  11.909 ms | rss=  202.38 MiB
run 2/5: mmap=     27.0 us | fill(touch )=    43.881 ms (  3428.2 ns/page) | unmap=   6.828 ms | rss=  202.41 MiB
run 3/5: mmap=     16.0 us | fill(touch )=    43.631 ms (  3408.7 ns/page) | unmap=   6.215 ms | rss=  202.41 MiB
run 4/5: mmap=     24.0 us | fill(touch )=    38.083 ms (  2975.2 ns/page) | unmap=   6.152 ms | rss=  202.41 MiB
run 5/5: mmap=     18.0 us | fill(touch )=    37.786 ms (  2952.0 ns/page) | unmap=   5.731 ms | rss=  202.41 MiB
---- summary (5 runs, 200.00 MiB, touch mode) ----
mmap   : avg     19.000 us | min     10.000 us | max     27.000 us
fill   : avg     43.828 ms | min     37.786 ms | max     55.757 ms
unmap  : avg      7.367 ms | min      5.731 ms | max     11.909 ms
first-touch fault: avg 3424.0 ns/page over 5 run(s)
peak rss: 202.41 MiB

----- mem_bench 200M -r 5 -m memset -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     11.0 us | fill(memset)=    46.442 ms (  4306.4 MiB/s) | unmap=   7.360 ms | rss=  202.31 MiB
run 2/5: mmap=     24.0 us | fill(memset)=    47.410 ms (  4218.5 MiB/s) | unmap=   7.586 ms | rss=  202.34 MiB
run 3/5: mmap=     14.0 us | fill(memset)=    50.152 ms (  3987.9 MiB/s) | unmap=   7.843 ms | rss=  202.34 MiB
run 4/5: mmap=     19.0 us | fill(memset)=    48.134 ms (  4155.1 MiB/s) | unmap=   7.731 ms | rss=  202.34 MiB
run 5/5: mmap=     20.0 us | fill(memset)=    46.689 ms (  4283.7 MiB/s) | unmap=   7.863 ms | rss=  202.34 MiB
---- summary (5 runs, 200.00 MiB, memset mode) ----
mmap   : avg     17.600 us | min     11.000 us | max     24.000 us
fill   : avg     47.765 ms | min     46.442 ms | max     50.152 ms
unmap  : avg      7.677 ms | min      7.360 ms | max      7.863 ms
memset throughput: avg 4190.3 MiB/s over 5 run(s)
peak rss: 202.34 MiB

----- vm_stat (重载-2 测量后) -----
Pages free:                               14969.
File-backed pages:                        43427.
Pages stored in compressor:               53622.
Pages occupied by compressor:             21995.
Decompressions:                           88635.
Compressions:                            226418.

----- B'7: 重启为 3600M + 1000M 双实例 (累计 4600M) -----
2489
--- +1000M ---
2511
Pages free:                                7753.
Pages speculative:                          749.
File-backed pages:                        33222.
Anonymous pages:                         198043.
Pages stored in compressor:              156749.
Pages occupied by compressor:             78372.
Decompressions:                           99552.
Compressions:                            353515.

########## Phase C'3: 重载-3 (4600M ballast, 压缩器持有 2.39G) 下测量 ##########
----- mem_bench 200M -r 5 -m touch -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     11.0 us | fill(touch )=    89.537 ms (  6995.1 ns/page) | unmap=  13.056 ms | rss=  202.38 MiB
run 2/5: mmap=     22.0 us | fill(touch )=    42.230 ms (  3299.2 ns/page) | unmap=   7.901 ms | rss=  202.44 MiB
run 3/5: mmap=     20.0 us | fill(touch )=    39.736 ms (  3104.4 ns/page) | unmap=   7.672 ms | rss=  202.44 MiB
run 4/5: mmap=     17.0 us | fill(touch )=    46.354 ms (  3621.4 ns/page) | unmap=   7.690 ms | rss=  202.44 MiB
run 5/5: mmap=     13.0 us | fill(touch )=    45.267 ms (  3536.5 ns/page) | unmap=   6.450 ms | rss=  202.44 MiB
---- summary (5 runs, 200.00 MiB, touch mode) ----
mmap   : avg     16.600 us | min     11.000 us | max     22.000 us
fill   : avg     52.625 ms | min     39.736 ms | max     89.537 ms
unmap  : avg      8.554 ms | min      6.450 ms | max     13.056 ms
first-touch fault: avg 4111.3 ns/page over 5 run(s)
peak rss: 202.44 MiB

----- mem_bench 200M -r 5 -m memset -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     11.0 us | fill(memset)=    53.629 ms (  3729.3 MiB/s) | unmap=   6.615 ms | rss=  202.31 MiB
run 2/5: mmap=     18.0 us | fill(memset)=    51.653 ms (  3872.0 MiB/s) | unmap=   7.813 ms | rss=  202.34 MiB
run 3/5: mmap=     12.0 us | fill(memset)=    49.734 ms (  4021.4 MiB/s) | unmap=   7.805 ms | rss=  202.34 MiB
run 4/5: mmap=     12.0 us | fill(memset)=    53.077 ms (  3768.1 MiB/s) | unmap=   7.610 ms | rss=  202.34 MiB
run 5/5: mmap=     18.0 us | fill(memset)=    49.575 ms (  4034.3 MiB/s) | unmap=   7.287 ms | rss=  202.34 MiB
---- summary (5 runs, 200.00 MiB, memset mode) ----
mmap   : avg     14.200 us | min     11.000 us | max     18.000 us
fill   : avg     51.534 ms | min     49.575 ms | max     53.629 ms
unmap  : avg      7.426 ms | min      6.615 ms | max      7.813 ms
memset throughput: avg 3885.0 MiB/s over 5 run(s)
peak rss: 202.34 MiB

----- vm_stat (重载-3 测量后) -----
Pages free:                               15131.
File-backed pages:                        32792.
Pages stored in compressor:              171828.
Pages occupied by compressor:             86710.
Decompressions:                          100680.
Compressions:                            369902.

########## Phase D: 释放 + 恢复复测 ##########
----- 释放全部 ballast -----
zsh:1: no matches found: [m]em_stress
killed
Pages free:                              240790.
File-backed pages:                        41256.
Pages stored in compressor:               41898.
Pages occupied by compressor:             15493.

----- 恢复后复测 mem_bench 200M -r 3 -m touch -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=touch , runs=3
run 1/3: mmap=     10.0 us | fill(touch )=    38.698 ms (  3023.3 ns/page) | unmap=   6.951 ms | rss=  202.38 MiB
run 2/3: mmap=      9.0 us | fill(touch )=    39.747 ms (  3105.2 ns/page) | unmap=   6.099 ms | rss=  202.41 MiB
run 3/3: mmap=      8.0 us | fill(touch )=    30.754 ms (  2402.7 ns/page) | unmap=   5.251 ms | rss=  202.41 MiB
---- summary (3 runs, 200.00 MiB, touch mode) ----
mmap   : avg      9.000 us | min      8.000 us | max     10.000 us
fill   : avg     36.400 ms | min     30.754 ms | max     39.747 ms
unmap  : avg      6.100 ms | min      5.251 ms | max      6.951 ms
first-touch fault: avg 2843.7 ns/page over 3 run(s)
peak rss: 202.41 MiB

----- 恢复后复测 mem_bench 200M -r 3 -m memset -----
target : 209715200 bytes (200.00 MiB), 12800 page(s) x 16384 B, mode=memset, runs=3
run 1/3: mmap=     10.0 us | fill(memset)=    45.712 ms (  4375.2 MiB/s) | unmap=   6.954 ms | rss=  202.38 MiB
run 2/3: mmap=     23.0 us | fill(memset)=    44.052 ms (  4540.1 MiB/s) | unmap=   7.309 ms | rss=  202.41 MiB
run 3/3: mmap=     18.0 us | fill(memset)=    41.952 ms (  4767.4 MiB/s) | unmap=   6.868 ms | rss=  202.41 MiB
---- summary (3 runs, 200.00 MiB, memset mode) ----
mmap   : avg     17.000 us | min     10.000 us | max     23.000 us
fill   : avg     43.905 ms | min     41.952 ms | max     45.712 ms
unmap  : avg      7.044 ms | min      6.868 ms | max      7.309 ms
memset throughput: avg 4560.9 MiB/s over 3 run(s)
peak rss: 202.41 MiB

----- 最终 vm_stat -----
Pages free:                              237475.
File-backed pages:                        41256.
Pages stored in compressor:               41897.
Compressions:                            369902.

########## 分析总结 (2026-10-09) ##########
# 设备: iPhone 12 Pro / iOS 14.8.1 / 6GB / 16K 页; mem_bench 200M = 12800 页
#
# | 状态          | ballast | free页  | mmap µs* | touch 首触 (纯申请)    | vs轻载 | memset 耗时 | vs轻载 | memset MiB/s |
# |---------------|---------|---------|----------|------------------------|--------|-------------|--------|--------------|
# | 轻载基线      | 0       | 110535  | 15.2     | 35.9ms (2803.7 ns/页)  | —      | 45.4ms      | —      | 4410.9       |
# | 重载-1        | 3200M   | 9596    | 15.3     | 43.0ms (3358.1 ns/页)  | +19.8% | 47.2ms      | +4.1%  | 4234.2       |
# | 重载-2        | 3600M   | 6690    | 18.3     | 43.8ms (3424.0 ns/页)  | +22.1% | 47.8ms      | +5.3%  | 4190.3       |
# | 重载-3        | 4600M   | 7753    | 15.4     | 52.6ms (4111.3 ns/页)  | +46.6% | 51.5ms      | +13.6% | 3885.0       |
# | 恢复复测      | 0       | 240790  | 13.0     | 36.4ms (2843.7 ns/页)  | +1.4%  | 43.9ms      | -3.2%  | 4560.9       |
# * mmap 为 touch/memset 两模式各 5 轮均值; 全部 50+ 次测量 8~27µs, 与压力无关
#   unmap 释放 5.3~20.5ms (重载偶发尖峰)
#
# 冷启动首轮尖峰 (压力态下第一笔分配最贵):
#   重载-2 run1 = 4356 ns/page (+55%), 重载-3 run1 = 6995 ns/page (+150%)
# mmap 建映射耗时恒定 9~27us (与压力无关); unmap 5.3~20.5ms (重载偶发尖峰)
#
# 结论:
# 1. "申请200M"的真实成本在首触缺页: 中载+20%, 重载+47%, 冷首轮最高+150%
# 2. memset 带宽受影响较小 (-4%~-12%), 流式写与回收重叠
# 3. XNU 压缩器是压力球阀: -P 50 ballast 以 ~2:1~2.5:1 被持续吸收,
#    4600M 负载下仍守在 free_target 之上, 系统优雅降级
# 4. 悬崖: 不可压缩(随机)数据 1:1 塞满压缩器 -> 守护进程饿死 ->
#    userspace watchdog 180s -> 内核 panic (10:11 事故, 见 INCIDENT-panic-1011.txt)
# 5. 加压实验设计准则: ballast 必须部分可压缩(-P 50≈真实App数据);
#    全随机=DoS, 模式填充=被零成本吸收

########## 补测实验: 申请 500M 轻重载对比 (2026-10-09 14:27 起, 与 200M 实验同一启动会话) ##########
----- Phase A-500: 轻载基线 mem_bench 500M -r 5 -m touch -----
target : 524288000 bytes (500.00 MiB), 32000 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     11.0 us | fill(touch )=    93.043 ms (  2907.6 ns/page) | unmap=  17.701 ms | rss=  502.44 MiB
run 2/5: mmap=     22.0 us | fill(touch )=    92.930 ms (  2904.1 ns/page) | unmap=  17.845 ms | rss=  502.47 MiB
run 3/5: mmap=     23.0 us | fill(touch )=    86.046 ms (  2688.9 ns/page) | unmap=  16.370 ms | rss=  502.47 MiB
run 4/5: mmap=     22.0 us | fill(touch )=    92.913 ms (  2903.5 ns/page) | unmap=  19.712 ms | rss=  502.47 MiB
run 5/5: mmap=      8.0 us | fill(touch )=    90.540 ms (  2829.4 ns/page) | unmap=  18.228 ms | rss=  502.47 MiB
---- summary (5 runs, 500.00 MiB, touch mode) ----
mmap   : avg     17.200 us | min      8.000 us | max     23.000 us
fill   : avg     91.094 ms | min     86.046 ms | max     93.043 ms
unmap  : avg     17.971 ms | min     16.370 ms | max     19.712 ms
first-touch fault: avg 2846.7 ns/page over 5 run(s)
peak rss: 502.47 MiB

----- Phase A-500: 轻载基线 mem_bench 500M -r 5 -m memset -----
target : 524288000 bytes (500.00 MiB), 32000 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     11.0 us | fill(memset)=   116.493 ms (  4292.1 MiB/s) | unmap=  18.669 ms | rss=  502.38 MiB
run 2/5: mmap=     20.0 us | fill(memset)=   110.456 ms (  4526.7 MiB/s) | unmap=  18.707 ms | rss=  502.41 MiB
run 3/5: mmap=     16.0 us | fill(memset)=   114.847 ms (  4353.6 MiB/s) | unmap=  18.936 ms | rss=  502.41 MiB
run 4/5: mmap=     19.0 us | fill(memset)=   114.640 ms (  4361.5 MiB/s) | unmap=  18.365 ms | rss=  502.41 MiB
run 5/5: mmap=     27.0 us | fill(memset)=   114.301 ms (  4374.4 MiB/s) | unmap=  18.904 ms | rss=  502.41 MiB
---- summary (5 runs, 500.00 MiB, memset mode) ----
mmap   : avg     18.600 us | min     11.000 us | max     27.000 us
fill   : avg    114.147 ms | min    110.456 ms | max    116.493 ms
unmap  : avg     18.716 ms | min     18.365 ms | max     18.936 ms
memset throughput: avg 4381.7 MiB/s over 5 run(s)
peak rss: 502.41 MiB

----- vm_stat (500M 轻载基线后) -----
Pages free:                              213317.
File-backed pages:                        56968.
Pages stored in compressor:               33760.
Compressions:                            370147.

----- B-500: ballast 3600M + 1000M -P 50 -t 600 (累计 4600M, 同 200M 实验重载-3) -----
9393
--- +1000M ---
9418
Pages free:                                4398.
Pages speculative:                          239.
File-backed pages:                        32916.
Anonymous pages:                         196860.
Pages stored in compressor:              158699.
Pages occupied by compressor:             79841.
Decompressions:                          112266.
Compressions:                            497542.

########## Phase C-500: 重载 (4600M ballast, free 4398) 下测 500M ##########
----- mem_bench 500M -r 5 -m touch -----
target : 524288000 bytes (500.00 MiB), 32000 page(s) x 16384 B, mode=touch , runs=5
run 1/5: mmap=     12.0 us | fill(touch )=   208.561 ms (  6517.5 ns/page) | unmap=  19.378 ms | rss=  501.69 MiB
run 2/5: mmap=     55.0 us | fill(touch )=    95.625 ms (  2988.3 ns/page) | unmap=  17.797 ms | rss=  501.80 MiB
run 3/5: mmap=     34.0 us | fill(touch )=   101.681 ms (  3177.5 ns/page) | unmap=  18.848 ms | rss=  501.80 MiB
run 4/5: mmap=     21.0 us | fill(touch )=   107.164 ms (  3348.9 ns/page) | unmap=  20.622 ms | rss=  501.80 MiB
run 5/5: mmap=     24.0 us | fill(touch )=    98.115 ms (  3066.1 ns/page) | unmap=  21.258 ms | rss=  501.80 MiB
---- summary (5 runs, 500.00 MiB, touch mode) ----
mmap   : avg     29.200 us | min     12.000 us | max     55.000 us
fill   : avg    122.229 ms | min     95.625 ms | max    208.561 ms
unmap  : avg     19.581 ms | min     17.797 ms | max     21.258 ms
first-touch fault: avg 3819.7 ns/page over 5 run(s)
peak rss: 502.41 MiB

----- mem_bench 500M -r 5 -m memset -----
target : 524288000 bytes (500.00 MiB), 32000 page(s) x 16384 B, mode=memset, runs=5
run 1/5: mmap=     12.0 us | fill(memset)=   116.955 ms (  4275.1 MiB/s) | unmap=  18.962 ms | rss=  502.38 MiB
run 2/5: mmap=     30.0 us | fill(memset)=   111.624 ms (  4479.3 MiB/s) | unmap=  20.174 ms | rss=  502.41 MiB
run 3/5: mmap=     14.0 us | fill(memset)=   120.215 ms (  4159.2 MiB/s) | unmap=  21.319 ms | rss=  502.41 MiB
run 4/5: mmap=     19.0 us | fill(memset)=   121.361 ms (  4119.9 MiB/s) | unmap=  18.992 ms | rss=  502.41 MiB
run 5/5: mmap=     14.0 us | fill(memset)=   117.598 ms (  4251.8 MiB/s) | unmap=  21.719 ms | rss=  502.41 MiB
---- summary (5 runs, 500.00 MiB, memset mode) ----
mmap   : avg     17.800 us | min     12.000 us | max     30.000 us
fill   : avg    117.551 ms | min    111.624 ms | max    121.361 ms
unmap  : avg     20.233 ms | min     18.962 ms | max     21.719 ms
memset throughput: avg 4257.1 MiB/s over 5 run(s)
peak rss: 502.41 MiB

----- vm_stat (500M 重载测量后) -----
Pages free:                               34438.
File-backed pages:                        30598.
Pages stored in compressor:              189999.
Pages occupied by compressor:             99181.
Decompressions:                          119690.
Compressions:                            550737.

########## Phase D-500: 释放 + 恢复复测 ##########
zsh:1: no matches found: [m]em_stress
Pages free:                              260589.
File-backed pages:                        30729.
Pages stored in compressor:               30349.

----- 恢复后复测 mem_bench 500M -r 3 -m touch -----
target : 524288000 bytes (500.00 MiB), 32000 page(s) x 16384 B, mode=touch , runs=3
run 1/3: mmap=     11.0 us | fill(touch )=    90.145 ms (  2817.0 ns/page) | unmap=  18.017 ms | rss=  502.38 MiB
run 2/3: mmap=     10.0 us | fill(touch )=    98.647 ms (  3082.7 ns/page) | unmap=  18.167 ms | rss=  502.41 MiB
run 3/3: mmap=     17.0 us | fill(touch )=    92.975 ms (  2905.5 ns/page) | unmap=  17.740 ms | rss=  502.41 MiB
---- summary (3 runs, 500.00 MiB, touch mode) ----
mmap   : avg     12.667 us | min     10.000 us | max     17.000 us
fill   : avg     93.922 ms | min     90.145 ms | max     98.647 ms
unmap  : avg     17.975 ms | min     17.740 ms | max     18.167 ms
first-touch fault: avg 2935.1 ns/page over 3 run(s)
peak rss: 502.41 MiB

----- 恢复后复测 mem_bench 500M -r 3 -m memset -----
target : 524288000 bytes (500.00 MiB), 32000 page(s) x 16384 B, mode=memset, runs=3
run 1/3: mmap=     13.0 us | fill(memset)=   117.374 ms (  4259.9 MiB/s) | unmap=  17.806 ms | rss=  502.31 MiB
run 2/3: mmap=     19.0 us | fill(memset)=   115.892 ms (  4314.4 MiB/s) | unmap=  18.547 ms | rss=  502.34 MiB
run 3/3: mmap=     21.0 us | fill(memset)=   113.271 ms (  4414.2 MiB/s) | unmap=  17.954 ms | rss=  502.34 MiB
---- summary (3 runs, 500.00 MiB, memset mode) ----
mmap   : avg     17.667 us | min     13.000 us | max     21.000 us
fill   : avg    115.512 ms | min    113.271 ms | max    117.374 ms
unmap  : avg     18.102 ms | min     17.806 ms | max     18.547 ms
memset throughput: avg 4329.5 MiB/s over 3 run(s)
peak rss: 502.34 MiB

########## 500M 补测分析总结 ##########
# 协议与 200M 实验一致; 重载态 = 3600M+1000M -P 50 (同 200M 的重载-3), 测量起点 free=4398 (略深于上午的 7753)
#
# | 状态     | ballast | free页  | mmap µs* | touch 首触 (纯申请)   | vs轻载 | memset 耗时 | vs轻载 | memset MiB/s |
# |----------|---------|---------|----------|-----------------------|--------|-------------|--------|--------------|
# | 轻载基线 | 0       | 213317  | 17.9     | 91.1ms (2846.7 ns/页) | —      | 114.1ms     | —      | 4381.7       |
# | 重载     | 4600M   | 4398    | 23.5     | 122.2ms (3819.7 ns/页)| +34.2% | 117.6ms     | +3.0%  | 4257.1       |
# | 恢复复测 | 0       | 260589  | 15.2     | 93.9ms (2935.1 ns/页) | +3.1%  | 115.5ms     | +1.2%  | 4329.5       |
# * mmap 两模式均值; 500M 下全部测量 8~55µs 仍与压力无关 (重载 touch 模式一轮 55µs 为离群值)
#
# 冷首轮: 重载 run1 = 6517.5 ns/page (208.6ms, +129%); run2~5 回落 2988~3349 (系统提前回收)
# 测量期间 compressions +53195 次 (500M×5 轮强制回收, 200M 实验同期约 +15k)
#
# 跨尺寸对比 (轻载单页成本完全一致 -> 横向可比):
#   轻载 ns/页:     200M 2804  ≈  500M 2847   (线性, 无尺寸惩罚)
#   重载均值 ns/页: 200M 4111 (+46.6%)  >  500M 3820 (+34.2%)
#   重载冷首轮:     200M 6995 (+150%)   ≈  500M 6518 (+129%)
#   memset 重载:    200M +13.6%      >  500M +3.0%
#   unmap: 200M ~7.7ms / 500M ~18ms (~38µs/MiB 线性, 与压力基本无关)
#
# 解释: 500M 单轮填充时长为 200M 的 2.5 倍, 给 pageout 更多"边填边回收"的重叠时间,
#   稳态均值反而更低; 但第一轮冷分配要付清全部欠账, 尖峰比例与 200M 相当。

########## run_case.sh 跨平台用例验证 (2026-10-09 15:0x, iOS 真机 dash) ##########
----- iOS 轻载 touch 64M x3 -----
CASE ts=2026-10-09T15:03:47+0800 platform=Darwin kernel=Darwin-20.6.0 arch=iPhone13,3 size_mib=64 mode=touch runs=3 ballast_mib=0 tool=mem_bench
PHASE name=case_start ts=2026-10-09T15:03:47+0800 pre_total=5735 pre_free=3851 pre_avail=-1 pre_free_spec=3958 pre_file=561 pre_anon=363 pre_swap_comp=380 pre_comp_occ=139
REC run=1 size_mib=64 mode=touch ballast_mib=0 mmap_us=10.0 fill_ms=10.484 unit=2559.6 unit_label=ns/page unmap_ms=0.977 rss_mib=66.39 pre_total=5735 pre_free=3851 pre_avail=-1 pre_free_spec=3958 pre_file=561 pre_anon=363 pre_swap_comp=380 pre_comp_occ=139
REC run=2 size_mib=64 mode=touch ballast_mib=0 mmap_us=15.0 fill_ms=11.910 unit=2907.7 unit_label=ns/page unmap_ms=1.538 rss_mib=66.39 pre_total=5735 pre_free=3905 pre_avail=-1 pre_free_spec=4012 pre_file=562 pre_anon=361 pre_swap_comp=380 pre_comp_occ=139
REC run=3 size_mib=64 mode=touch ballast_mib=0 mmap_us=10.0 fill_ms=12.455 unit=3040.8 unit_label=ns/page unmap_ms=0.903 rss_mib=66.42 pre_total=5735 pre_free=3905 pre_avail=-1 pre_free_spec=4012 pre_file=562 pre_anon=361 pre_swap_comp=380 pre_comp_occ=139
SUMMARY runs=3 fill_ms_avg=11.616 fill_ms_min=10.484 fill_ms_max=12.455

----- iOS 轻载 memset 64M x2 -----
CASE ts=2026-10-09T15:03:50+0800 platform=Darwin kernel=Darwin-20.6.0 arch=iPhone13,3 size_mib=64 mode=memset runs=2 ballast_mib=0 tool=mem_bench
PHASE name=case_start ts=2026-10-09T15:03:50+0800 pre_total=5735 pre_free=3905 pre_avail=-1 pre_free_spec=4012 pre_file=562 pre_anon=361 pre_swap_comp=380 pre_comp_occ=139
REC run=1 size_mib=64 mode=memset ballast_mib=0 mmap_us=10.0 fill_ms=14.360 unit=4456.8 unit_label=MiB/s unmap_ms=1.182 rss_mib=66.39 pre_total=5735 pre_free=3904 pre_avail=-1 pre_free_spec=4011 pre_file=562 pre_anon=361 pre_swap_comp=380 pre_comp_occ=139
REC run=2 size_mib=64 mode=memset ballast_mib=0 mmap_us=11.0 fill_ms=15.565 unit=4111.8 unit_label=MiB/s unmap_ms=1.326 rss_mib=66.39 pre_total=5735 pre_free=3904 pre_avail=-1 pre_free_spec=4011 pre_file=562 pre_anon=363 pre_swap_comp=380 pre_comp_occ=139
SUMMARY runs=2 fill_ms_avg=14.962 fill_ms_min=14.360 fill_ms_max=15.565

----- iOS 重载 touch 64M x2 -b 1200 -----
CASE ts=2026-10-09T15:03:52+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-09T15:03:52+0800 pre_total=5735 pre_free=3904 pre_avail=-1 pre_free_spec=4011 pre_file=562 pre_anon=362 pre_swap_comp=380 pre_comp_occ=139
PHASE name=ballast_start ts=2026-10-09T15:03:52+0800 ballast_mib=1200 hold_s=900 pre_total=5735 pre_free=3904 pre_avail=-1 pre_free_spec=4011 pre_file=562 pre_anon=362 pre_swap_comp=380 pre_comp_occ=139
PHASE name=ballast_settled ts=2026-10-09T15:04:05+0800 waited_s=12 pid=10805 pre_total=5735 pre_free=2654 pre_avail=-1 pre_free_spec=2761 pre_file=562 pre_anon=1559 pre_swap_comp=380 pre_comp_occ=139
REC run=1 size_mib=64 mode=touch ballast_mib=1200 mmap_us=11.0 fill_ms=12.157 unit=2968.0 unit_label=ns/page unmap_ms=5.487 rss_mib=66.42 pre_total=5735 pre_free=2651 pre_avail=-1 pre_free_spec=2759 pre_file=562 pre_anon=1559 pre_swap_comp=380 pre_comp_occ=139
REC run=2 size_mib=64 mode=touch ballast_mib=1200 mmap_us=11.0 fill_ms=9.648 unit=2355.5 unit_label=ns/page unmap_ms=4.325 rss_mib=66.39 pre_total=5735 pre_free=2653 pre_avail=-1 pre_free_spec=2760 pre_file=562 pre_anon=1562 pre_swap_comp=380 pre_comp_occ=139
SUMMARY runs=2 fill_ms_avg=10.902 fill_ms_min=9.648 fill_ms_max=12.157
PHASE name=ballast_released ts=2026-10-09T15:04:06+0800 post_total=5735 post_free=3854 post_avail=-1 post_free_spec=3961 post_file=562 post_anon=361 post_swap_comp=380 post_comp_occ=139

其他原始数据下载: 设备功能验证日志 · panic 事故记录

平台须知

源码

以下为完整源码(与服务端文件一致),右上角可复制 / 下载原始文件:

mem_stress.c内存加压工具 · 497 行 下载
/*
 * 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_bench.cmmap/memset 耗时统计 · 439 行 下载
/*
 * mem_bench.c — 匿名页申请耗时基准 (iOS / HarmonyOS / macOS / Linux 通用)
 *
 * 功能:
 *   按入参大小反复 mmap 一块匿名私有内存, 分别统计:
 *     - mmap   耗时 (建立映射本身, 通常只有 µs 级)
 *     - fill   耗时 (memset 整块填充 —— 真正触发缺页 + 写入的主成本;
 *                     或 touch 模式: 每页写 1 字节, 专门测首次触页缺页成本)
 *     - unmap  耗时 (munmap 释放)
 *   每轮结束后做轻量读回校验 (防止填充被优化掉, 并确认内存真实可读),
 *   最后输出 avg/min/max 汇总与吞吐率。
 *
 * 用法:
 *   mem_bench [-r n] [-m memset|touch|none] [-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
}

/*
 * 严格的整数解析 (拒绝垃圾输入)。注: 不用 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] [-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"
        "  -q        quiet: per-run lines suppressed\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",
        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;
    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, "-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++) {
        /* --- 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();

        /* --- 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("\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;
}
Makefile跨平台构建 (host / ios / ohos) 下载
# 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
README.md完整文档 (Markdown 源) 下载
# mem_alloc_bench

两个纯 POSIX C 的匿名内存工具, 单文件、零依赖, 同一份源码可在
**iOS**(越狱设备) / **HarmonyOS** / macOS / Linux 上编译运行:

| 源码 | 功能 |
|---|---|
| `mem_stress.c` | 按入参大小 mmap 匿名页并逐页触页提交, 进行**内存加压**; 可选周期性重触对抗压缩回收, 可选保持时长后释放 |
| `mem_bench.c`  | 按入参大小申请匿名页, 分轮统计 **mmap / 填充(memset) / munmap** 耗时, 输出 avg/min/max 与吞吐 |

## 用法

### mem_stress — 内存加压

```
usage: mem_stress [-f|-R|-P n] [-k sec] [-t sec] [-qv] <size>

  <size>    bytes of anonymous memory to commit
            e.g. 268435456 | 100K | 512M | 1500M | 2G | 1T
            (binary units: K=1024, M=1024^2, G=1024^3; B/iB suffix ok)
  -f        full fill: memset the whole region (default: 1 byte per page)
  -R        fill with pseudo-random bytes: incompressible ballast that
            the compressor cannot absorb (WARNING: see 压力实验准则)
  -P n      partial random fill: first n% of each page random, rest zero
            (n=50 approximates real app data, ~2:1 WKdm ratio)
  -k sec    re-touch all pages every <sec> while holding, to defeat
            compressor/zram reclaim (0 = off, default off)
  -t sec    hold duration after touch, then release
            (default: hold until SIGINT/SIGTERM)
  -q        quiet (suppress progress lines)
  -v        verbose progress
```

```console
$ ./mem_stress 512M -t 2
alloc  : 536870912 bytes (512.00 MiB), 32768 page(s) x 16384 B, mmap 0.040 ms
touch  :  10% (    51.20 MiB) elapsed 0.01 s
...
touch  :  99% (   506.88 MiB) elapsed 0.09 s
touch  : touched 32768 pages (512.00 MiB) in 0.092 s [5580.1 MiB/s, 357.1K pages/s]
rss    : resident 229.52 MiB, phys_footprint 513.22 MiB
hold   : 2.000 s
release: munmap ok, total 2.120 s, peak rss 233.16 MiB
```

要点:

- 默认**每页写 1 字节**触发缺页提交物理内存 (加压的标准姿势); `-f` 改为整块 memset。
- 默认**一直持有**直到 `Ctrl-C` / SIGTERM; `-t` 指定保持秒数。
- `-k sec` 周期性向所有页写入新值: 迫使被 XNU compressor / Linux zram 压缩的页
  解压回 RAM、干净页重新变脏, 用于长时间维持真实压力。
- iOS/macOS 上额外打印 `phys_footprint` (jetsam 的判定口径, = 驻留脏页 + 已压缩脏页)。
  上面真实样例里 RSS 229 MiB < footprint 513 MiB, 就是压缩器已经吃掉一部分页的
  直接证据 —— 加压时看 footprint 才是真实压力。

### mem_bench — mmap / memset 耗时统计

```
usage: mem_bench [-r n] [-m mode] [-q] <size>

  <size>    bytes of anonymous memory per run
            e.g. 268435456 | 100K | 512M | 2G (binary units, B/iB suffix ok)
  -r n      repeat runs (default 3)
  -m mode   fill mode after mmap (default memset):
              memset  memset the whole region (faults + bandwidth)
              touch   write 1 byte per page (first-touch fault cost, ns/page)
              none    mmap only (mapping setup/teardown cost)
  -q        quiet: per-run lines suppressed
```

```console
$ ./mem_bench 512M -r 3
target : 536870912 bytes (512.00 MiB), 32768 page(s) x 16384 B, mode=memset, runs=3
run 1/3: mmap=      3.0 us | fill(memset)=    82.261 ms (  6221.3 MiB/s) | unmap=   8.328 ms | rss=  513.44 MiB
run 2/3: mmap=      5.0 us | fill(memset)=    55.465 ms (  9231.9 MiB/s) | unmap=   5.724 ms | rss=  512.94 MiB
run 3/3: mmap=     12.0 us | fill(memset)=    42.753 ms ( 11975.5 MiB/s) | unmap=   5.822 ms | rss=  512.94 MiB
---- summary (3 runs, 512.00 MiB, memset mode) ----
mmap   : avg      7.667 us | min      3.000 us | max     12.000 us
fill   : avg     60.160 ms | min     42.753 ms | max     82.261 ms
unmap  : avg      6.658 ms | min      5.724 ms | max      8.328 ms
memset throughput: avg 9143.6 MiB/s over 3 run(s)
peak rss: 512.78 MiB
```

要点:

- mmap 本身只是建立映射 (µs 级); 真正的成本在填充阶段 (缺页 + 写入),
  两者分开计时, 恰好回答"申请"与"提交"各自的耗时。
- `-m touch` 每页只写 1 字节, 输出 **ns/page** —— 首次触页缺页的单页成本,
  可直接对比 iOS (16K 页) 与鸿蒙 (4K 页) 平台。
- 每轮 memset 使用不同填充值 (0x11/0x22/…), 并做读回校验, 防止零页/去重类优化。

## 编译

### 本机 (macOS / Linux)

```console
$ make                     # 或 cc -O2 -o mem_stress mem_stress.c
$ ./mem_stress 512M -t 5
$ ./mem_bench 256M
```

### iOS (越狱设备, 无需 Xcode 工程)

```console
$ make ios                 # 产物在 ios/, arm64 Mach-O
# 等价命令:
$ xcrun -sdk iphoneos clang -arch arm64 -O2 -miphoneos-version-min=12.0 \
      -o ios/mem_stress mem_stress.c
```

部署到设备 (iproxy 2222 → 设备 22, root/alpine):

```console
$ scp -P 2222 ios/mem_stress ios/mem_bench root@localhost:/var/mobile/ios_memslice/
$ ssh -p 2222 root@localhost \
    '/usr/bin/ldid -S /var/mobile/ios_memslice/mem_stress && chmod +x /var/mobile/ios_memslice/mem_stress'
$ ssh -p 2222 root@localhost '/var/mobile/ios_memslice/mem_stress 200M -t 10'
```

注意 (2026-09 越狱设备实测的坑):

- **必须在设备端 `ldid -S` 签名**; Mac 侧 ldid 签完传上去会被 SIGKILL (rc=137)。
- **覆盖已有二进制必须先 `rm` 再传**, 直接覆盖同样触发 rc=137。
- `scp`/sftp 上传后必须 `chmod +x`。
- 本工具只需普通 mmap, 无需任何 entitlements。
- 非越狱 iOS 无法直接运行 CLI 二进制; 同一份源码可嵌进 App target 编译执行。

### HarmonyOS (DevEco Studio 的 OHOS NDK, musl libc)

```console
$ make ohos               # 产物在 ohos/
# 等价命令 (OHOS_NATIVE 按实际安装路径调整):
$ OHOS_NATIVE=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native
$ $OHOS_NATIVE/llvm/bin/clang --target=aarch64-linux-ohos \
      --sysroot=$OHOS_NATIVE/sysroot -O2 -o ohos/mem_stress mem_stress.c
```

部署到设备 (hdc, 设备需处于开发者模式):

```console
$ hdc file send ohos/mem_stress /data/local/tmp/mem_stress
$ hdc file send ohos/mem_bench  /data/local/tmp/mem_bench
$ hdc shell "chmod +x /data/local/tmp/mem_stress /data/local/tmp/mem_bench"
$ hdc shell "/data/local/tmp/mem_stress 200M -t 10"
$ hdc shell "/data/local/tmp/mem_bench 256M -r 5"
```

注意:

- 必须用 **OHOS NDK 的 clang** (链接 musl, 动态链接器
  `/lib/ld-musl-aarch64.so.1`); 普通 glibc 交叉链产物在设备上无法运行。
- 模拟器 (x86_64) 把 target 改为 `x86_64-linux-ohos`:
  `make ohos OHOS_TRIPLE=x86_64-linux-ohos`。
- 鸿蒙内核同样存在 zram 压缩回收, 长时间加压建议加 `-k 5` 之类的重触选项。

## 验证状态 (2026-10 实测)

| 平台 | 编译 | 运行 |
|---|---|---|
| macOS (Apple Silicon, 16K 页) | ✅ `-Wall -Wextra` 零警告 | ✅ 全模式 + SIGTERM 干净退出 |
| iOS arm64 (`xcrun -sdk iphoneos clang`) | ✅ 零警告, 纯 libSystem 符号, 无需 entitlements | 待设备 (需越狱部署, 见上) |
| Linux aarch64 glibc (VM) | ✅ 零警告 | ✅ 全模式 (4K 页, /proc RSS) |
| Linux aarch64 **musl** (VM, 鸿蒙同族 libc) | ✅ 零警告 | ✅ 全模式 |
| HarmonyOS 真机 (OHOS NDK) | 命令已给出, 本机无 NDK/hdc | 待设备 |

## 已知平台行为

- **单位均为二进制**: K=1024, M=1024², G=1024³, T=1024⁴; 大小自动向上取整到页。
- **iOS 单进程匿名保留上限 ~4GiB** (iOS 14.8 实测: mmap 4G 成功 / 8G 失败),
  超限 mmap 直接报错; 更常见的是压力过大先被 **jetsam SIGKILL** —— 属预期行为。
- iOS 极限加压 (free 页 < 500) 有内核 panic 风险, 手机上测试请留余量。
- macOS/Linux 上两者同样可用 (`make` 即可), 便于先在本机验证行为再到手机上跑。

## 压力实验准则 (2026-10-09 iPhone 12 Pro 6GB 实测教训)

在 iOS 上做内存压力实验, ballast 的**可压缩性**决定系统行为, 三种模式天差地别:

| ballast 模式 | WKdm 压缩比 | 系统行为 |
|---|---|---|
| 默认触页 / `-f` 模式填充 | ~1000:1 | 被 compressor 零成本吸收, **无法形成持续压力** |
| `-P 50` (半随机) | ~2:1~2.5:1 | **真实负载模型**: 压缩器持续工作但有余量, 系统优雅降级 |
| `-R` (全随机) | ~1:1 | 压缩器被塞满, 守护进程饿死 → **watchdog 内核 panic** |

实测事故: 4600M `-R` ballast → mediaserverd 180 秒无 checkin →
userspace watchdog panic → 设备重启丢越狱 (panic 日志归档于 raw-data/)。
做压力实验请用 `-P 50` + `-t` 死开关 + free 页实时监控 (保持 > 2000)。