- 编程语言
- 语言运行时
- 编译器
- 解释器
- 嵌入式
【免费下载链接】mruby
Lightweight Ruby
本文以 mruby 官方 mrbgem mruby-benchmark 为线索,系统讲解如何在轻量级 Ruby 实现 mruby 中完成代码执行时间测量、多实现对比基准(benchmark)与内存分配统计。读者将掌握Benchmark.measure、Benchmark.realtime、Benchmark.bm三个核心入口的完整用法,理解Benchmark::Tms与Benchmark::Report的数据结构,并能基于仓库源码理解时间与内存指标的真实来源及其平台限制。
概览:为资源受限环境设计的轻量级基准工具
mruby-benchmark是 mruby 的官方基准测试与性能剖析(profiling)gem,提供简洁、轻量的基准能力,用于测量 mruby 应用中的执行时间与内存使用情况。它专门面向嵌入式系统和资源受限环境(embedded systems and resource-constrained environments)设计,API 与 CRuby 标准库的benchmark保持一致——按 mrbgem.rake 中的注释,Benchmark.measure的构建方式与 CRuby 完全相同:CPU 时间来自Process.times,真实时间来自对单调时钟(monotonic clock)的Process.clock_gettime调用。
整个 gem 的实现非常精简:核心逻辑全部位于 mrblib/benchmark.rb(约 130 行纯 Ruby 代码),由Benchmark模块、Benchmark::Tms类和Benchmark::Report类三部分组成,没有 C 扩展代码,依赖关系也集中在 mruby-process、mruby-objectspace、mruby-sprintf 与 mruby-io 四个 gem 上。
安装与依赖
在 build_config.rb 中启用
在 mruby 中,gem 通过构建配置启用。编辑你的build_config.rb,添加一行:
conf.gem :core => 'mruby-benchmark'需要注意:mruby-benchmark并不在任何默认 gembox(如 default.gembox、stdlib.gembox)中,必须手动加入。conf.gem :core => ...表示使用 mruby 仓库内自带的核心 gem,而不是从外部 gem 仓库拉取。
传递依赖
在 mrbgem.rake 中声明了四个依赖:
| 依赖 gem | 作用 | 说明 |
|---|---|---|
mruby-process | 提供时间测量原语 | Process.times与Process.clock_gettime(Process::CLOCK_MONOTONIC)均由它提供 |
mruby-objectspace | 提供内存剖析 | 内存统计基于ObjectSpace.count_objects |
mruby-sprintf | 提供格式化 | Tms#to_s与Tms#format的格式化输出依赖%格式化 |
mruby-io | 提供标准输出 | Report#report与Benchmark.bm通过$stdout打印结果 |
其中mruby-process是时间测量的核心。根据 mruby-process 的 README,Process.clock_gettime、Process.times与Process::CLOCK_MONOTONIC均由该 gem 提供,而它本身又依赖mruby-signal与mruby-struct(Process::Tms是mruby-struct定义的Struct)。mruby-process 的时间读取通过其平台端口(port)实现:POSIX 端口在 ports/posix/clock_hal.c 中使用clock_gettime(2)(缺失时回退到gettimeofday(2))读取墙钟,并用getrusage(2)(缺失时回退到times(2)换算)读取 CPU 时间累计值;Windows 端口在 ports/win/clock_hal.c 中则全部通过 Win32 API 调用实现。
无 Float 构建的限制
一个重要的构建前提是:mruby-benchmark需要带 Float 支持的构建。因为Process.times返回浮点时间,且Tms#to_s内部使用%f浮点格式化——在没有 Float 的构建(MRB_NO_FLOAT)中这两者都会直接抛异常。这一点在 README 的 Limitations 一节中明确列出。
API 详解
Benchmark 模块
Benchmark模块是基准操作的唯一入口,提供三个模块级方法。
Benchmark.measure { block }→ Benchmark::Tms
测量给定代码块的执行时间,返回包含完整时间信息的Benchmark::Tms对象:
result = Benchmark.measure do # code to benchmark 1000.times { "string interpolation: #{42}" } end puts result # Prints formatted timing information从 mrblib/benchmark.rb 的源码可以看到measure的实现思路:在yield前后各取一次Process.times与Process.clock_gettime(Process::CLOCK_MONOTONIC),然后分别做差值。四个 CPU 时间分量(utime、stime、cutime、cstime)来自Process.times的差值,real来自单调时钟读数的差值。
Benchmark.realtime { block }→ Float
只返回真实(墙钟)时间,单位为秒的浮点数,适合快速计时:
time = Benchmark.realtime do sleep(0.1) end puts "Took #{time} seconds" # => "Took 0.100... seconds"源码实现(mrblib/benchmark.rb)比measure更简单:取两次Process.clock_gettime(Process::CLOCK_MONOTONIC)之差,不关心 CPU 时间。
Benchmark.bm(label_width = 0) { |x| ... }
执行带格式化的多项基准对比,输出各标签对齐的结果:
Benchmark.bm(10) do |x| x.report("array:") { 1000.times { [1, 2, 3, 4, 5] } } x.report("hash:") { 1000.times { {a: 1, b: 2, c: 3} } } x.report("string:") { 1000.times { "hello" * 100 } } end输出示例:
user system total real array: 0.010000 0.000000 0.010000 ( 0.012345) hash: 0.015000 0.000000 0.015000 ( 0.016789) string: 0.008000 0.000000 0.008000 ( 0.009012)参数label_width指定标签列宽,大于 0 时会在打印头部前输出对应宽度的空格,保证标签列对齐。从源码(mrblib/benchmark.rb)可见bm会先打印" user system total real"表头,然后创建Benchmark::Report实例并yield给用户代码块,最后返回该Report对象(其中保留了所有Tms结果,可通过report.results取回)。
Benchmark.measure(memory: true) { block }→ Benchmark::Tms
measure支持memory:关键字参数。传入memory: true时,除了测量时间,还会统计对象分配数量与估算内存占用:
result = Benchmark.measure(memory: true) do array = [] 1000.times { |i| array << i } end puts "Objects allocated: #{result.objects}" puts "Memory used: #{result.memory} bytes"内存统计的实现细节(mrblib/benchmark.rb):
- 在块执行前后各调用一次
ObjectSpace.count_objects(仅当ObjectSpace常量存在时); - 分别对返回哈希的
values求和,得到存活对象总数; objects_allocated = end_objects - start_objects即差值;memory_allocated = objects_allocated * 40,即按“mruby 平均对象开销约 40 字节”的近似估算。
这里 40 字节是源码中的固定估算值,注释明确写着 “Average object overhead in mruby (approximate)”。因此内存结果是估算值,可能并不反映真实的堆使用情况。
注意:ObjectSpace.count_objects来自mruby-objectspacegem。其 C 实现在 src/mruby_objectspace.c,会遍历整个 mruby 对象空间,按类型(:TOTAL、:FREE、:T_OBJECT、:T_CLASS、:T_STRING、:T_ARRAY等)统计存活对象数量并返回一个 Hash。
Benchmark::Tms 类
Benchmark::Tms持有一次测量的结果。构造时接收(utime, stime, cutime, cstime, real, label = nil, objects = nil, memory = nil)(mrblib/benchmark.rb)。
属性(Attributes)
| 属性 | 含义 | 类型 |
|---|---|---|
utime | 用户态 CPU 时间(秒) | Float |
stime | 系统态 CPU 时间(秒) | Float |
cutime | 块内被回收(reaped)的子进程的用户态 CPU 时间 | Float |
cstime | 块内被回收的子进程的系统态 CPU 时间 | Float |
real | 真实墙钟时间(秒) | Float |
objects | 分配的对象数量(启用内存跟踪时) | Integer |
memory | 分配的内存字节数(启用内存跟踪时) | Integer |
方法
total→ Float
返回总 CPU 时间,即utime + stime + cutime + cstime(mrblib/benchmark.rb):
result = Benchmark.measure { heavy_computation } puts "Total CPU time: #{result.total} seconds"to_s→ String
返回格式化字符串表示,格式为"%10.6f %10.6f %10.6f (%10.6f)\n" % [@utime, @stime, total, @real],即依次是 utime、stime、total、real,各占 10 位宽、6 位小数,real 用括号包裹:
result = Benchmark.measure { sleep(0.1) } puts result.to_s # => " 0.000000 0.000000 0.000000 ( 0.100123)"format(format_str)→ String
按格式字符串输出结果。格式化通过字符串替换实现(mrblib/benchmark.rb),支持以下说明符:
| 说明符 | 含义 |
|---|---|
%u | 用户态 CPU 时间 |
%s | 系统态 CPU 时间 |
%t | 总 CPU 时间 |
%r | 真实时间 |
%o | 分配的对象数(启用内存跟踪时) |
%m | 分配的内存(启用内存跟踪时) |
%n | 标签名(label) |
result = Benchmark.measure { computation } puts result.format("Real: %rs, CPU: %ts") # => "Real: 0.123s, CPU: 0.100s"从源码实现可以看出%o、%m、%n只在对应字段非空时才会被替换(if @objects、if @memory、if @label守卫),未启用内存跟踪或未设置标签时这些说明符会原样保留。
Benchmark::Report 类
Benchmark::Report在Benchmark.bm内部使用,负责格式化的逐项报告输出。
report(label = "") { block }
在bm块内执行并报告单个基准项:
Benchmark.bm do |x| x.report("first test") { code1 } x.report("second test") { code2 } endreport的实现(mrblib/benchmark.rb)会:
- 调用
Benchmark.measure { yield }测量; - 把 label 写入
Tms对象(measure本身构造的Tms不带 label,report会重建一个带 label 的副本); - 按构造
Report时传入的width对标签做右补空格对齐; - 通过
$stdout.print输出label_str加tms.to_s; - 把
Tms存入@results数组并返回它。
因此bm块内每次x.report(...)的返回值就是对应的Tms,可以收集起来用于断言或后续分析;Report#results则返回全部结果的数组。
实战示例
基础计时
require 'benchmark' # Simple timing time = Benchmark.realtime do sum = 0 1000000.times { |i| sum += i } end puts "Calculation took #{time} seconds" # Detailed timing result = Benchmark.measure do arr = (1..10000).to_a arr.sort! end puts result对比不同实现
require 'benchmark' Benchmark.bm(15) do |x| x.report("Array#each:") do arr = (1..1000).to_a sum = 0 arr.each { |n| sum += n } end x.report("Array#inject:") do arr = (1..1000).to_a arr.inject(0) { |sum, n| sum + n } end x.report("Numeric#times:") do sum = 0 1000.times { |n| sum += n } end end内存剖析
require 'benchmark' # Track memory allocation result = Benchmark.measure(memory: true) do strings = [] 1000.times { |i| strings << "string_#{i}" } end puts "Execution time: #{result.real}s" puts "Objects created: #{result.objects}" puts "Memory allocated: #{result.memory} bytes"在测试中做性能断言
# In test files assert('String concatenation performance') do time = Benchmark.realtime do 1000.times { "hello" + "world" } end # Assert it completes within reasonable time assert_true time < 0.1, "String concat should be fast" end在 mruby 的测试框架(mruby-test)中,assert/assert_true的语法与 CRuby 的 minitest 一致,可以直接把Benchmark.realtime的返回值写进断言。测试断言需要谨慎:在时间片粒度较大的平台上,过小的耗时可能被时钟粒度吞掉,导致时间读数恰好为 0。
实现原理:时间测量
README 的 Implementation Notes 明确说明:mruby-benchmark的测量方式与 CRuby 的benchmark完全一致:
- 真实时间= 两次
Process.clock_gettime(Process::CLOCK_MONOTONIC)读数之差。由于使用的是单调时钟(monotonic clock,只增不减、不受系统时间调整影响),即使块执行期间 NTP 校准了墙钟,也不会体现在测量结果中; - 四个 CPU 时间(utime、stime、cutime、cstime)= 两次
Process.times读数之差。
两者都来自mruby-process,而mruby-process通过其平台端口读取。各平台能区分的内容可参考 mruby-process 的 README:POSIX 端口下Process.times优先用getrusage(2)读取(微秒级粒度),缺失时回退到times(2)按 tick 换算;Windows 端口则使用 Win32 调用,cutime/cstime恒为 0(因为 Win32 不报告已回收子进程的 CPU 时间,CRuby 的 Windows 构建同样如此)。
从 mruby-process 的源码注释(include/process_hal.h)还可以看到一层设计细节:Process.times跨 HAL 边界时以四个int64_t秒/纳秒时钟读数传递,从不以 tick 或 Float 形式传递,到公共层才统一转换为 Float;Process::CLOCK_MONOTONIC是 mruby 自己的常量值(Linux 上是 1、macOS 上是 6),而非直接使用平台值。
实现原理:内存跟踪
内存剖析基于ObjectSpace.count_objects(mruby-objectspace提供)来跟踪对象分配:通过块前后对象总数之差得到分配数量,再乘以固定的 40 字节平均对象开销估算内存占用。README 强调“内存大小估算基于典型对象开销,可能并非所有平台都精确”。
因此在实际使用中需要注意:
objects统计的是“当前存活对象的净增量”,块内分配但块内释放的对象不会计入;memory是objects * 40的线性估算,不反映真实堆使用(例如字符串内部缓冲、数组容量扩张等都不在估算范围内);- 如果构建中没有
mruby-objectspace,measure(memory: true)会静默跳过内存统计(源码中通过Object.const_defined?(:ObjectSpace)守卫),objects与memory保持为nil。
平台限制与注意事项
README 的 Limitations 一节列出了以下关键限制:
| 限制 | 说明 |
|---|---|
cutime/cstime仅统计块内等待(reaped)的子进程 | 与Process.times语义一致;在 Windows 上恒为 0 |
| CPU 时间粒度取决于平台时钟 | getrusage(2)下为微秒级;times(2)或 Win32 下为一个 tick |
真实时间需要宿主机支持CLOCK_MONOTONIC | 极少数不支持它的 POSIX 主机上,measure和realtime会抛出Errno::EINVAL,与 CRuby 的benchmark行为一致 |
| 需要带 Float 的构建 | Process.times与Tms#to_s中的%f在无 Float 构建下都会抛异常 |
| 内存测量为估算值 | 可能无法反映真实堆使用情况 |
| GC 活动影响计时 | 基准测量期间发生的 GC 可能影响时间结果 |
第 3 条在 mruby-process 的 POSIX 端口中有对应实现佐证:ports/posix/clock_hal.c 用MRB_PROCESS_HAVE_CLOCK_MONOTONIC宏探测宿主是否提供CLOCK_MONOTONIC,缺失时clock_gettime调用失败并最终映射为Errno::EINVAL。
官方测试用例佐证
仓库自带的测试文件 test/benchmark.rb 覆盖了本 gem 的全部核心行为,是理解各 API 语义的最佳参考:
Benchmark.measure返回Tms对象且各字段均为 Float,无子进程时cutime/cstime为0.0;- 构造耗时块后
real > 0,验证墙钟时间确实在增长; - “CPU 时间计数”用例(第 51-68 行)用
Process.times自检循环忙等,直到 CPU 读数前进,从而兼容 tick 粒度的times(2)平台——这段注释解释了为何基准断言要绕过时钟粒度问题; - “CPU 时间只计块内”用例(第 70-87 行)在测量前先消耗 CPU,再测量近乎空的块,验证 CPU 时间是两次读数之差而非累计值;
Tms#total用Tms.new(1.0, 2.0, 3.0, 4.0, 5.0).total == 10.0验证求和逻辑;Tms#format验证%u/%s/%t/%r以及带内存属性的%o/%m/%n替换;- 内存跟踪用例用
skip unless Object.const_defined?(:ObjectSpace)处理未启用mruby-objectspace的构建; Benchmark.bm用例通过把$stdout置空来抑制输出,并验证report.results的长度与Tms类型。
延伸:仓库自带的 benchmark 目录与rake benchmark
除了mruby-benchmarkgem,仓库根目录下还有一套独立的基准测试基础设施,二者可以搭配使用,理解它有助于你设计自己的基准方案:
- benchmark/ 目录存放
bm_*.rb脚本,如 bm_fib.rb(递归 Fibonacci)、bm_ao_render.rb(环境光遮蔽渲染,含大量对象创建与浮点运算)、bm_hash_access.rb(对比字符串键与符号键的哈希查找)等,均是可独立运行的耗时型负载; - tasks/benchmark.rake 定义了
rake benchmark任务:用系统time -p对每个bm_*.rb脚本重复运行 4 次(MRuby::Build::BENCHMARK_REPEAT = 4),取 user+sys 的均值/最小值/最大值写入.dat文件,再调用 gnuplot 依据 benchmark/plot.gpl 生成 PNG 柱状图; - 该任务通过构建目标的
benchmark_enabled?开关控制(lib/mruby/build.rb),且只对非 internal 目标生效。
这套基础设施衡量的是“整个进程从启动到结束”的耗时,粒度与mruby-benchmark的块内测量不同,但目标一致:为 mruby 的性能演进提供可复现的量化依据。对日常开发而言,mruby-benchmarkgem 是嵌入到应用代码与测试中做定向测量的更合适选择。
总结
mruby-benchmark以约 130 行纯 Ruby 实现了与 CRubybenchmark语义对齐的三大能力:measure(完整时间信息 + 可选内存跟踪)、realtime(快速墙钟计时)、bm(格式化多项目对比)。其时间测量完全建立在mruby-process的Process.times与Process.clock_gettime(CLOCK_MONOTONIC)之上,内存测量建立在mruby-objectspace的ObjectSpace.count_objects之上。理解这些底层来源,就能准确预判各平台上的粒度、限制与误差来源——这正是嵌入式与资源受限环境中做可靠性能测量的关键。
- 编程语言
- 语言运行时
- 编译器
- 解释器
- 嵌入式
【免费下载链接】mruby
Lightweight Ruby
相关推荐
H2O 内置 mruby-json 完全指南:在 mruby 处理器中解析与生成 JSON
H2O 内置 mruby json 完全指南:在 mruby 处理器中解析与生成 JSON mruby json 是面向 mruby(轻量级嵌入式 Ruby)的
后端网络8B参数大模型革命:DeepSeek-R1-0528-Qwen3-8B如何重塑中小企业AI应用格局
8B参数大模型革命:DeepSeek R1 0528 Qwen3 8B如何重塑中小企业AI应用格局 导语 2025年,大模型不再是大型企业的专属"高端资源",D
编程语言语言运行时编译器解释器嵌入式mruby 编译与交叉编译完全指南:从构建配置到嵌入集成(nghttp2 内置 mruby 实战)
mruby 编译与交叉编译完全指南:从构建配置到嵌入集成(nghttp2 内置 mruby 实战) 导读 mruby 是一套轻量级、可嵌入的 Ruby 实现,其
可观测性日志分析云原生流处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考