Ruby is a great programming language, but unfortunately it does have some problems when using on Windows. One of it’s biggest drawbacks is it’s slowness when loading files. This is also slower than it ought to be on Unix platforms, but not as slow as on Windows. Thankfully there is some work going on to make it faster in future versions. It’s already possible to make it faster yourself!

Benchmarks

I will conduct two types of benchmarks. Firstly i’m gonna create an empty Rails 3 project and see how much time would it boot up. Secondly there is a project called measurements which allows to perform some operations on your Ruby and one of these are benchmarks for loading files. Let’s see how it goes for Ruby 1.9.2, 1.9.3 from RubyInstaller and patched 1.9.3.

# ruby 1.9.2p136 C:\>rails new empty-192 -d sqlite3 # ... Using rails (3.1.1) C:\empty-192>timer ruby script\rails runner "" user system total real 0.000000 0.016000 0.016000 ( 28.808579) C:\measurements>rci bench core_require_empty && rci bench core_require_nested ruby 1.9.2p136 (2010-12-25) [i386-mingw32] Rehearsal ------------------------------------------------------ core_require_empty 2.917000 7.722000 10.639000 ( 10.755556) -------------------------------------------- total: 10.639000sec user system total real core_require_empty 3.167000 7.815000 10.982000 ( 11.133547) ruby 1.9.2p136 (2010-12-25) [i386-mingw32] Rehearsal ------------------------------------------------------- core_require_nested 3.556000 8.736000 12.292000 ( 12.453693) --------------------------------------------- total: 12.292000sec user system total real core_require_nested 3.526000 9.188000 12.714000 ( 13.157714) # ruby 1.9.3p0 from RubyInstaller C:\>rails new empty-193-p0 -d sqlite3 # ... Using rails (3.1.1) C:\empty-193-p0>timer ruby script\rails runner "" user system total real 0.000000 0.000000 0.000000 ( 17.425940) C:\measurements>rci bench core_require_empty && rci bench core_require_nested ruby 1.9.3p0 (2011-10-30) [i386-mingw32] Rehearsal ------------------------------------------------------ core_require_empty 3.120000 9.937000 13.057000 ( 13.278704) -------------------------------------------- total: 13.057000sec user system total real core_require_empty 3.167000 9.578000 12.745000 ( 13.231747) ruby 1.9.3p0 (2011-10-30) [i386-mingw32] Rehearsal ------------------------------------------------------- core_require_nested 3.822000 11.373000 15.195000 ( 15.853803) --------------------------------------------- total: 15.195000sec user system total real core_require_nested 3.822000 11.653000 15.475000 ( 16.779881) # Ruby 1.9.3p0 patched C:\>rails new empty-193-faster -d sqlite3 # ... Using rails (3.1.1) C:\empty-193-faster>timer ruby script\rails runner "" user system total real 0.000000 0.000000 0.000000 ( 9.184323) C:\measurements>rci bench core_require_empty && rci bench core_require_nested ruby 1.9.3p0 (2011-11-08 revision 33661) [i386-mingw32] Rehearsal ------------------------------------------------------ core_require_empty 2.620000 5.616000 8.236000 ( 9.144510) --------------------------------------------- total: 8.236000sec user system total real core_require_empty 2.247000 5.179000 7.426000 ( 7.930452) ruby 1.9.3p0 (2011-11-08 revision 33661) [i386-mingw32] Rehearsal ------------------------------------------------------- core_require_nested 2.496000 5.522000 8.018000 ( 8.123630) ---------------------------------------------- total: 8.018000sec user system total real core_require_nested 2.652000 5.960000 8.612000 ( 8.861500)

These benchmarks show that Ruby 1.9.2 is really slow and patched 1.9.3 got about 50% performance boost compared to regular 1.9.3. That’s something to be happy about! I’m using average laptop PC which means that if you have a more decent hardware then the results might be very different from me.

Prerequisites

The following components are needed to speedup your Ruby:

Ruby 1.9.3 from RubyInstaller

Devkit

Git

If you’d like to skip all that hassle of building all the things yourself then you can download already prebuilt patched Ruby versions too! Read the “Faster Way” part about that below.

Get Fenix

Fenix is a Ruby extension written by Luis Lavena. Luis is a really helpful and great guy, at least when it comes to Ruby on Windows. He is part of the Ruby core team, is the main man behind RubyInstaller and has created many nice libraries like sqlite3-ruby, rake-compiler and many others.

