-
Notifications
You must be signed in to change notification settings - Fork 228
DoWithReader for reduced allocations during Redis response parsing #935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Hmm, I think I was so glad about results that missed that strings were returned unsafe in my bench. It seems results are invalid here. The benefit exists, but it's not that huge in all cases. Let me re-evaluate and reopen later with proper bench if the benefit will be obvious still. |
|
Yep, so the original benchmark I used in description had a problem - missing data copy. But the bench which was reading from Redis stream seems correct - data is properly copied, and the benefit is sufficient - I updated PR description to use proper bench. |
|
Added UPD. And more benchmarks for XREAD and blocking XREAD: |
|
Hi @FZambia, Seems like you want to only pick certain parts from a complex RESP response. Given that the
The purpose of For example: var results []string
for val := range c.DoStream(ctx, cmd).IterateStrings(func(typ byte, length, depth, index int) bool {
if typ == '*' {
return true // go into the array
} else if typ == '$' && index % 2 == 1 {
return true // read only values from key value pairs
}
return false // discard anything else
}) {
results = append(results, val)
}
I feel like this can still be done in
Yes, the |
|
Hi, thanks for response! Do you mean that it will work like picking some specific parts of the response and collecting slice of strings? Generally – yes, the goal is to avoid allocs during processing, just it's not always values from one level, and sometimes I need more complex parsing of byte streams than just saving a raw value, sometimes parsing some prefixes, sometimes extracting int64 from bytes directly. From this perspective the feeling is that while In benchmarks I appended here I demonstrated extracting a single value from each entry, but generally there are more complex cases where I have more complex data returned by Lua and where the end goal is not just picking a value from top-level array, but get some other values from top level array, and then collect entries from array inside array (result of xrange). It seems it will be still more allocs than needed - because with streaming approach it's possible to avoid one extra copy for each stream entry because we can convert it to a struct during iteration. While with iteration - most probably it will be extra copy to safely provide string BTW, I missed the fact that Let me try to provide another benchmark with Lua to demonstrate a more real-life use case, to evaluate it over |
Agree, the Reader is actually like how rueidis parses messages internally. It is for sure without limitations. I think it is fine to provide the Reader under
I think we can also provide the result of var results []string
for range c.DoStream(ctx, cmd).IterateStrings(func(typ byte, intval, depth, index int, peak []byte) bool {
if typ == '*' {
return true // go into the array
} else if typ == '$' && index % 2 == 1 {
if intval == len(peak) {
results = append(results, string(peak))
}
}
return false // discard anything else
}) {
// nothing
}However, there is always a risk that the peak may fall short. |
|
Added one more bench which is close to what I have in real-life - Maybe it can help you to evaluate what fits better here? For now it's hard for me to imagine how to use
Maybe it's possible to somehow make the API more explicit to reduce chance of mistake, some ideas:
The feeling is that it's hard to forget what is possible to achieve allocation wise after looking at these numbers.. |
The iterate function may look like this for your example: func (typ byte, intval, depth, index int, peak []byte) bool {
switch depth {
case 0:
return typ == '*'
case 1:
switch {
case index == 0 && typ == ':':
offset = uint64(intval)
case index == 0 && typ == '$' && intval == len(peak):
offset, _ = strconv.ParseUint(string(peak), 10, 64)
case index == 1 && typ == '$' && intval == len(peak):
epoch = string(peak)
case index == 2 && typ == '*':
pubs = make([]BenchPub, 0, intval)
return true
}
case 2:
return typ == '*'
case 3:
switch {
case index == 0 && typ == '$' && intval == len(peak):
hyphenIdx := bytes.IndexByte(peak, '-')
pubOffset, _ = strconv.ParseUint(unsafe.String(peak[:hyphenIdx], hyphenIdx), 10, 64)
case index == 1 && typ == '*':
return true
}
case 4:
switch {
case index == 0 && typ == '$' && len(peak) == 1 && peak[0] == 'd':
isdata = true
case index == 1 && typ == '$' && intval == len(peak) && isdata:
pubs = append(pubs, BenchPub{Offset: pubOffset, Data: string(peak)})
}
}
return false
}Yes, this may feel a bit unnatural and may require some state management.
Yes, we should not expose *bufio.Reader, and even with *resp.Reader, we should make sure the reader can't be used after reading the response that belongs to it.
We probably need to stack how many messages are left in each depth. We can't return a connection until it is in depth 0 and there are no messages left in depth 0. |
Hello @rueian , hope you are doing well!
Currently,
rueidisdoes not allow parsing large Redis replies with multiple messages in nested arrays without extra allocations inreadB(like results from XRANGE).Here is a profile of one of the benches I have.
From what I see these allocs are often unavoidable, because they happen before we get control over
RedisResultinDo.I noticed
DoStream, but it seems very limited in supported types, and the purpose of it seems different.So eventually I tried to go in a bit different direction: introduce
DoWithReader.A benchmark that demonstrates the problem and how DoWithReader helps:
Results:
To simplify parsing the idea is introducing new package in rueidis called
respwithresp.Readerhelper. The example above already uses the prototype of that package. It's a wrapper to read values.Most of the code here is generated by LLM, so maybe I missed something important which ruins the idea.
Some design ideas which I had in mind:
rueidislevel - so that it works natively with Redis Cluster.There are more open questions, which I'll postpone for now waiting for your general feedback on the above. Maybe there is a simpler way to achieve the same?