Speeding up Ruby gems compilation (installation)

August 1, 2021 -
Tags: linux, quick, ruby

I’m very annoyed by how slow installing Ruby gems is, even with a large number of available hardware threads (and even with many Bundler jobs configured).

I had a quick look a the Rubygems/Bundler source code today, and I’ve found a quick win.

Content:

Context

Ruby gems can have native extensions, which require compilation.

When installing via Bundler, it’s very common to install with the --jobs option, which launches each gem installation in a separate job (with the disclaimer that Ruby concurrency is limited).

Such option does not apply though, to the native extensions compilation; gems with relatively large C code may take a relatively considerable time to compile.

I’ve inspected the source code, and tried to find out where make (which is used to compile) was invoked, and found it here:

# simplified version

make_program = ENV['MAKE'] || ENV['make'] || $1
make_program = Shellwords.split(make_program)

destdir = 'DESTDIR=%s' % ENV['DESTDIR']

['clean', '', 'install'].each do |target|
  cmd = [
    *make_program,
    destdir,
    target,
  ].reject(&:empty?)

  run(cmd, results, "make #{target}".rstrip, make_dir)
end

Here we observe that make is invoked without the jobs argument (-j), therefore, it’s run by default in single-thread.

The tweak

Since the make executable is configurable (also) via MAKE environment variable, we just use it:

# Run with all the available threads!
#
MAKE="make -j $(nproc)" bundle install

On our application, this sped up the bundle install operation from 2’41” to 59”!

(This works on Linux/macOS; Windows user may need to adjust the variable)

Conclusion

Due to history, parallelism is not pervasive in the Ruby culture. Let’s fix that! 🙂

Happy gem installation!