Skip to content

Commit 41b3abe

Browse files
byroottenderlove
andcommitted
RESP3: parse sequence sizes with getbyte
All RESP3 types start with a single line containing a postive integer that announce the size of the data. e.g. `*42\r\n....`. Before this commit we would read one line, hence allocate a String, then parse it with `Kernel.Integer`. By instead reading bytes one by one and rebuilding the integer we save that string allocation, which has a significant impact. The `gets_integer` method also optimistically assumes the buffer already contains the line, which saves some methods calls in the happy path. Co-authored-by: Aaron Patterson <[email protected]>
1 parent f066167 commit 41b3abe

File tree

5 files changed

+74
-39
lines changed

5 files changed

+74
-39
lines changed

.rubocop.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ Metrics/ParameterLists:
6666
Metrics/PerceivedComplexity:
6767
Enabled: false
6868

69+
Style/InfiniteLoop:
70+
Enabled: false
71+
72+
Style/WhileUntilModifier:
73+
Enabled: false
74+
6975
Style/Alias:
7076
EnforcedStyle: prefer_alias_method
7177

benchmark/drivers_ruby.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,53 @@ redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a1
77

88
```
99
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
10-
hiredis: 5416.0 i/s
11-
ruby: 2923.7 i/s - 1.85x slower
10+
hiredis: 5346.6 i/s
11+
ruby: 2984.3 i/s - 1.79x slower
1212
1313
```
1414

1515
### large string x 100
1616

1717
```
1818
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
19-
hiredis: 286.7 i/s
20-
ruby: 302.3 i/s - same-ish: difference falls within error
19+
hiredis: 304.2 i/s
20+
ruby: 204.1 i/s - 1.49x slower
2121
2222
```
2323

2424
### small list x 100
2525

2626
```
2727
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
28-
hiredis: 2596.0 i/s
29-
ruby: 1155.6 i/s - 2.25x slower
28+
hiredis: 2612.0 i/s
29+
ruby: 1240.8 i/s - 2.11x slower
3030
3131
```
3232

3333
### large list
3434

3535
```
3636
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
37-
hiredis: 6797.0 i/s
38-
ruby: 1388.9 i/s - 4.89x slower
37+
hiredis: 6772.7 i/s
38+
ruby: 1540.7 i/s - 4.40x slower
3939
4040
```
4141

4242
### small hash x 100
4343

4444
```
4545
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
46-
hiredis: 3459.8 i/s
47-
ruby: 1170.4 i/s - 2.96x slower
46+
hiredis: 3293.2 i/s
47+
ruby: 1234.0 i/s - 2.67x slower
4848
4949
```
5050

5151
### large hash
5252

5353
```
5454
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
55-
hiredis: 1345.9 i/s
56-
ruby: 1329.5 i/s - same-ish: difference falls within error
55+
hiredis: 1421.7 i/s
56+
ruby: 1481.0 i/s - same-ish: difference falls within error
5757
5858
```
5959

benchmark/drivers_yjit.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,53 @@ redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a1
77

88
```
99
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23]
10-
hiredis: 6775.6 i/s
11-
ruby: 5212.3 i/s - 1.30x slower
10+
hiredis: 6723.1 i/s
11+
ruby: 5507.5 i/s - 1.22x slower
1212
1313
```
1414

1515
### large string x 100
1616

1717
```
1818
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23]
19-
hiredis: 306.1 i/s
20-
ruby: 335.5 i/s - same-ish: difference falls within error
19+
hiredis: 290.8 i/s
20+
ruby: 335.2 i/s - same-ish: difference falls within error
2121
2222
```
2323

2424
### small list x 100
2525

2626
```
2727
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23]
28-
hiredis: 3566.7 i/s
29-
ruby: 1999.6 i/s - 1.78x slower
28+
hiredis: 3686.7 i/s
29+
ruby: 2437.1 i/s - 1.51x slower
3030
3131
```
3232

3333
### large list
3434

