Skip to content

Performance Improvements (and refactoring)#48

Draft
chabad360 wants to merge 15 commits into
hypebeast:masterfrom
chabad360:patch-1
Draft

Performance Improvements (and refactoring)#48
chabad360 wants to merge 15 commits into
hypebeast:masterfrom
chabad360:patch-1

Conversation

@chabad360

@chabad360 chabad360 commented Mar 22, 2021

Copy link
Copy Markdown
Contributor

Here are a large number of performance improvements. I want to move the discussion over from #47 as it's not possible to comment on actual code in an issue.

This code is not intended to be merged, while it is functional, it is primarily for discussion. I would rather wait until the repo has been restructured into separate files, as that will make it easier to digest the changes.

Note: the current changes in this patch are not the changes that are shown in this benchmark, those are from my master branch (warning, I've modified the api there)

Just to give an idea of the performance improvement [1]:

Function ops ↑ ns/op ↓ B/op ↓ allocs/op ↓
Server.ReceivePacket()
Original 23,005 60,837 70,080 18
New 1,000,000 1,911 208 8
ParsePacket()
Original 121,566 8,235 4,624 18
New 1,000,000 1,303 192 7
Message.String()
Original 804,878 3,510 256 10
New 1,000,000 1560 83 2
Message.MarshalBinary()
Original 1,000,000 1,942 368 10
New 1,000,000 1,017 83 2
New (Light Version) 3,024,367 498 3 1

[1]: https://replit.com/@chabad360/go-osc-benchmark

@chabad360 chabad360 changed the title Performance Improvements Performance Improvements (and refactoring) Mar 30, 2021

@chabad360 chabad360 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone through the PR and just wrote down the reasons why I did somethings a certain way (specifically the stuff that may not be quite clear). You may want to view these with the diff view, that should make some of them more clear.

Comment thread osc/osc.go
type Timetag struct {
timeTag uint64 // The acutal time tag
time time.Time
MinValue uint64 // Minimum value of an OSC Time Tag. Is always 1.

@chabad360 chabad360 Mar 30, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This results in an unnecessarily larger struct, and is admittedly rather useless, considering it's intended purpose and there's no way to make this value read-only, which means there's no guarantee that the value will remain 1 (which you get with a const).

Comment thread osc/osc.go
Comment on lines +248 to +255
strBuf = strBuf[:0]
strBuf = append(strBuf, msg.Address...)
if len(tags) == 0 {
return string(strBuf)
}

formatString := "%s %s"
var args []interface{}
args = append(args, msg.Address)
args = append(args, tags)
strBuf = append(strBuf, ' ')
strBuf = append(strBuf, tags...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing a byte array is far more efficient than appending to a string, as strings are immutable. While reusing a slice can use the same underlying array as far as it goes, only expanding it when necessary.

Comment thread osc/osc.go
}

// LightMarshalBinary allows you to provide a pre-created `*bytes.Buffer` for MarshalBinary
func (msg *Message) LightMarshalBinary(data *bytes.Buffer) error {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this function for areas where speed and efficiency is very important (I'm going to assume that's most OSC applications). This allows you to provide a pre-created bytes.Buffer saving a rather large allocation.

Comment thread osc/osc.go
Comment on lines +360 to +361
b := initBuf[:data.Len()]
data.Read(b)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-using the same buffer allows us to avoid allocating a second one, while not actually using any extra room (thanks to initBuf, bit of a hack.). However this does have the downside of being a race-condition, so methods to deal with this need to be explored.

Comment thread osc/osc.go

// Process all OSC Messages
for _, m := range b.Messages {
buf, err := m.MarshalBinary()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanna figure out how to use m.LightMarshalBinary for this.

Comment thread osc/osc.go
if _, err := buf.Write(data); err != nil {
return 0, nil
}
n, _ := buf.Write(data)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytes.Buffer.Write() never returns an error, it only panics.

Comment thread osc/osc.go
numPadBytes = n
}
numPadBytes := padBytesNeeded(n)
buf.Write(padBytes[:numPadBytes])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that instead of creating a new slice with the right length or using a loop and WriteByte it just made more sense to pre-allocate a []byte with 4 0's and only write the amount required..

Comment thread osc/osc.go
// bytes are removed from the buffer.
func readPaddedString(buffer *bytes.Buffer) (string, int, error) {
//Read the string from the buffer
str, err := buffer.ReadString(0)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still haven't found a good substitute for this (and it's horribly expensive).

Comment thread osc/osc.go
// Add the padding bytes to the buffer
buf.Write(padBytes[:numPadBytes])

return n + numPadBytes, nil

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need to return an error if we'll never get one, right?

Comment thread osc/osc.go
// getTypeTag returns the OSC type tag for the given argument.
func getTypeTag(arg interface{}) (string, error) {
// GetTypeTag returns the OSC type tag for the given argument.
func GetTypeTag(arg interface{}) (string, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured this would be better public.

@chabad360

Copy link
Copy Markdown
Contributor Author

@hypebeast?

@giohappy

giohappy commented Oct 7, 2021

Copy link
Copy Markdown

@chabad360 are you still planning to complete this PR and merge the improvements?

@chabad360

Copy link
Copy Markdown
Contributor Author

I would love to, but I would kinda prefer to split osc.go into multiple separate files before finishing this up. It's a lot of changes, so it would be better to push this once the changes are more clear. However, if @hypebeast would prefer to do this first, I'll gladly finish it up.

@chabad360

Copy link
Copy Markdown
Contributor Author

@giohappy and anyone else who's interested in this: I would recommend trying to use my fork, it has some api changes (that I believe are sane and make it more idiomatic), I'm actively maintaining it, so at least I'll be more quick to fix bugs.

@depili

depili commented Jan 26, 2022

Copy link
Copy Markdown

I have tried this fork, and sadly this isn't functional in a real world environment, as the decoder panics on empty strings. In general issuing fatal errors on decoding of invalid packets on a server is a door for DoS attacks and should be avoided if possible.

The forked repository also doesn't have issue tracker enabled for reporting such issues with it.

@chabad360

chabad360 commented Jan 26, 2022

Copy link
Copy Markdown
Contributor Author

Oh, yes. I know about that, it bit me in back side pretty hard already. Very poor decision on my part (I don't actually remember why I made it do that). I used a temporary fix in my application, but in a week or two I'm going to be going on a bit of a bug hunt/major rewrite. So it'll be fixed then.

I'll enable issue tracker later today.

@hypebeast hypebeast mentioned this pull request Apr 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants