
Ruby元编程性能
2015 年,Pragtob 写了一篇关于 Ruby 元编程的贴子。 好吧,您可能想知道为什么我要谈论五年前写的博客,在一个计算机科学正在以不断加快的速度发展的时代。 我搜索了它,因为在工作中我们遇到了这个问题。
在工作中我们使用 Cocoapods 来进行依赖管理。在 Pod 很少的项目中,您不会注意到 Cocoapods 的速度。但是,假设有 600 多个Pod会是什么样的呢?仅 Cocoapods 生成 XCode 项目文件的时间就可能是半分钟!我们设法将性能问题缩小到 this 非常 Ruby 文件。从表面上看,它是无害的。有一个辅助方法可以使用元编程生成其他方法,省去了手动编写排序结果、记忆等事情的麻烦……但是,也有一些方法仅返回一个常量字符串,但也使用了这个辅助方法。使用 define_method
定义的方法开销很小,但是当该方法被调用数万次时,开销足够大,可以使用一些Monkey Patching来优化它。我修补了那些使用 define_method
但简单地返回一个常量字符串的方法。使用简单的 def
重新定义这些方法导致了 5 秒的优化。 5s 听起来可能不多,但在编程世界中,它就像 3 年!
通常,故事到这里就结束了。但是,我决定深入挖掘。你看,我可以从使用 def
重新定义方法中挤出 5 秒。 Ruby 解释器不能首先做类似的事情吗?我查看了 Ruby 解释器源代码,发现用普通 def
定义的方法和用 define_method
定义的方法确实区别对待。对于def
,源代码是这里。 define_method
源代码是 here。主要区别在于def
函数的类型是VM_METHOD_TYPE_CFUNC
,而define_method
的类型是VM_METHOD_TYPE_BMETHOD
。不幸的是,我没有发现为什么VM_METHOD_TYPE_CFUNC
比VM_METHOD_TYPE_BMETHOD
快,我也不知道为什么它们是不同的类型。在网上快速搜索也无济于事。所以我只好放弃了这件事。然而,我确实通过这个过程对 Ruby 有了更多的了解。我现在知道默认类是 kernel
,你在任何类之外定义的所有东西都会进入内核。而类类型实际上也是……一个类! Ruby 中的符号之所以唯一,是因为 rb_intern
将字符串转换为内部的 ID
类型,相同的字符串转换为相同的 ID。 Ruby 维护一个中央函数表,以确定您正在调用哪个函数、函数可见性和它们所属的类与函数本身一起存储。
测试
好吧,如果您对 Ruby 3.0 中的元编程性能感兴趣,在 Pragtob 的帖子发布 5 年后。 这是测试它的代码。 你需要 benchmark-ips gem 来运行它。
require 'benchmark/ips'
require 'set'
class BuildSettings
def self.define_method_cocoapods(method_name, build_setting: false, &implementation)
(@build_settings_names ||= Set.new) << method_name.to_s.upcase if build_setting
raw_method_name = :"_raw_#{method_name}"
define_method(raw_method_name, &implementation)
private(raw_method_name)
define_method(method_name) do
retval = send(raw_method_name)
retval
end
end
define_method_cocoapods :meta_fn, build_setting: true do
'A simple string'
end
def direct_fn
'A simple string'
end
define_method_cocoapods :meta_complex_fn, build_setting: true do
10.times do
foo = 'String'
end
end
def direct_complex_fn
10.times do
foo = 'String'
end
end
end
Benchmark.ips do |x|
build_settings = BuildSettings.new
x.report('direct') { build_settings.direct_fn }
x.report('meta') { build_settings.meta_fn }
x.report('complex meta') { build_settings.meta_complex_fn }
x.report('complex direct') { build_settings.direct_complex_fn }
x.compare!
end
在我的电脑上,这是结果:
Warming up --------------------------------------
direct 1.020M i/100ms
meta 436.276k i/100ms
complex meta 118.988k i/100ms
complex direct 143.041k i/100ms
Calculating -------------------------------------
direct 10.511M (± 3.3%) i/s - 53.022M in 5.050267s
meta 4.372M (± 3.0%) i/s - 22.250M in 5.093786s
complex meta 1.196M (± 2.6%) i/s - 6.068M in 5.079384s
complex direct 1.458M (± 2.1%) i/s - 7.295M in 5.005728s
Comparison:
direct: 10511119.1 i/s
meta: 4372098.4 i/s - 2.40x (± 0.00) slower
complex direct: 1457994.7 i/s - 7.21x (± 0.00) slower
complex meta: 1195505.1 i/s - 8.79x (± 0.00) slower
似乎5年来没有任何进展?
并非如此,我们在 2015 年还没有 TruffleRuby! 在 TruffleRuby 上,define_method
和 def
具有相同的性能! 通过 TruffleRuby 运行此测试产生以下输出:
Warming up --------------------------------------
direct 317.071M i/100ms
meta 334.676M i/100ms
complex meta 328.865M i/100ms
complex direct 331.058M i/100ms
Calculating -------------------------------------
direct 3.306B (± 4.9%) i/s - 16.488B in 5.001178s
meta 3.305B (± 3.2%) i/s - 16.734B in 5.068132s
complex meta 3.267B (± 3.6%) i/s - 16.443B in 5.040465s
complex direct 3.314B (± 3.3%) i/s - 16.553B in 5.000735s
Comparison:
complex direct: 3313971061.1 i/s
direct: 3306325701.2 i/s - same-ish: difference falls within error
meta: 3305189666.5 i/s - same-ish: difference falls within error
complex meta: 3266924543.9 i/s - same-ish: difference falls within error
而且它也比 CRuby 快很多! 然而令我沮丧的是,Cocoapods 在 TruffleRuby 上运行得不够好。 我想我们永远不会有好东西。
对于那些想确切知道为什么 define_method
在 CRuby 上慢的人,以下是我在研究期间发现的相关行:
- https://github.com/ruby/ruby/blob/b5e94916bfb6aca65211047dcc4c55481c5b30a2/proc.c#L2241 define_method -> rb_add_func
- https://github.com/ruby/ruby/blob/53a094ea45567cdfb7b8aab2f3dde96a15b89565/class.c#L1799 rb_define_method -> rb_add_method_cfunc
- https://github.com/ruby/ruby/blob/a0a8f2abf533702b2cd96e79f700ce5b9cd94f50/vm_method.c#L322 rb_add_method_cfunc -> rb_add_func
- https://github.com/ruby/ruby/blob/a0a8f2abf533702b2cd96e79f700ce5b9cd94f50/vm_method.c#L907 rb_add_method -> rb_method_entry_make
- https://github.com/ruby/ruby/blob/a0a8f2abf533702b2cd96e79f700ce5b9cd94f50/vm_method.c#L762 rb_method_entry_make -> rb_method_definition_create
- https://github.com/ruby/ruby/blob/a0a8f2abf533702b2cd96e79f700ce5b9cd94f50/vm_method.c#L555 rb_method_definition_create
如果你知道原因,请分享在互联网上。