3535
```
3636
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23]
37-
hiredis: 6563.1 i/s
38-
ruby: 2984.7 i/s - 2.20x slower
37+
hiredis: 6725.9 i/s
38+
ruby: 4990.0 i/s - 1.35x slower
3939
4040
```
4141

4242
### small hash x 100
4343

4444
```
4545
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23]
46-
hiredis: 3870.9 i/s
47-
ruby: 2387.4 i/s - 1.62x slower
46+
hiredis: 3893.0 i/s
47+
ruby: 2994.7 i/s - 1.30x slower
4848
4949
```
5050

5151
### large hash
5252

5353
```
5454
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23]
55-
hiredis: 2641.9 i/s
56-
ruby: 2575.8 i/s - same-ish: difference falls within error
55+
hiredis: 4303.3 i/s
56+
ruby: 4244.6 i/s - same-ish: difference falls within error
5757
5858
```
5959

lib/redis_client/ruby_connection/buffered_io.rb

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,29 @@ def gets_chomp
9999
line
100100
end
101101

102+
def gets_integer
103+
int = 0
104+
offset = @offset
105+
while true
106+
chr = @buffer.getbyte(offset)
107+
108+
if chr
109+
if chr == 13 # "\r".ord
110+
@offset = offset + 2
111+
break
112+
else
113+
int = (int * 10) + chr - 48
114+
end
115+
offset += 1
116+
else
117+
ensure_line
118+
return gets_integer
119+
end
120+
end
121+
122+
int
123+
end
124+
102125
def read_chomp(bytes)
103126
ensure_remaining(bytes + EOL_SIZE)
104127
str = @buffer.byteslice(@offset, bytes)
@@ -108,6 +131,13 @@ def read_chomp(bytes)
108131

109132
private
110133

134+
def ensure_line
135+
fill_buffer(false) if @offset >= @buffer.bytesize
136+
until @buffer.index(EOL, @offset)
137+
fill_buffer(false)
138+
end
139+
end
140+
111141
def ensure_remaining(bytes)
112142
needed = bytes - (@buffer.bytesize - @offset)
113143
if needed > 0
@@ -117,7 +147,8 @@ def ensure_remaining(bytes)
117147

118148
def fill_buffer(strict, size = @chunk_size)
119149
remaining = size
120-
empty_buffer = @offset >= @buffer.bytesize
150+
start = @offset - @buffer.bytesize
151+
empty_buffer = start >= 0
121152

122153
loop do
123154
bytes = if empty_buffer
@@ -126,15 +157,6 @@ def fill_buffer(strict, size = @chunk_size)
126157
@io.read_nonblock([remaining, @chunk_size].max, exception: false)
127158
end
128159
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
138160
when :wait_readable
139161
unless @io.to_io.wait_readable(@read_timeout)
140162
raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads
@@ -144,7 +166,14 @@ def fill_buffer(strict, size = @chunk_size)
144166
when nil
145167
raise EOFError
146168
else
147-
raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
169+
if empty_buffer
170+
@offset = start
171+
empty_buffer = false
172+
else
173+
@buffer << bytes
174+
end
175+
remaining -= bytes.bytesize
176+
return if !strict || remaining <= 0
148177
end
149178
end
150179
end

lib/redis_client/ruby_connection/resp3.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,16 @@ def parse_boolean(io)
165165
end
166166

167167
def parse_array(io)
168-
parse_sequence(io, parse_integer(io))
168+
parse_sequence(io, io.gets_integer)
169169
end
170170

171171
def parse_set(io)
172-
parse_sequence(io, parse_integer(io))
172+
parse_sequence(io, io.gets_integer)
173173
end
174174

175175
def parse_map(io)
176176
hash = {}
177-
parse_integer(io).times do
177+
io.gets_integer.times do
178178
hash[parse(io)] = parse(io)
179179
end
180180
hash
@@ -217,7 +217,7 @@ def parse_null(io)
217217
end
218218

219219
def parse_blob(io)
220-
bytesize = parse_integer(io)
220+
bytesize = io.gets_integer
221221
return if bytesize < 0 # RESP2 nil type
222222

223223
str = io.read_chomp(bytesize)

0 commit comments

Comments
 (0)