Skip to content

Commit bf00251

Browse files
authored
Merge pull request #184 from redis-rb/bench-hiredis-vs-ruby
Various Ruby driver optimizations
2 parents 925bb6f + a82593c commit bf00251

File tree

7 files changed

+256
-24
lines changed

7 files changed

+256
-24
lines changed

.rubocop.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Lint/UnderscorePrefixedVariableName:
3636
Lint/EmptyBlock:
3737
Enabled: false
3838

39+
Lint/DuplicateBranch:
40+
Enabled: false
41+
3942
Lint/MissingSuper:
4043
Enabled: false
4144

@@ -63,6 +66,12 @@ Metrics/ParameterLists:
6366
Metrics/PerceivedComplexity:
6467
Enabled: false
6568

69+
Style/InfiniteLoop:
70+
Enabled: false
71+
72+
Style/WhileUntilModifier:
73+
Enabled: false
74+
6675
Style/Alias:
6776
EnforcedStyle: prefer_alias_method
6877

Rakefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,15 @@ namespace :hiredis do
7171
end
7272
end
7373

74-
benchmark_suites = %w(single pipelined)
74+
benchmark_suites = %w(single pipelined drivers)
7575
benchmark_modes = %i[ruby yjit hiredis]
7676
namespace :benchmark do
7777
benchmark_suites.each do |suite|
7878
benchmark_modes.each do |mode|
79+
next if suite == "drivers" && mode == :hiredis
80+
7981
name = "#{suite}_#{mode}"
82+
desc name
8083
task name do
8184
output_path = "benchmark/#{name}.md"
8285
sh "rm", "-f", output_path

benchmark/drivers.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "setup"
4+
5+
ruby = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: :ruby)
6+
hiredis = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: :hiredis)
7+
8+
ruby.call("SET", "key", "value")
9+
ruby.call("SET", "large", "value" * 10_000)
10+
ruby.call("LPUSH", "list", *5.times.to_a)
11+
ruby.call("LPUSH", "large-list", *1000.times.to_a)
12+
ruby.call("HMSET", "hash", *8.times.to_a)
13+
ruby.call("HMSET", "large-hash", *1000.times.to_a)
14+
15+
benchmark("small string x 100") do |x|
16+
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("GET", "key") } } }
17+
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("GET", "key") } } }
18+
end
19+
20+
benchmark("large string x 100") do |x|
21+
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("GET", "large") } } }
22+
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("GET", "large") } } }
23+
end
24+
25+
benchmark("small list x 100") do |x|
26+
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } }
27+
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } }
28+
end
29+
30+
benchmark("large list") do |x|
31+
x.report("hiredis") { hiredis.call("LRANGE", "large-list", 0, -1) }
32+
x.report("ruby") { ruby.call("LRANGE", "large-list", 0, -1) }
33+
end
34+
35+
benchmark("small hash x 100") do |x|
36+
x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } }
37+
x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } }
38+
end
39+
40+
benchmark("large hash") do |x|
41+
x.report("hiredis") { ruby.call("HGETALL", "large-hash") }
42+
x.report("ruby") { ruby.call("HGETALL", "large-hash") }
43+
end

