Created
March 9, 2024 23:12
-
-
Save jgaskins/d62c32dc57aec6ff77b8e7c7119d2884 to your computer and use it in GitHub Desktop.
Minimal Redis cache implementation in pure Ruby, outperforming Hiredis by 2-3x
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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