Cap initial usermem.CopyStringIn buffer size.

Almost (?) all uses of CopyStringIn are via linux.copyInPath(), which
passes maxlen = linux.PATH_MAX = 4096. Pre-allocating a buffer of this
size is measurably inefficient in most cases: most paths will not be
this long, 4 KB is a lot of bytes to zero, and as of this writing the Go
runtime allocator maps only two 4 KB objects to each 8 KB span,
necessitating a call to runtime.mcache.refill() on ~every other call.
Limit the initial buffer size to 256 B instead, and geometrically
reallocate if necessary.

PiperOrigin-RevId: 251960441
This commit is contained in:
Jamie Liu
2019-06-06 17:22:00 -07:00
committed by Shentubot
parent 315cf9a523
commit 9ea248489b
2 changed files with 37 additions and 14 deletions
+23 -13
View File
@@ -222,9 +222,11 @@ func CopyObjectIn(ctx context.Context, uio IO, addr Addr, dst interface{}, opts
return int(r.Addr - addr), nil
}
// copyStringIncrement is the maximum number of bytes that are copied from
// virtual memory at a time by CopyStringIn.
const copyStringIncrement = 64
// CopyStringIn tuning parameters, defined outside that function for tests.
const (
copyStringIncrement = 64
copyStringMaxInitBufLen = 256
)
// CopyStringIn copies a NUL-terminated string of unknown length from the
// memory mapped at addr in uio and returns it as a string (not including the
@@ -234,31 +236,38 @@ const copyStringIncrement = 64
//
// Preconditions: As for IO.CopyFromUser. maxlen >= 0.
func CopyStringIn(ctx context.Context, uio IO, addr Addr, maxlen int, opts IOOpts) (string, error) {
buf := make([]byte, maxlen)
initLen := maxlen
if initLen > copyStringMaxInitBufLen {
initLen = copyStringMaxInitBufLen
}
buf := make([]byte, initLen)
var done int
for done < maxlen {
start, ok := addr.AddLength(uint64(done))
if !ok {
// Last page of kernel memory. The application can't use this
// anyway.
return stringFromImmutableBytes(buf[:done]), syserror.EFAULT
}
// Read up to copyStringIncrement bytes at a time.
readlen := copyStringIncrement
if readlen > maxlen-done {
readlen = maxlen - done
}
end, ok := start.AddLength(uint64(readlen))
end, ok := addr.AddLength(uint64(readlen))
if !ok {
return stringFromImmutableBytes(buf[:done]), syserror.EFAULT
}
// Shorten the read to avoid crossing page boundaries, since faulting
// in a page unnecessarily is expensive. This also ensures that partial
// copies up to the end of application-mappable memory succeed.
if start.RoundDown() != end.RoundDown() {
if addr.RoundDown() != end.RoundDown() {
end = end.RoundDown()
readlen = int(end - addr)
}
n, err := uio.CopyIn(ctx, start, buf[done:done+int(end-start)], opts)
// Ensure that our buffer is large enough to accommodate the read.
if done+readlen > len(buf) {
newBufLen := len(buf) * 2
if newBufLen > maxlen {
newBufLen = maxlen
}
buf = append(buf, make([]byte, newBufLen-len(buf))...)
}
n, err := uio.CopyIn(ctx, addr, buf[done:done+readlen], opts)
// Look for the terminating zero byte, which may have occurred before
// hitting err.
for i, c := range buf[done : done+n] {
@@ -270,6 +279,7 @@ func CopyStringIn(ctx context.Context, uio IO, addr Addr, maxlen int, opts IOOpt
if err != nil {
return stringFromImmutableBytes(buf[:done]), err
}
addr = end
}
return stringFromImmutableBytes(buf), syserror.ENAMETOOLONG
}
+14 -1
View File
@@ -192,6 +192,7 @@ func TestCopyObject(t *testing.T) {
}
func TestCopyStringInShort(t *testing.T) {
// Tests for string length <= copyStringIncrement.
want := strings.Repeat("A", copyStringIncrement-2)
mem := want + "\x00"
if got, err := CopyStringIn(newContext(), newBytesIOString(mem), 0, 2*copyStringIncrement, IOOpts{}); got != want || err != nil {
@@ -200,13 +201,25 @@ func TestCopyStringInShort(t *testing.T) {
}
func TestCopyStringInLong(t *testing.T) {
want := strings.Repeat("A", copyStringIncrement+1)
// Tests for copyStringIncrement < string length <= copyStringMaxInitBufLen
// (requiring multiple calls to IO.CopyIn()).
want := strings.Repeat("A", copyStringIncrement*3/4) + strings.Repeat("B", copyStringIncrement*3/4)
mem := want + "\x00"
if got, err := CopyStringIn(newContext(), newBytesIOString(mem), 0, 2*copyStringIncrement, IOOpts{}); got != want || err != nil {
t.Errorf("CopyStringIn: got (%q, %v), wanted (%q, nil)", got, err, want)
}
}
func TestCopyStringInVeryLong(t *testing.T) {
// Tests for string length > copyStringMaxInitBufLen (requiring buffer
// reallocation).
want := strings.Repeat("A", copyStringMaxInitBufLen*3/4) + strings.Repeat("B", copyStringMaxInitBufLen*3/4)
mem := want + "\x00"
if got, err := CopyStringIn(newContext(), newBytesIOString(mem), 0, 2*copyStringMaxInitBufLen, IOOpts{}); got != want || err != nil {
t.Errorf("CopyStringIn: got (%q, %v), wanted (%q, nil)", got, err, want)
}
}
func TestCopyStringInNoTerminatingZeroByte(t *testing.T) {
want := strings.Repeat("A", copyStringIncrement-1)
got, err := CopyStringIn(newContext(), newBytesIOString(want), 0, 2*copyStringIncrement, IOOpts{})