benchmark/drivers_ruby.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
ruby: `ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]`
2+
3+
redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c`
4+
5+
6+
### small string x 100
7+
8+
```
9+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
10+
hiredis: 4825.5 i/s
11+
ruby: 2863.4 i/s - 1.69x slower
12+
13+
```
14+
15+
### large string x 100
16+
17+
```
18+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
19+
hiredis: 266.6 i/s
20+
ruby: 198.1 i/s - 1.35x slower
21+
22+
```
23+
24+
### small list x 100
25+
26+
```
27+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
28+
hiredis: 2416.9 i/s
29+
ruby: 1223.3 i/s - 1.98x slower
30+
31+
```
32+
33+
### large list
34+
35+
```
36+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
37+
hiredis: 5351.6 i/s
38+
ruby: 1718.0 i/s - 3.11x slower
39+
40+
```
41+
42+
### small hash x 100
43+
44+
```
45+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
46+
hiredis: 2854.3 i/s
47+
ruby: 1294.4 i/s - 2.21x slower
48+
49+
```
50+
51+
### large hash
52+
53+
```
54+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]
55+
hiredis: 1580.6 i/s
56+
ruby: 1634.7 i/s - same-ish: difference falls within error
57+
58+
```
59+

benchmark/drivers_yjit.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
ruby: `ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]`
2+
3+
redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c`
4+
5+
6+
### small string x 100
7+
8+
```
9+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
10+
hiredis: 6407.8 i/s
11+
ruby: 5852.0 i/s - same-ish: difference falls within error
12+
13+
```
14+
15+
### large string x 100
16+
17+
```
18+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
19+
hiredis: 302.8 i/s
20+
ruby: 337.3 i/s - same-ish: difference falls within error
21+
22+
```
23+
24+
### small list x 100
25+
26+
```
27+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
28+
hiredis: 4067.7 i/s
29+
ruby: 2721.5 i/s - 1.49x slower
30+
31+
```
32+
33+
### large list
34+
35+
```
36+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
37+
hiredis: 7138.7 i/s
38+
ruby: 6605.4 i/s - same-ish: difference falls within error
39+
40+
```
41+
42+
### small hash x 100
43+
44+
```
45+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
46+
hiredis: 4219.8 i/s
47+
ruby: 3586.4 i/s - 1.18x slower
48+
49+
```
50+
51+
### large hash
52+
53+
```
54+
ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23]
55+
hiredis: 5240.9 i/s
56+
ruby: 5312.5 i/s - same-ish: difference falls within error
57+
58+
```
59+

lib/redis_client/ruby_connection/buffered_io.rb

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ class BufferedIO
1010

1111
attr_accessor :read_timeout, :write_timeout
1212

13-
def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
13+
def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096, encoding: Encoding.default_external)
1414
@io = io
15-
@buffer = "".b
15+
@encoding = encoding
16+
@buffer = "".dup.force_encoding(@encoding)
1617
@offset = 0
1718
@chunk_size = chunk_size
1819
@read_timeout = read_timeout
@@ -82,8 +83,10 @@ def write(string)
8283
end
8384

8485
def getbyte
85-
ensure_remaining(1)
86-
byte = @buffer.getbyte(@offset)
86+
unless byte = @buffer.getbyte(@offset)
87+
ensure_remaining(1)
88+
byte = @buffer.getbyte(@offset)
89+
end
8790
@offset += 1
8891
byte
8992
end
@@ -99,6 +102,29 @@ def gets_chomp
99102
line
100103
end
101104

105+
def gets_integer
106+
int = 0
107+
offset = @offset
108+
while true
109+
chr = @buffer.getbyte(offset)
110+
111+
if chr
112+
if chr == 13 # "\r".ord
113+
@offset = offset + 2
114+
break
115+
else
116+
int = (int * 10) + chr - 48
117+
end
118+
offset += 1
119+
else
120+
ensure_line
121+
return gets_integer
122+
end
123+
end
124+
125+
int
126+
end
127+
102128
def read_chomp(bytes)
103129
ensure_remaining(bytes + EOL_SIZE)
104130
str = @buffer.byteslice(@offset, bytes)
@@ -108,16 +134,27 @@ def read_chomp(bytes)
108134

109135
private
110136

137+
def ensure_line
138+
fill_buffer(false) if @offset >= @buffer.bytesize
139+
until @buffer.index(EOL, @offset)
140+
fill_buffer(false)
141+
end
142+
end
143+
111144
def ensure_remaining(bytes)
112145
needed = bytes - (@buffer.bytesize - @offset)
113146
if needed > 0
114147
fill_buffer(true, needed)
115148
end
116149
end
117150

