├── README.md ├── Rakefile ├── benchmark.rb └── ext └── zig_rb ├── .gitignore ├── build.zig ├── extconf.rb └── src └── main.zig /README.md: -------------------------------------------------------------------------------- 1 | ## What's this? 2 | 3 | This repo contains an experiment of building a Ruby extension with Zig programming language. 4 | It implements a slightly altered version of [100 doors](https://rosettacode.org/wiki/100_doors) from Rosetta Code. 5 | 6 | These are results of benchmarks on my machine (Thinkpad T14): 7 | 8 | ``` 9 | Warming up -------------------------------------- 10 | Ruby 924.000 i/100ms 11 | Zig 13.885k i/100ms 12 | Calculating ------------------------------------- 13 | Ruby 12.745k (±22.1%) i/s - 60.984k in 5.052486s 14 | Zig 233.096k (± 0.1%) i/s - 1.166M in 5.003698s 15 | 16 | Comparison: 17 | Zig: 233095.9 i/s 18 | Ruby: 12744.7 i/s - 18.29x (± 0.00) slower 19 | ``` 20 | 21 | However, if you edit `extconf.rb` to use `-Drelease-fast` flag, the difference is much bigger: 22 | 23 | ``` 24 | Warming up -------------------------------------- 25 | Ruby 1.020k i/100ms 26 | Zig 171.828k i/100ms 27 | Calculating ------------------------------------- 28 | Ruby 10.289k (± 2.2%) i/s - 52.020k in 5.058112s 29 | Zig 2.833M (± 6.3%) i/s - 14.262M in 5.059011s 30 | 31 | Comparison: 32 | Zig: 2833045.1 i/s 33 | Ruby: 10289.0 i/s - 275.35x (± 0.00) slower 34 | ``` 35 | 36 | Please note that this is only one benchmark, not much science behind it. It doesn't mean you will always get 37 | 270x speed boost on just rewriting in Zig. 38 | 39 | ## How to run it 40 | 41 | 1. You need fairly recent version of Zig, which at this time means a version built from git 42 | 2. Clone this repo 43 | 3. Run `rake benchmark` 44 | 45 | Note that it likely only works on Linux, I'd gladly 46 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :compile_ext do 2 | cd "ext/zig_rb" do 3 | ruby "extconf.rb" 4 | sh "make" 5 | end 6 | end 7 | 8 | task :benchmark => :compile_ext do 9 | ruby "benchmark.rb" 10 | end -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | gem "benchmark-ips" 6 | end 7 | 8 | file = case RbConfig::CONFIG["host_os"] 9 | when /linux/ then "libzig_rb.so" 10 | when /darwin|mac os/ then "libzig_rb.dylib" 11 | else 12 | raise "Unknown OS" 13 | end 14 | 15 | require File.join("./ext/zig_rb/zig-out/lib", file) 16 | 17 | def hundred_doors(passes) 18 | doors = Array.new(101, false) 19 | passes.times do |i| 20 | i += 1 21 | (i..100).step(i) do |d| 22 | doors[d] = !doors[d] 23 | end 24 | end 25 | # dropping first one as it does not count 26 | doors.drop(1).count {|d| d} 27 | end 28 | 29 | puts "Ruby: #{hundred_doors(100)}, Zig: #{ZigRb.new.hundred_doors(100)}" 30 | 31 | require "benchmark/ips" 32 | zig = ZigRb.new 33 | 34 | Benchmark.ips do |x| 35 | x.report("Ruby") { hundred_doors(100) } 36 | x.report("Zig") { zig.hundred_doors(100) } 37 | x.compare! 38 | end 39 | -------------------------------------------------------------------------------- /ext/zig_rb/.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | Makefile 4 | -------------------------------------------------------------------------------- /ext/zig_rb/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | // Standard release options allow the person running `zig build` to select 5 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 6 | const target = b.standardTargetOptions(.{}); 7 | const optimize = b.standardOptimizeOption(.{}); 8 | 9 | const lib = b.addSharedLibrary(.{ .name = "zig_rb", .root_source_file = b.path("src/main.zig"), .version = .{ .major = 0, .minor = 0, .patch = 1 }, .optimize = optimize, .target = target }); 10 | 11 | // Ruby stuff 12 | const ruby_libdir = std.posix.getenv("RUBY_LIBDIR") orelse ""; 13 | lib.addLibraryPath(std.Build.LazyPath{ .cwd_relative = ruby_libdir }); 14 | const ruby_hdrdir = std.posix.getenv("RUBY_HDRDIR") orelse ""; 15 | lib.addIncludePath(std.Build.LazyPath{ .cwd_relative = ruby_hdrdir }); 16 | const ruby_archhdrdir = std.posix.getenv("RUBY_ARCHHDRDIR") orelse ""; 17 | lib.addIncludePath(std.Build.LazyPath{ .cwd_relative = ruby_archhdrdir }); 18 | 19 | lib.linkSystemLibrary("c"); 20 | b.installArtifact(lib); 21 | 22 | // const main_tests = b.addTest("src/main.zig"); 23 | // main_tests.setBuildMode(mode); 24 | 25 | // const test_step = b.step("test", "Run library tests"); 26 | // test_step.dependOn(&main_tests.step); 27 | } 28 | -------------------------------------------------------------------------------- /ext/zig_rb/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | 3 | makefile_path = File.join("Makefile") 4 | config = RbConfig::CONFIG 5 | File.open(makefile_path, "w") do |f| 6 | f.puts <<~MFILE 7 | all: 8 | \tRUBY_LIBDIR=#{config["libdir"]} RUBY_HDRDIR=#{config["rubyhdrdir"]} RUBY_ARCHHDRDIR=#{config["rubyarchhdrdir"]} zig build -Doptimize=ReleaseFast 9 | MFILE 10 | end 11 | -------------------------------------------------------------------------------- /ext/zig_rb/src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ruby = @cImport(@cInclude("ruby/ruby.h")); 3 | const testing = std.testing; 4 | 5 | // Calculate number of open doors after N passes 6 | // Code taken from Rosetta Code: https://rosettacode.org/wiki/100_doors#Zig 7 | fn hundred_doors(passes: c_int) c_int { 8 | var doors = [_]bool{false} ** 101; 9 | var pass: u8 = 1; 10 | var door: u8 = undefined; 11 | 12 | while (pass <= passes) : (pass += 1) { 13 | door = pass; 14 | while (door <= 100) : (door += pass) 15 | doors[door] = !doors[door]; 16 | } 17 | 18 | var num_open: u8 = 0; 19 | for (doors) |open| { 20 | if (open) 21 | num_open += 1; 22 | } 23 | return num_open; 24 | } 25 | 26 | // This is a wrapper for hundred_doors function to make it work with Ruby. 27 | fn rb_hundred_doors(...) callconv(.C) ruby.VALUE { 28 | var ap = @cVaStart(); 29 | defer @cVaEnd(&ap); 30 | 31 | // first argument is `self`, but we don't use it so we need to discard it 32 | const self = @cVaArg(&ap, ruby.VALUE); 33 | _ = self; 34 | 35 | // back and forth conversion from Ruby types to internal types + delegation to 36 | // actual `hundred_doors` function 37 | const passes = ruby.NUM2INT(@cVaArg(&ap, ruby.VALUE)); 38 | return ruby.INT2NUM(hundred_doors(passes)); 39 | } 40 | 41 | export fn Init_libzig_rb() void { 42 | const zig_rb_class: ruby.VALUE = ruby.rb_define_class("ZigRb", ruby.rb_cObject); 43 | _ = ruby.rb_define_method(zig_rb_class, "hundred_doors", rb_hundred_doors, 1); 44 | } 45 | 46 | test "hundred doors 100 passes" { 47 | try testing.expect(hundred_doors(100) == 10); 48 | } 49 | 50 | test "hundred_doors 1 pass" { 51 | try testing.expect(hundred_doors(1) == 100); 52 | } 53 | --------------------------------------------------------------------------------