☰
mruby-benchmark 完全指南:在 mruby 嵌入式环境中测量执行时间与内存分配
2026/10/12 3:23:06 网站建设 项目流程
  • 编程语言
  • 语言运行时
  • 编译器
  • 解释器
  • 嵌入式

【免费下载链接】mruby

Lightweight Ruby

项目地址:https://gitcode.com/gh_mirrors/mr/mruby
点击查看免费下载

本文以 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):

  1. 在块执行前后各调用一次ObjectSpace.count_objects(仅当ObjectSpace常量存在时);
  2. 分别对返回哈希的values求和,得到存活对象总数;
  3. objects_allocated = end_objects - start_objects即差值;
  4. 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 } end

report的实现(mrblib/benchmark.rb)会:

  1. 调用Benchmark.measure { yield }测量;
  2. 把 label 写入Tms对象(measure本身构造的Tms不带 label,report会重建一个带 label 的副本);
  3. 按构造Report时传入的width对标签做右补空格对齐;
  4. 通过$stdout.print输出label_str加tms.to_s;
  5. 把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

项目地址:https://gitcode.com/gh_mirrors/mr/mruby
点击查看免费下载

相关推荐

上一篇:输入格式全解析:如何用wav、numpy数组与JSONL清单驱动diar_streaming_sortformer_4spk-v2说话人分离
下一篇:用Clojure MCP创建专属AI智能体:Agent配置系统实战教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询