Back to Fenix. It changes File.expand_path method to be faster. All the 50% speedup seen from the benchmarks were coming from this change since this method is called more than once when loading files. Pretty impressive or rather sad bottleneck in Ruby. Get the Fenix extension, compile and benchmark it:

C:\>git clone git://github.com/luislavena/fenix.git Initialized empty Git repository in C:/fenix/.git/ # ... C:\>devkit\devkitvars.bat Adding the DevKit to PATH... C:\fenix>rake compile # ... install -c tmp/i386-mingw32/fenix/1.9.3/fenix.so lib/fenix.so C:\fenix>rake bench File.expand_path: 10000 times. user system total real Ruby '' 0.297000 0.593000 0.890000 ( 0.884027) Fenix '' 0.062000 0.000000 0.062000 ( 0.066004) Ruby '.' 0.312000 0.546000 0.858000 ( 0.876040) Fenix '.' 0.062000 0.000000 0.062000 ( 0.060000) Ruby 'foo', 'bar' 0.312000 0.983000 1.295000 ( 1.328060) Fenix 'foo', 'bar' 0.078000 0.000000 0.078000 ( 0.106003) Ruby '', 'C:/' 0.031000 0.000000 0.031000 ( 0.031000) Fenix '', 'C:/' 0.047000 0.000000 0.047000 ( 0.050000) Ruby 'foo', 'C:/' 0.171000 1.965000 2.136000 ( 2.248100) Fenix 'foo', 'C:/' 0.047000 0.000000 0.047000 ( 0.054003) Ruby '~' 0.530000 0.562000 1.092000 ( 1.111063) Fenix '~' 0.094000 0.000000 0.094000 ( 0.100006) Ruby '~/foo' 0.468000 0.827000 1.295000 ( 1.321076) Fenix '~/foo' 0.078000 0.000000 0.078000 ( 0.076005) Ruby 'foo/' 0.280000 0.624000 0.904000 ( 0.917052) Fenix 'foo/' 0.078000 0.000000 0.078000 ( 0.071004) Ruby '~', 'C:/Foo' 0.375000 0.717000 1.092000 ( 1.110063) Fenix '~', 'C:/Foo' 0.093000 0.000000 0.093000 ( 0.090005) Ruby long_path 0.203000 0.016000 0.219000 ( 0.214006) Fenix long_path 0.203000 0.000000 0.203000 ( 0.207005) Ruby long_path, 'rel' 0.421000 0.734000 1.155000 ( 1.237058) Fenix long_path, 'rel' 0.234000 0.000000 0.234000 ( 0.227013) Ruby long_path, 'C:/Foo' 0.375000 1.981000 2.356000 ( 2.470126) Fenix long_path, 'C:/Foo' 0.187000 0.000000 0.187000 ( 0.184010) Ruby full_long_path 0.156000 0.015000 0.171000 ( 0.170010) Fenix full_long_path 0.156000 0.000000 0.156000 ( 0.183011) Ruby to_path 0.234000 0.359000 0.593000 ( 0.619036) Fenix to_path 0.062000 0.000000 0.062000 ( 0.071004) Ruby to_path, 'rel' 0.468000 0.999000 1.467000 ( 1.455083) Fenix to_path, 'rel' 0.094000 0.000000 0.094000 ( 0.091006) Ruby to_path, 'C:/Foo' 0.390000 2.262000 2.652000 ( 2.844161) Fenix to_path, 'C:/Foo' 0.078000 0.000000 0.078000 ( 0.090005) Ruby full_to_path 0.390000 1.950000 2.340000 ( 2.526143) Fenix full_to_path 0.031000 0.000000 0.031000 ( 0.044003)

Not bad benchmark results.

Patching & Compiling Ruby

Unfortunately only File.expand_path method is faster when using this extension, but there’s Ruby’s require and load methods, which also execute expand_path, but they will do so by using internal C expand_path function instead. Solution for that problem is to patch Ruby code to use Fenix.expand_path internally also! First step would be to clone Ruby itself (make sure that line endings are not converted by Git):

C:\>git config --global core.autocrlf false && git clone -b ruby_1_9_3 git@github.com:ruby/ruby.git Initialized empty Git repository in C:/ruby/.git/ # ...

Apply the patch created by Luis:

C:\ruby>git clone git://gist.github.com/1360449.git expand_path-patch C:\ruby>git apply -v expand_path-patch\0001-make-load-and-require-use-file-expand_path-and-file-realpath.diff Checking patch file.c... Checking patch load.c... Applied patch file.c cleanly. Applied patch load.c cleanly. C:\ruby>git status # On branch ruby_1_9_3 # Changed but not updated: # (use "git add ..." to update what will be committed) # (use "git checkout -- ..." to discard changes in working directory) # # modified: file.c # modified: load.c

`git status` shows us that file.c and load.c files are modified. It’s time to start compiling Ruby itself. Best way to do that would be to use RubyInstaller itself (how ironic, we’re using Ruby to build Ruby). Make sure to disable ANSICON too since it is known to cause problems during the compilation process:

C:\rubyinstaller>..\ansicon\x64\ansicon.exe -u C:\rubyinstaller>rake ruby19 NOGEMS=1 NOTK=1 LOCAL=..\ruby

This takes some time and if the process raises any errors, just try again. If there’s still errors don’t hesitate to write to RubyInstaller mailing list or contact Luis directly. He’s very helpful, as i already stated above.

After the process is done “install” new Ruby with Fenix and make sure that it’s used by setting environment variables too:

C:\rubyinstaller>xcopy /E /I sandbox\ruby19_mingw \ruby193-faster # ... C:\rubyinstaller>cd ..\fenix C:\fenix>xcopy /E /I lib \ruby193-faster\lib\ruby\1.9.1\i386-mingw32 lib\fenix.so lib\fenix\replace.rb 2 File(s) copied C:\fenix>set PATH=c:\ruby193-faster\bin;%PATH% C:\fenix>set RUBYOPT=-rfenix/replace

Make sure that the PATH and RUBYOPT environment variables are set permanently. And try it out if it’s faster for you too!

Faster Way

In case you don’t want to spend your time cloning, compiling and testing then there is a faster way to boost your Ruby. There exists a project called The Code Shop, which tries to solve the Ruby performance problem on Windows. It makes different builds available to download, each experimenting with different set of patches. I can recommend tcs-ruby193_require_winio_fenix-20111113 because it seems to be the fastest. Here are some benchmark results:

# tcs-ruby193_winio_fenix-20111113 C:\empty-193>timer ruby script\rails runner "" user system total real 0.000000 0.000000 0.000000 ( 6.802955) C:\measurements>rci bench core_require_empty && rci bench core_require_nested tcs-ruby 1.9.3p0 (2011-11-08) [i386-mingw32] Rehearsal ------------------------------------------------------ core_require_empty 2.044000 2.589000 4.633000 ( 4.726269) --------------------------------------------- total: 4.633000sec user system total real core_require_empty 1.778000 2.746000 4.524000 ( 4.652266) tcs-ruby 1.9.3p0 (2011-11-08) [i386-mingw32] Rehearsal ------------------------------------------------------- core_require_nested 2.075000 3.603000 5.678000 ( 5.779272) ---------------------------------------------- total: 5.678000sec user system total real core_require_nested 2.028000 3.526000 5.554000 ( 5.791128) # tcs-ruby193_require_winio_fenix-20111113 C:\empty-193>timer ruby script\rails runner "" user system total real 0.000000 0.000000 0.000000 ( 5.635120) C:\measurements>rci bench core_require_empty && rci bench core_require_nested tcs-ruby 1.9.3p0 (2011-11-08) [i386-mingw32] Rehearsal ------------------------------------------------------ core_require_empty 1.497000 2.948000 4.445000 ( 4.571255) --------------------------------------------- total: 4.445000sec user system total real core_require_empty 1.326000 3.151000 4.477000 ( 4.577208) tcs-ruby 1.9.3p0 (2011-11-08) [i386-mingw32] Rehearsal ------------------------------------------------------- core_require_nested 1.732000 3.650000 5.382000 ( 5.529468) ---------------------------------------------- total: 5.382000sec user system total real core_require_nested 1.716000 3.604000 5.320000 ( 5.437311)

Fenix is already precompiled into these builds. You still need to compile and install Fenix and set PATH and RUBYOPT environments as described above.

In Conclusion

The main point of this post is that things aren’t always as rosy as they could be, but it’s possible to make them better. It is really valuable that you give feedback to projects like The Code Shop (e.g. what are your benchmark results) so the final result could get better in the future without any additional hassle for everyone. I hope that you can now start your engines much faster!