如何使用Rust提高Ruby性能

来源:互联网 发布:没卡怎么在淘宝买东西 编辑:程序博客网 时间:2024/06/06 12:27

摘要:Ruby是一种简单快捷面向对象的脚本语言,而Rust是一种系统编程语言,它有着惊人的运行速度,能够防止段错误,并保证线程安全。本文作者以项目为例,结合大量的编程代码描述了如何借助Rust语言提高Ruby的性能,以下是译文。

几年前,在我的Rails(提供一个纯Ruby的开发环境)应用程序里发现了一些被调用数千次的方法,占了网站页面加载时间的30%以上。这些方法都完全地专注于文件路径名。

除此之外,我还看过一篇博客写“拯救Ruby的Rust”,这表明可以用Rust编写执行慢的Ruby代码,让Ruby变得更快。Rust还提供了一种安全、快速、高效的编写代码的方法。在用Rust语言重写了我的Rails站点上一些效率较低的方法之后,网站页面加载速度比以前快了33%以上。

如果想了解通过FFI集成Rust,那么建议看看上文中提到的那篇博客(“拯救Ruby的Rust”)。当前这篇文章的重点是分享我在过去两年整合Ruby和Rust所得到的经验教训。当方法被调用数千次,它的性能稍有改进都会对项目有很大的影响。

入门

这篇文章的相关代码可以在GitHub上看到,如果打算开始了解Rust和Ruby项目,可以创建一个ffi_example 项目,并把下面的代码添加到Cargo.toml文件:

[lib]name = "ffi_example"crate-type = ["dylib"][dependencies]array_tool = "*"libc = "0.2.33"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

添加如下代码到ffi_example.gemspec文件:

spec.add_dependency "bundler", "~> 1.12"  spec.add_dependency "rake", "~> 12.0"  spec.add_dependency "ffi", "~> 1.9"  spec.add_development_dependency "minitest", "~> 5.10"  spec.add_development_dependency "minitest-reporters", "~> 1.1"  spec.add_development_dependency "benchmark-ips", "~> 2.7.2"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

由于构建的库需要在客户端系统上使用FFI,所以最好将FFI,Rake和Bundler作为常规依赖项包含在内。

用于这篇文章的例子,可以从FasterPath的repo历史记录中获取basename方法代码,与File.basename进行比较。

请记住,Ruby用C语言实现了这个功能,所以这并不是你通常会用Rust重写的那种方法。大多数FasterPath都会为Pathname类重写Ruby代码,这就是可以显著提高性能的地方。我们正在使用File.basename作为纯粹的比较基准。
  • 1
  • 2
  • 3
  • 4

为了简单起见,将所有的Rust代码放入到src/lib.rs。这是一个用Rust编写的basename代码副本(可以复制和粘贴它;在这里不再介绍它是如何工作的):

mod rust {  extern crate array_tool;  use self::array_tool::string::Squeeze;  use std::path::MAIN_SEPARATOR;  static SEP: u8 = MAIN_SEPARATOR as u8;  pub fn extract_last_path_segment(path: &str) -> &str {    // Works with bytes directly because MAIN_SEPARATOR is always in the ASCII 7-bit range so we can    // avoid the overhead of full UTF-8 processing.    // See src/benches/path_parsing.rs for benchmarks of different approaches.    let ptr = path.as_ptr();    let mut i = path.len() as isize - 1;    while i >= 0 {      let c = unsafe { *ptr.offset(i) };      if c != SEP { break; };      i -= 1;    }    let end = (i + 1) as usize;    while i >= 0 {      let c = unsafe { *ptr.offset(i) };      if c == SEP {        return &path[(i + 1) as usize..end];      };      i -= 1;    }    &path[..end]  }  pub fn basename(pth: &str, ext: &str) -> String {    // Known edge case    if &pth.squeeze("/")[..] == "/" { return "/".to_string(); }    let mut name = extract_last_path_segment(pth);    if ext == ".*" {      if let Some(dot_i) = name.rfind('.') {        name = &name[0..dot_i];      }    } else if name.ends_with(ext) {      name = &name[..name.len() - ext.len()];    };    name.to_string()  }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

这个案例是为了模仿File.basename返回结果的方式而写的。这里唯一需要注意的是在basename方法开始的地方的边界情况。这有效地使方法在迭代给定输入的时间加倍,并且应该重构到现有的系统中去。

感谢Gleb Mazovetskiy,extract_last_path_segment是一个有效的贡献。这个方法在其它方法中使用,并在边界情况已知之前就实现了。在本文的后面,将讨论有和没有边界情况下的基准性能的细节。






宁波整形医院www.lyxcl.org

原创粉丝点击