Skip to content

Instantly share code, notes, and snippets.

@jgaskins
Created March 9, 2024 23:12
Show Gist options
  • Save jgaskins/d62c32dc57aec6ff77b8e7c7119d2884 to your computer and use it in GitHub Desktop.
Save jgaskins/d62c32dc57aec6ff77b8e7c7119d2884 to your computer and use it in GitHub Desktop.
Minimal Redis cache implementation in pure Ruby, outperforming Hiredis by 2-3x
$ ruby -v bench_redis.rb
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
KEY : bench-redis
VALUE SIZE: 102400
ITERATIONS: 10000
GET
Rehearsal -------------------------------------------
MyRedis 0.140202 0.186112 0.326314 ( 0.555126)
hiredis 0.204024 0.347284 0.551308 ( 0.748424)
---------------------------------- total: 0.877622sec
user system total real
MyRedis 0.129526 0.194071 0.323597 ( 0.545520)
hiredis 0.206162 0.421801 0.627963 ( 0.835756)
Total CPU time:
MyRedis: fastest
hiredis: 1.94x slower
SET
Rehearsal -------------------------------------------
MyRedis 0.041352 0.106972 0.148324 ( 0.440225)
hiredis 0.101738 0.378923 0.480661 ( 0.671545)
---------------------------------- total: 0.628985sec
user system total real
MyRedis 0.041063 0.106380 0.147443 ( 0.436962)
hiredis 0.100178 0.372010 0.472188 ( 0.644582)
Total CPU time:
MyRedis: fastest
hiredis: 3.2x slower
SET ... EX
Rehearsal -------------------------------------------
MyRedis 0.046742 0.103616 0.150358 ( 0.452327)
hiredis 0.107150 0.372023 0.479173 ( 0.663598)
---------------------------------- total: 0.629531sec
user system total real
MyRedis 0.046639 0.103697 0.150336 ( 0.451419)
hiredis 0.105448 0.367293 0.472741 ( 0.647509)
Total CPU time:
MyRedis: fastest
hiredis: 3.14x slower
# frozen_string_literal: true
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem "hiredis-client"
gem "redis"
# gem "activesupport", require: "active_support/all"
gem "benchmark-ips"
end
require "socket"
require "benchmark"
ITERATIONS = ARGV.fetch(2, "10_000").to_i
def bench(iterations: ITERATIONS)
# Benchmark.ips do |x|
# yield x
# x.compare!
# end
result = Benchmark.bmbm do |x|
yield BMBM.new(x, iterations)
x
end
fastest = result.min_by(&:total)
label_size = result.map { |r| r.label.length }.max
puts "Total CPU time:"
result.sort_by(&:total).each do |r|
if r == fastest
label = "fastest"
else
label = "#{(r.total / fastest.total).round(2)}x slower"
end
puts "#{r.label.rjust(label_size)}: #{label}"
end
end
class BMBM
def initialize(x, iterations)
@x = x
@iterations = iterations
end
def report(name)
@x.report(name) { @iterations.times { yield } }
end
end
class MyRedis
def initialize(host = "localhost", port = 6379)
@host = host
@port = port
@socket = TCPSocket.new(host, port)
@socket.sync = false
end
def get(key)
# ["GET", key]
@socket << "*2\r\n"
@socket << "$3\r\n"
@socket << "GET\r\n"
@socket << "$" << key.bytesize << "\r\n"
@socket << key << "\r\n"
byte_size = @socket.readline[1...-2].to_i
if byte_size >= 0
value = String.new(capacity: byte_size)
@socket.read(byte_size, value)
@socket.readline
end
value
end
def set(key, value, ex: nil, px: nil, nx: nil)
size = 3 # SET key value
if ex || px
size = 5
end
if nx
size += 1
end
@socket << "*" << size << "\r\n"
@socket << "$3\r\n"
@socket << "SET\r\n"
@socket << "$" << key.bytesize << "\r\n"
@socket << key << "\r\n"
@socket << "$" << value.bytesize << "\r\n"
@socket << value << "\r\n"
if nx
@socket << "$2\r\n"
@socket << "NX\r\n"
end
if ex || px
type = ex ? "EX" : "PX"
value = (ex || px).to_s
@socket << "$2\r\n"
@socket << type << "\r\n"
@socket << "$" << value.bytesize << "\r\n"
@socket << value << "\r\n"
end
@socket.readline[1...-2]
end
def del(*keys)
@socket << "*" << keys.size + 1 << "\r\n"
@socket << "$3\r\nDEL\r\n"
keys.each do |key|
@socket << "$" << key.bytesize << "\r\n"
@socket << key << "\r\n"
end
@socket.readline[1...-2].to_i
end
end
key = ARGV.fetch(0, "bench-redis")
value = "." * ARGV.fetch(1, "102400").to_i # 100KB default
hiredis = Redis.new(driver: :hiredis)
redis = Redis.new(driver: :ruby)
myredis = MyRedis.new
Value = Struct.new(:value)
# Example to use this Redis client with ActiveSupport::Cache::RedisCacheStore
# ActiveSupport::Cache.format_version = 7.1
# cache = ActiveSupport::Cache::RedisCacheStore.new(redis: myredis)
# result = nil
# 100000.times do
# result = cache.fetch "key", expires_in: 2.seconds do
# Value.new(42)
# end
# end
# pp result: result
# exit 0
puts
puts
puts "KEY : #{key}"
puts "VALUE SIZE: #{value.bytesize}"
puts "ITERATIONS: #{ITERATIONS}"
actual = myredis.set(key, value)
if actual != "OK"
raise "Oops! Result should be \"OK\", got #{actual.inspect}"
end
actual = myredis.get(key)
if actual != value
raise "Oops! #{key} should be #{value.inspect}, got #{actual.inspect}"
end
myredis.set key, value, ex: 1_000
if redis.ttl(key) != 1_000
raise "Oops! The TTL for #{key} should be 1000, got: #{redis.ttl(key)}"
end
puts
puts "GET"
bench do |x|
x.report("MyRedis") { myredis.get(key) }
x.report("hiredis") { hiredis.get(key) }
# x.report("redis") { redis.get(key) }
end
puts
puts "SET"
bench do |x|
x.report("MyRedis") { myredis.set(key, value) }
x.report("hiredis") { hiredis.set(key, value) }
# x.report("redis") { redis.set(key, value) }
end
puts
puts "SET ... EX"
bench do |x|
x.report("MyRedis") { myredis.set(key, value, ex: 1) }
x.report("hiredis") { hiredis.set(key, value, ex: 1) }
# x.report("redis") { redis.set(key, value, ex: 1) }
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment