令我惊讶的是 Ruby
num >> 1
比 num/2
慢,因为我一直认为 bit operators
应该比普通运算符更快。
# ruby 3.0.3
require "benchmark/ips"
Benchmark.ips do |bm|
num = 1000000
bm.report("num/2") do
num/2
end
bm.report("num >> 1") do
num >> 1
end
bm.compare!
end
# num/2: 16045584.8 i/s
# num >> 1: 14591335.3 i/s - 1.10x slower
Benchmark.ips do |bm|
num = 1000000
bm.report("num * 2") do
num * 2
end
bm.report("num << 1") do
num << 1
end
bm.compare!
end
# num * 2: 18252815.2 i/s
# num << 1: 14289700.6 i/s - 1.28x slower
我没有使用 benchmark/ips 进行测试,因为它不是核心 gem,也不是 Ruby 标准库的一部分。然而,基于使用标准 Benchmark#bmbm 方法的一些重构测试,似乎您的一些结果可能会因某些版本的 Ruby 中字符串赋值的开销而产生偏差,因为我注意到一个更常见的差异为多次运行仅需 1 微秒,且字符串不会冻结。在启用 YJIT 的情况下,我也得到了稍微更加一致的结果,因为这似乎会增加一些轻微的开销。然而,无论是否启用 YJIT,我都没有注意到 Ruby 3.3.0 在有或没有冻结字符串的情况下存在material差异。 考虑以下内容,它在 Ruby 3.3.0 上使用冻结字符串:
# frozen_string_literal: true
require "benchmark"
NUM = 500_000_000
Benchmark.bmbm do |x|
x.report("NUM / 2") { NUM/2 }
x.report("NUM * 2") { NUM * 2 }
x.report("NUM >> 1") { NUM >> 1 }
x.report("NUM << 1") { NUM << 1 }
end
即使在5 亿次迭代中
,在启用了 YJIT 和冻结字符串的 Ruby 3.3.0 上,在丢弃排练值后,我始终得到以下正负一微秒的系统时间:
user system total real
NUM / 2 0.000001 0.000000 0.000001 ( 0.000001)
NUM * 2 0.000001 0.000000 0.000001 ( 0.000001)
NUM >> 1 0.000001 0.000000 0.000001 ( 0.000001)
NUM << 1 0.000001 0.000000 0.000001 ( 0.000001)
大约一微秒的挂钟时间中的 5 亿次迭代对于我来说对于任何实际目的来说似乎都足够快,并且当总时间在合理精度内基本上无法测量时担心每秒迭代似乎是不必要的优化。
如果您确实需要更快的性能,请考虑使用线程、ractor 或某些针对并发性进行优化的多核方法。
parallelgem 可以在主线 Ruby 上提供帮助,其他引擎可能会提供其他替代方案。 JRuby 和 TruffleRuby 在此基准测试中表现更差
在 TruffleRuby 24.0.0 上进行同样的 5 亿次迭代,无需任何代码更改或特定于引擎的优化,结果是:
user system total real
NUM / 2 0.000038 0.000025 0.000063 ( 0.000012)
NUM * 2 0.000036 0.000003 0.000039 ( 0.000013)
NUM >> 1 0.000039 0.000002 0.000041 ( 0.000011)
NUM << 1 0.000024 0.000007 0.000031 ( 0.000010)
对于任何实际目的来说,这仍然足够快,但确实强调了问题在于底层 VM 或运算符的实现,而不是 Ruby 语言本身。
优化 IRB 会议以进行基准测试
请注意,如果您在 IRB 内运行而不是调用文件,则应按以下方式启动 IRB:
RUBYOPT="--enable=frozen_string_literal" irb --nocolorize
确保默认启用冻结字符串并避免 ANSI 颜色影响性能。