-
Notifications
You must be signed in to change notification settings - Fork 249
cgroup1: Reduce allocations when reading uints from files #275
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 927396272 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,9 @@ package cgroup1 | |
|
|
||
| import ( | ||
| "bufio" | ||
| "bytes" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
| "strconv" | ||
|
|
@@ -130,12 +132,23 @@ func hugePageSizes() ([]string, error) { | |
| return pageSizes, nil | ||
| } | ||
|
|
||
| func readUint(path string) (uint64, error) { | ||
| v, err := os.ReadFile(path) | ||
| // readUint reads a uint64 from the given file and uses the given buffer as | ||
| // scratch space. The buffer is intended to be reused across calls. | ||
| func readUint(path string, buf *bytes.Buffer) (uint64, error) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this threadsafe? If you called any of the methods that now invoke this in a couple goroutines, there's no guarantee we'll get back the value we're expecting as another read might finish before, you may end up with interleaved data as shown below, or another thread might hit the func readUintHelper(path string, expected uint64, buf *bytes.Buffer) error {
actual, err := readUint(path, buf)
if err != nil {
return err
}
if expected != actual {
return fmt.Errorf("expected %d, got %d", expected, actual)
}
return nil
}
func TestReadUint(t *testing.T) {
var (
buf = bytes.NewBuffer(nil)
eg errgroup.Group
)
eg.Go(func() error {
return readUintHelper("testdata/uint100", 100, buf)
})
eg.Go(func() error {
return readUintHelper("testdata/uint200", 200, buf)
})
eg.Go(func() error {
return readUintHelper("testdata/uint300", 300, buf)
})
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
}Yields: dcantah@dcantah ~/go/src/github.com/containerd/cgroups ((HEAD detached at brancz/readuint)) $ go test ./cgroup1/ -v -run TestReadUint
=== RUN TestReadUint
utils_test.go:66: expected 100, got 300100
--- FAIL: TestReadUint (0.00s)
FAIL
FAIL github.com/containerd/cgroups/v3/cgroup1 0.003s
FAIL
dcantah@dcantah ~/go/src/github.com/containerd/cgroups ((HEAD detached at brancz/readuint)) $ go test ./cgroup1/ -v -run TestReadUint
=== RUN TestReadUint
utils_test.go:66: expected 100, got 200100
--- FAIL: TestReadUint (0.00s)
FAIL
FAIL github.com/containerd/cgroups/v3/cgroup1 0.002s
FAIL
dcantah@dcantah ~/go/src/github.com/containerd/cgroups ((HEAD detached at brancz/readuint)) $ go test ./cgroup1/ -v -run TestReadUint
=== RUN TestReadUint
utils_test.go:66: strconv.ParseUint: parsing "": invalid syntax
--- FAIL: TestReadUint (0.00s)
FAIL
FAIL github.com/containerd/cgroups/v3/cgroup1 0.002s
FAIL
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh nice, I forgot the cg1 code has a lock around most of these for the main *cgroup operations. Let me see if there's any that the above might be a concern
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the very least if you created individual controllers (e.g. cgroup1.NewMemory() -> mem.Stat()) you could run into this
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I was briefly contemplating using a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the controllers themselves there's not, but the library doesn't dissuade you from doing this yourself. How containerd uses this library is mainly through manipulating the main The library offers you the ability to Create individual controllers however with New{Controller} functions (NewMemory for example). These don't have any locking around them, so if anyone using this library relied on making an individual controller and then spinning up some goroutines to grab/update data, they may run into this now.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You’re absolutely right. How do you propose to move forward? I would say we can either analyze usages of the library and if there are no current concurrent usages we add a code comment saying they are not concurrency safe, or we go straight to the hammer and use a buffer pool? I generally prefer to not add synchronization if I don’t absolutely have to, but ultimately your call as a maintainer.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You wanna benchmark the buffer pool idea? It'd be a shame if this caused us to not be able to freely call these however we want, so anything to keep that alive would be nice. Thanks for the quick follow ups! This is awesome to see btw, very cool stream idea There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the problem before was that You can make this much simpler and still thread safe by creating a slice with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh fantastic point duh, can confirm it shouldn't escape: package main
import (
"log"
"os"
)
func readFile() error {
f, err := os.Open("howdy.txt")
if err != nil {
return err
}
defer f.Close()
howdy := make([]byte, 64)
_, err = f.Read(howdy)
if err != nil {
return err
}
return nil
}
func main() {
if err := readFile(); err != nil {
log.Fatal(err)
}
}alloc go build -gcflags '-m -l'
# github.com/dcantah/fun/alloc
./main.go:15:15: make([]byte, 64) does not escape
./main.go:25:12: ... argument does not escapeEvery so often we must be reminded of why
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @brancz Want to take this for a spin? |
||
| buf.Reset() | ||
|
|
||
| f, err := os.Open(path) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return parseUint(strings.TrimSpace(string(v)), 10, 64) | ||
| defer f.Close() | ||
|
|
||
| _, err = io.Copy(buf, f) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
|
|
||
| return parseUint(strings.TrimSpace(buf.String()), 10, 64) | ||
| } | ||
|
|
||
| func parseUint(s string, base, bitSize int) (uint64, error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /* | ||
| Copyright The containerd Authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| package cgroup1 | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
| ) | ||
|
|
||
| func BenchmarkReaduint64(b *testing.B) { | ||
| b.ReportAllocs() | ||
|
|
||
| buf := bytes.NewBuffer(nil) | ||
| for i := 0; i < b.N; i++ { | ||
| _, err := readUint("testdata/uint", buf) | ||
| if err != nil { | ||
| b.Fatal(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.
I like the idea!
I'm not sure how many users are using single subsystem. If we agree with this change, we should add comment about
Statfunction withit is not thread-safetyas warning.cc @AkihiroSuda
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.
It'd be any of the methods that call
readUintnot just Stat, but yea either a warning or some way to avoid this #275 (comment)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.
I wanted to get try this.
See: #277