-
Notifications
You must be signed in to change notification settings - Fork 669
portfwd: Optimize errors.Is() checks #2969
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
Conversation
Did you do any measurements?
What is impossible? see my comment here: 553ebd9#r1868342753 |
I'll change this to remove all the io.EOF checks, as @tamird pointed out that ReadFrom is documented not to the return io.EOF, and io.Copy() implementation doesn't return it. |
Here are some of the results from the blog post you linked:
So the nil check saves approximate 1 nanosecond. These code paths are invoked once per socket connection. Does one nanosecond matter?
This is a dead link, but I replied to your comment: #2944 (comment) |
pkg/portfwd/client.go
Outdated
if err != nil && errors.Is(err, io.EOF) { | ||
return nil | ||
} | ||
return err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can simplify this further by completely removing errors.Is(err, io.EOF)
:
if err != nil && errors.Is(err, io.EOF) { | |
return nil | |
} | |
return err | |
return err |
According to the doc, io.Copy
never returns io.EOF
:
Copy copies from src to dst until either EOF is reached on src or an error occurs. It returns the number of bytes copied and the first error encountered while copying, if any.
A successful Copy returns err == nil, not err == EOF. Because Copy is defined to read from src until EOF, it does not treat an EOF from Read as an error to be reported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the check was removed now.
9e3c2e7
to
ab26efe
Compare
@tamird as I wrote, errors.Is() does not make sense even if the difference is not significant. We should handle errors only if there was an error. The change updated to remove the unneeded checks. |
ab26efe
to
70a1bd3
Compare
@@ -64,9 +58,6 @@ func HandleUDPConnection(ctx context.Context, client *guestagentclient.GuestAgen | |||
buf := make([]byte, 65507) | |||
for { | |||
n, addr, err := conn.ReadFrom(buf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Callers should always process
// the n > 0 bytes returned before considering the error err.
https://pkg.go.dev/net#PacketConn
Seems this code is wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, this is a bug in the code, but it is not related this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #2970
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be fixd by #2971, thanks for pointing out this isue!
Don't worry about conflicts. One of the PRs will be merged first and the other will do a trivial rebase. |
This is simply not true. It shortcuts if either func Is(err, target error) bool {
if err == nil || target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
return is(err, target, isComparable)
} There is no reason to avoid calling it, even if |
On the read data path, we had many checks like: if errors.Is(err, io.EOF) { return nil } There are 2 issues with these checks: 1. io.Copy() is documented to never return io.EOF, and current code matches the docs. 2. errors.Is() is expensive and should not be used in the fast path This change remove the checks when they are not needed, and it the case of handling Recv() which is documented to return io.EOF, or PacketCon.ReadFrom() which does not mention io.EOF semantics, move the check into a err != nil block. I did not make any measurements with lima, but dolthub.com did synthetic benchmarks: https://www.dolthub.com/blog/2024-05-31-benchmarking-go-error-handling/ Based on the benchmarks the difference in the case when err == nil is not big, but the code is more clear when we gather all error handling under a err != nil check. Signed-off-by: Nir Soffer <nirsof@gmail.com>
70a1bd3
to
ef60e8e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, LGTM
PacketCon.ReadFrom documentation says[1]: > Callers should always process the n > 0 bytes returned before > considering the error err. But we handled err first, and dropped the last read bytes. Fixed by sending a message if n > 0, and handling the error after the send. I'm not sure what how this bug affects the system. It was found while reviewing lima-vm#2969. stream.Recv() does is not documented, but the trivial implementation ensure that we get nil message on any error. Add comment to make it more clear. [1] https://pkg.go.dev/net#PacketConn Fixes lima-vm#2970 Thanks: Tamir Duberstein <tamird@gmail.com> Signed-off-by: Nir Soffer <nirsof@gmail.com>
The shortcut is still a useless function call. It makes sense in code that you don't care about, if it is more clear.
Right, it make the code worse.
I'm sure the compiler cannot inline it. We can have 1000's of these, and it does not make sense to inline such function. Doing more work in the data path is always bad idea, even if the compiler optimize it. The entire errors.Is() thing is is a bad idea. Instead of returning sentinel error and doing a fast check, people wrap the sentinel in other errors and wasted cycles on finding the wrapped errors. Even worse, the error tree may contain a wrapped io.EOF when the error is real and should not be handled as io.EOF. io.Copy() does not the right way doing equality check on io.EOF. |
You are just manually inlining code to avoid a "useless" call. The same argument could be made if you have a function to add 2 numbers, and you write: if b != 0 {
a = add(a, b)
} to avoid a "useless function call" in case To me replacing The new code in this PR was not of that kind, so I merged it, but I would not agree with fluffing up code by manually inlining parts of functions. That's what optimizing compilers are for.
Why not? I know there are a bunch of conditions around inlining, but anything smaller than something like 100 AST nodes will be inlined, and |
My naïve attempt shows that I still think the overhead of a single "useless" function call is trivial. I don't think it will be noticeable if this is on top of a system call. We will never make back the time we wasted discussing this! |
On the read data path, we had many checks like:
There are 2 issues with these checks:
matches the docs.
This change remove the checks when they are not needed, and it the case
of handling Recv() which is documented to return io.EOF, or
PacketCon.ReadFrom() which does not mention io.EOF semantics, move the
check into a err != nil block.
I did not make any measurements with lima, but dolthub.com did synthetic
benchmarks:
https://www.dolthub.com/blog/2024-05-31-benchmarking-go-error-handling/
Based on the benchmarks the difference in the case when err == nil is
not big, but the code is more clear when we gather all error handling
under a err != nil check.