2 分钟

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_CFUNCVM_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_methoddef 具有相同的性能! 通过 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 上慢的人,以下是我在研究期间发现的相关行:

如果你知道原因,请分享在互联网上。

最新文章

分类

关于

A young developer who loves Linux.