151+
RESET_BUFFER_ENCODING = RUBY_ENGINE == "truffleruby"
152+
private_constant :RESET_BUFFER_ENCODING
153+
118154
def fill_buffer(strict, size = @chunk_size)
119155
remaining = size
120-
empty_buffer = @offset >= @buffer.bytesize
156+
start = @offset - @buffer.bytesize
157+
empty_buffer = start >= 0
121158

122159
loop do
123160
bytes = if empty_buffer
@@ -126,15 +163,6 @@ def fill_buffer(strict, size = @chunk_size)
126163
@io.read_nonblock([remaining, @chunk_size].max, exception: false)
127164
end
128165
case bytes
129-
when String
130-
if empty_buffer
131-
@offset = 0
132-
empty_buffer = false
133-
else
134-
@buffer << bytes
135-
end
136-
remaining -= bytes.bytesize
137-
return if !strict || remaining <= 0
138166
when :wait_readable
139167
unless @io.to_io.wait_readable(@read_timeout)
140168
raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads
@@ -144,7 +172,15 @@ def fill_buffer(strict, size = @chunk_size)
144172
when nil
145173
raise EOFError
146174
else
147-
raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
175+
if empty_buffer
176+
@offset = start
177+
empty_buffer = false
178+
@buffer.force_encoding(@encoding) if RESET_BUFFER_ENCODING
179+
else
180+
@buffer << bytes.force_encoding(@encoding)
181+
end
182+
remaining -= bytes.bytesize
183+
return if !strict || remaining <= 0
148184
end
149185
end
150186
end

lib/redis_client/ruby_connection/resp3.rb

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,39 @@ def dump_symbol(symbol, buffer)
111111

112112
def parse(io)
113113
type = io.getbyte
114-
method = PARSER_TYPES.fetch(type) do
114+
if type == 35 # '#'.ord
115+
parse_boolean(io)
116+
elsif type == 36 # '$'.ord
117+
parse_blob(io)
118+
elsif type == 43 # '+'.ord
119+
parse_string(io)
120+
elsif type == 61 # '='.ord
121+
parse_verbatim_string(io)
122+
elsif type == 45 # '-'.ord
123+
parse_error(io)
124+
elsif type == 58 # ':'.ord
125+
parse_integer(io)
126+
elsif type == 40 # '('.ord
127+
parse_integer(io)
128+
elsif type == 44 # ','.ord
129+
parse_double(io)
130+
elsif type == 95 # '_'.ord
131+
parse_null(io)
132+
elsif type == 42 # '*'.ord
133+
parse_array(io)
134+
elsif type == 37 # '%'.ord
135+
parse_map(io)
136+
elsif type == 126 # '~'.ord
137+
parse_set(io)
138+
elsif type == 62 # '>'.ord
139+
parse_array(io)
140+
else
115141
raise UnknownType, "Unknown sigil type: #{type.chr.inspect}"
116142
end
117-
send(method, io)
118143
end
119144

120145
def parse_string(io)
121146
str = io.gets_chomp
122-
str.force_encoding(Encoding.default_external)
123147
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
124148
str.freeze
125149
end
@@ -140,16 +164,16 @@ def parse_boolean(io)
140164
end
141165

142166
def parse_array(io)
143-
parse_sequence(io, parse_integer(io))
167+
parse_sequence(io, io.gets_integer)
144168
end
145169

146170
def parse_set(io)
147-
parse_sequence(io, parse_integer(io))
171+
parse_sequence(io, io.gets_integer)
148172
end
149173

150174
def parse_map(io)
151175
hash = {}
152-
parse_integer(io).times do
176+
io.gets_integer.times do
153177
hash[parse(io)] = parse(io)
154178
end
155179
hash
@@ -192,11 +216,10 @@ def parse_null(io)
192216
end
193217

194218
def parse_blob(io)
195-
bytesize = parse_integer(io)
219+
bytesize = io.gets_integer
196220
return if bytesize < 0 # RESP2 nil type
197221

198222
str = io.read_chomp(bytesize)
199-
str.force_encoding(Encoding.default_external)
200223
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
201224
str
202225
end

0 commit comments

Comments
 (0)