buffer: refactor to a form that won't require memclr.

Necessary because our evil memclr should go away.

Benchmarks:

    benchmark                                          old ns/op     new ns/op     delta
    BenchmarkOutMessageReset/Single_buffer-12          4.94          4.95          +0.20%
    BenchmarkOutMessageReset/Many_buffers-12           10.4          12.0          +15.38%
    BenchmarkOutMessageGrowShrink/Single_buffer-12     42861         47666         +11.21%
    BenchmarkOutMessageGrowShrink/Many_buffers-12      101896        100626        -1.25%

    benchmark                                          old MB/s     new MB/s     speedup
    BenchmarkOutMessageReset/Single_buffer-12          4853.85      4851.64      1.00x
    BenchmarkOutMessageReset/Many_buffers-12           2311.82      2006.72      0.87x
    BenchmarkOutMessageGrowShrink/Single_buffer-12     24464.39     21998.19     0.90x
    BenchmarkOutMessageGrowShrink/Many_buffers-12      10290.60     10420.48     1.01x
geesefs-0-30-9
Aaron Jacobs 2016-12-19 13:19:31 +11:00
commit 641629d124
3 changed files with 99 additions and 88 deletions

View File

@ -279,7 +279,7 @@ func convertInMessage(
o = to o = to
readSize := int(in.Size) readSize := int(in.Size)
p := outMsg.GrowNoZero(uintptr(readSize)) p := outMsg.GrowNoZero(readSize)
if p == nil { if p == nil {
err = fmt.Errorf("Can't grow for %d-byte read", readSize) err = fmt.Errorf("Can't grow for %d-byte read", readSize)
return return
@ -305,7 +305,7 @@ func convertInMessage(
o = to o = to
readSize := int(in.Size) readSize := int(in.Size)
p := outMsg.GrowNoZero(uintptr(readSize)) p := outMsg.GrowNoZero(readSize)
if p == nil { if p == nil {
err = fmt.Errorf("Can't grow for %d-byte read", readSize) err = fmt.Errorf("Can't grow for %d-byte read", readSize)
return return
@ -469,7 +469,7 @@ func (c *Connection) kernelResponse(
// the header, because on OS X the kernel otherwise returns EINVAL when we // the header, because on OS X the kernel otherwise returns EINVAL when we
// attempt to write an error response with a length that extends beyond the // attempt to write an error response with a length that extends beyond the
// header. // header.
m.ShrinkTo(buffer.OutMessageInitialSize) m.ShrinkTo(buffer.OutMessageHeaderSize)
} }
// Otherwise, fill in the rest of the response. // Otherwise, fill in the rest of the response.
@ -489,45 +489,45 @@ func (c *Connection) kernelResponseForOp(
// Create the appropriate output message // Create the appropriate output message
switch o := op.(type) { switch o := op.(type) {
case *fuseops.LookUpInodeOp: case *fuseops.LookUpInodeOp:
size := fusekernel.EntryOutSize(c.protocol) size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size)) out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out) convertChildInodeEntry(&o.Entry, out)
case *fuseops.GetInodeAttributesOp: case *fuseops.GetInodeAttributesOp:
size := fusekernel.AttrOutSize(c.protocol) size := int(fusekernel.AttrOutSize(c.protocol))
out := (*fusekernel.AttrOut)(m.Grow(size)) out := (*fusekernel.AttrOut)(m.Grow(size))
out.AttrValid, out.AttrValidNsec = convertExpirationTime( out.AttrValid, out.AttrValidNsec = convertExpirationTime(
o.AttributesExpiration) o.AttributesExpiration)
convertAttributes(o.Inode, &o.Attributes, &out.Attr) convertAttributes(o.Inode, &o.Attributes, &out.Attr)
case *fuseops.SetInodeAttributesOp: case *fuseops.SetInodeAttributesOp:
size := fusekernel.AttrOutSize(c.protocol) size := int(fusekernel.AttrOutSize(c.protocol))
out := (*fusekernel.AttrOut)(m.Grow(size)) out := (*fusekernel.AttrOut)(m.Grow(size))
out.AttrValid, out.AttrValidNsec = convertExpirationTime( out.AttrValid, out.AttrValidNsec = convertExpirationTime(
o.AttributesExpiration) o.AttributesExpiration)
convertAttributes(o.Inode, &o.Attributes, &out.Attr) convertAttributes(o.Inode, &o.Attributes, &out.Attr)
case *fuseops.MkDirOp: case *fuseops.MkDirOp:
size := fusekernel.EntryOutSize(c.protocol) size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size)) out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out) convertChildInodeEntry(&o.Entry, out)
case *fuseops.MkNodeOp: case *fuseops.MkNodeOp:
size := fusekernel.EntryOutSize(c.protocol) size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size)) out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out) convertChildInodeEntry(&o.Entry, out)
case *fuseops.CreateFileOp: case *fuseops.CreateFileOp:
eSize := fusekernel.EntryOutSize(c.protocol) eSize := int(fusekernel.EntryOutSize(c.protocol))
e := (*fusekernel.EntryOut)(m.Grow(eSize)) e := (*fusekernel.EntryOut)(m.Grow(eSize))
convertChildInodeEntry(&o.Entry, e) convertChildInodeEntry(&o.Entry, e)
oo := (*fusekernel.OpenOut)(m.Grow(unsafe.Sizeof(fusekernel.OpenOut{}))) oo := (*fusekernel.OpenOut)(m.Grow(int(unsafe.Sizeof(fusekernel.OpenOut{}))))
oo.Fh = uint64(o.Handle) oo.Fh = uint64(o.Handle)
case *fuseops.CreateSymlinkOp: case *fuseops.CreateSymlinkOp:
size := fusekernel.EntryOutSize(c.protocol) size := int(fusekernel.EntryOutSize(c.protocol))
out := (*fusekernel.EntryOut)(m.Grow(size)) out := (*fusekernel.EntryOut)(m.Grow(size))
convertChildInodeEntry(&o.Entry, out) convertChildInodeEntry(&o.Entry, out)
@ -541,20 +541,20 @@ func (c *Connection) kernelResponseForOp(
// Empty response // Empty response
case *fuseops.OpenDirOp: case *fuseops.OpenDirOp:
out := (*fusekernel.OpenOut)(m.Grow(unsafe.Sizeof(fusekernel.OpenOut{}))) out := (*fusekernel.OpenOut)(m.Grow(int(unsafe.Sizeof(fusekernel.OpenOut{}))))
out.Fh = uint64(o.Handle) out.Fh = uint64(o.Handle)
case *fuseops.ReadDirOp: case *fuseops.ReadDirOp:
// convertInMessage already set up the destination buffer to be at the end // convertInMessage already set up the destination buffer to be at the end
// of the out message. We need only shrink to the right size based on how // of the out message. We need only shrink to the right size based on how
// much the user read. // much the user read.
m.ShrinkTo(buffer.OutMessageInitialSize + uintptr(o.BytesRead)) m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead)
case *fuseops.ReleaseDirHandleOp: case *fuseops.ReleaseDirHandleOp:
// Empty response // Empty response
case *fuseops.OpenFileOp: case *fuseops.OpenFileOp:
out := (*fusekernel.OpenOut)(m.Grow(unsafe.Sizeof(fusekernel.OpenOut{}))) out := (*fusekernel.OpenOut)(m.Grow(int(unsafe.Sizeof(fusekernel.OpenOut{}))))
out.Fh = uint64(o.Handle) out.Fh = uint64(o.Handle)
if o.KeepPageCache { if o.KeepPageCache {
@ -565,10 +565,10 @@ func (c *Connection) kernelResponseForOp(
// convertInMessage already set up the destination buffer to be at the end // convertInMessage already set up the destination buffer to be at the end
// of the out message. We need only shrink to the right size based on how // of the out message. We need only shrink to the right size based on how
// much the user read. // much the user read.
m.ShrinkTo(buffer.OutMessageInitialSize + uintptr(o.BytesRead)) m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead)
case *fuseops.WriteFileOp: case *fuseops.WriteFileOp:
out := (*fusekernel.WriteOut)(m.Grow(unsafe.Sizeof(fusekernel.WriteOut{}))) out := (*fusekernel.WriteOut)(m.Grow(int(unsafe.Sizeof(fusekernel.WriteOut{}))))
out.Size = uint32(len(o.Data)) out.Size = uint32(len(o.Data))
case *fuseops.SyncFileOp: case *fuseops.SyncFileOp:
@ -584,7 +584,7 @@ func (c *Connection) kernelResponseForOp(
m.AppendString(o.Target) m.AppendString(o.Target)
case *fuseops.StatFSOp: case *fuseops.StatFSOp:
out := (*fusekernel.StatfsOut)(m.Grow(unsafe.Sizeof(fusekernel.StatfsOut{}))) out := (*fusekernel.StatfsOut)(m.Grow(int(unsafe.Sizeof(fusekernel.StatfsOut{}))))
out.St.Blocks = o.Blocks out.St.Blocks = o.Blocks
out.St.Bfree = o.BlocksFree out.St.Bfree = o.BlocksFree
out.St.Bavail = o.BlocksAvailable out.St.Bavail = o.BlocksAvailable
@ -620,7 +620,7 @@ func (c *Connection) kernelResponseForOp(
out.St.Frsize = o.BlockSize out.St.Frsize = o.BlockSize
case *initOp: case *initOp:
out := (*fusekernel.InitOut)(m.Grow(unsafe.Sizeof(fusekernel.InitOut{}))) out := (*fusekernel.InitOut)(m.Grow(int(unsafe.Sizeof(fusekernel.InitOut{}))))
out.Major = o.Library.Major out.Major = o.Library.Major
out.Minor = o.Library.Minor out.Minor = o.Library.Minor

View File

@ -23,14 +23,9 @@ import (
"github.com/jacobsa/fuse/internal/fusekernel" "github.com/jacobsa/fuse/internal/fusekernel"
) )
const outHeaderSize = unsafe.Sizeof(fusekernel.OutHeader{}) // OutMessageHeaderSize is the size of the leading header in every
// properly-constructed OutMessage. Reset brings the message back to this size.
// OutMessage structs begin life with Len() == OutMessageInitialSize. const OutMessageHeaderSize = int(unsafe.Sizeof(fusekernel.OutHeader{}))
const OutMessageInitialSize = outHeaderSize
// We size out messages to be large enough to hold a header for the response
// plus the largest read that may come in.
const outMessageSize = outHeaderSize + MaxReadSize
// OutMessage provides a mechanism for constructing a single contiguous fuse // OutMessage provides a mechanism for constructing a single contiguous fuse
// message from multiple segments, where the first segment is always a // message from multiple segments, where the first segment is always a
@ -38,72 +33,81 @@ const outMessageSize = outHeaderSize + MaxReadSize
// //
// Must be initialized with Reset. // Must be initialized with Reset.
type OutMessage struct { type OutMessage struct {
offset uintptr // The offset into payload to which we're currently writing.
storage [outMessageSize]byte payloadOffset int
header fusekernel.OutHeader
payload [MaxReadSize]byte
} }
// Make sure alignment works out correctly, at least for the header. // Make sure that the header and payload are contiguous.
func init() { func init() {
a := unsafe.Alignof(OutMessage{}) a := unsafe.Offsetof(OutMessage{}.header) + uintptr(OutMessageHeaderSize)
o := unsafe.Offsetof(OutMessage{}.storage) b := unsafe.Offsetof(OutMessage{}.payload)
e := unsafe.Alignof(fusekernel.OutHeader{})
if a%e != 0 || o%e != 0 { if a != b {
log.Panicf("Bad alignment or offset: %d, %d, need %d", a, o, e) log.Panicf(
"header ends at offset %d, but payload starts at offset %d",
a, b)
} }
} }
// Reset the message so that it is ready to be used again. Afterward, the // Reset resets m so that it's ready to be used again. Afterward, the contents
// contents are solely a zeroed header. // are solely a zeroed fusekernel.OutHeader struct.
func (m *OutMessage) Reset() { func (m *OutMessage) Reset() {
m.offset = OutMessageInitialSize m.payloadOffset = 0
memclr(unsafe.Pointer(&m.storage), OutMessageInitialSize) memclr(unsafe.Pointer(&m.header), uintptr(OutMessageHeaderSize))
} }
// Return a pointer to the header at the start of the message. // OutHeader returns a pointer to the header at the start of the message.
func (b *OutMessage) OutHeader() (h *fusekernel.OutHeader) { func (m *OutMessage) OutHeader() *fusekernel.OutHeader {
h = (*fusekernel.OutHeader)(unsafe.Pointer(&b.storage)) return &m.header
return
} }
// Grow the buffer by the supplied number of bytes, returning a pointer to the // Grow grows m's buffer by the given number of bytes, returning a pointer to
// start of the new segment, which is zeroed. If there is no space left, return // the start of the new segment, which is guaranteed to be zeroed. If there is
// the nil pointer. // insufficient space, it returns nil.
func (b *OutMessage) Grow(size uintptr) (p unsafe.Pointer) { func (m *OutMessage) Grow(n int) (p unsafe.Pointer) {
p = b.GrowNoZero(size) p = m.GrowNoZero(n)
if p != nil { if p != nil {
memclr(p, size) memclr(p, uintptr(n))
} }
return return
} }
// Equivalent to Grow, except the new segment is not zeroed. Use with caution! // GrowNoZero is equivalent to Grow, except the new segment is not zeroed. Use
func (b *OutMessage) GrowNoZero(size uintptr) (p unsafe.Pointer) { // with caution!
if outMessageSize-b.offset < size { func (m *OutMessage) GrowNoZero(n int) (p unsafe.Pointer) {
// Will we overflow the buffer?
o := m.payloadOffset
if len(m.payload)-o < n {
return return
} }
p = unsafe.Pointer(uintptr(unsafe.Pointer(&b.storage)) + b.offset) p = unsafe.Pointer(uintptr(unsafe.Pointer(&m.payload)) + uintptr(o))
b.offset += size m.payloadOffset = o + n
return return
} }
// Shrink to the supplied size. Panic if the size is greater than Len() or less // ShrinkTo shrinks m to the given size. It panics if the size is greater than
// than OutMessageInitialSize. // Len() or less than OutMessageHeaderSize.
func (b *OutMessage) ShrinkTo(n uintptr) { func (m *OutMessage) ShrinkTo(n int) {
if n < OutMessageInitialSize || n > b.offset { if n < OutMessageHeaderSize || n > m.Len() {
panic(fmt.Sprintf("ShrinkTo(%d) out of range for offset %d", n, b.offset)) panic(fmt.Sprintf(
"ShrinkTo(%d) out of range (current Len: %d)",
n,
m.Len()))
} }
b.offset = n m.payloadOffset = n - OutMessageHeaderSize
} }
// Equivalent to growing by the length of p, then copying p over the new // Append is equivalent to growing by len(src), then copying src over the new
// segment. Panics if there is not enough room available. // segment. Int panics if there is not enough room available.
func (b *OutMessage) Append(src []byte) { func (m *OutMessage) Append(src []byte) {
p := b.GrowNoZero(uintptr(len(src))) p := m.GrowNoZero(len(src))
if p == nil { if p == nil {
panic(fmt.Sprintf("Can't grow %d bytes", len(src))) panic(fmt.Sprintf("Can't grow %d bytes", len(src)))
} }
@ -114,10 +118,9 @@ func (b *OutMessage) Append(src []byte) {
return return
} }
// Equivalent to growing by the length of s, then copying s over the new // AppendString is like Append, but accepts string input.
// segment. Panics if there is not enough room available. func (m *OutMessage) AppendString(src string) {
func (b *OutMessage) AppendString(src string) { p := m.GrowNoZero(len(src))
p := b.GrowNoZero(uintptr(len(src)))
if p == nil { if p == nil {
panic(fmt.Sprintf("Can't grow %d bytes", len(src))) panic(fmt.Sprintf("Can't grow %d bytes", len(src)))
} }
@ -128,12 +131,20 @@ func (b *OutMessage) AppendString(src string) {
return return
} }
// Return the current size of the buffer. // Len returns the current size of the message, including the leading header.
func (b *OutMessage) Len() int { func (m *OutMessage) Len() int {
return int(b.offset) return OutMessageHeaderSize + m.payloadOffset
} }
// Return a reference to the current contents of the buffer. // Bytes returns a reference to the current contents of the buffer, including
func (b *OutMessage) Bytes() []byte { // the leading header.
return b.storage[:int(b.offset)] func (m *OutMessage) Bytes() []byte {
l := m.Len()
sh := reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(&m.header)),
Len: l,
Cap: l,
}
return *(*[]byte)(unsafe.Pointer(&sh))
} }

View File

@ -101,7 +101,7 @@ func TestOutMessageAppend(t *testing.T) {
om.Append(wantPayload[4:]) om.Append(wantPayload[4:])
// The result should be a zeroed header followed by the desired payload. // The result should be a zeroed header followed by the desired payload.
const wantLen = int(OutMessageInitialSize) + len(wantPayloadStr) const wantLen = OutMessageHeaderSize + len(wantPayloadStr)
if got, want := om.Len(), wantLen; got != want { if got, want := om.Len(), wantLen; got != want {
t.Errorf("om.Len() = %d, want %d", got, want) t.Errorf("om.Len() = %d, want %d", got, want)
@ -113,7 +113,7 @@ func TestOutMessageAppend(t *testing.T) {
} }
want := append( want := append(
make([]byte, OutMessageInitialSize), make([]byte, OutMessageHeaderSize),
wantPayload...) wantPayload...)
if !bytes.Equal(b, want) { if !bytes.Equal(b, want) {
@ -131,7 +131,7 @@ func TestOutMessageAppendString(t *testing.T) {
om.AppendString(wantPayload[4:]) om.AppendString(wantPayload[4:])
// The result should be a zeroed header followed by the desired payload. // The result should be a zeroed header followed by the desired payload.
const wantLen = int(OutMessageInitialSize) + len(wantPayload) const wantLen = OutMessageHeaderSize + len(wantPayload)
if got, want := om.Len(), wantLen; got != want { if got, want := om.Len(), wantLen; got != want {
t.Errorf("om.Len() = %d, want %d", got, want) t.Errorf("om.Len() = %d, want %d", got, want)
@ -143,7 +143,7 @@ func TestOutMessageAppendString(t *testing.T) {
} }
want := append( want := append(
make([]byte, OutMessageInitialSize), make([]byte, OutMessageHeaderSize),
wantPayload...) wantPayload...)
if !bytes.Equal(b, want) { if !bytes.Equal(b, want) {
@ -159,10 +159,10 @@ func TestOutMessageShrinkTo(t *testing.T) {
om.AppendString("burrito") om.AppendString("burrito")
// Shrink it. // Shrink it.
om.ShrinkTo(OutMessageInitialSize + uintptr(len("taco"))) om.ShrinkTo(OutMessageHeaderSize + len("taco"))
// The result should be a zeroed header followed by "taco". // The result should be a zeroed header followed by "taco".
const wantLen = int(OutMessageInitialSize) + len("taco") const wantLen = OutMessageHeaderSize + len("taco")
if got, want := om.Len(), wantLen; got != want { if got, want := om.Len(), wantLen; got != want {
t.Errorf("om.Len() = %d, want %d", got, want) t.Errorf("om.Len() = %d, want %d", got, want)
@ -174,7 +174,7 @@ func TestOutMessageShrinkTo(t *testing.T) {
} }
want := append( want := append(
make([]byte, OutMessageInitialSize), make([]byte, OutMessageHeaderSize),
"taco"...) "taco"...)
if !bytes.Equal(b, want) { if !bytes.Equal(b, want) {
@ -233,7 +233,7 @@ func TestOutMessageReset(t *testing.T) {
om.Reset() om.Reset()
// Check that the length was updated. // Check that the length was updated.
if got, want := int(om.Len()), int(OutMessageInitialSize); got != want { if got, want := om.Len(), OutMessageHeaderSize; got != want {
t.Fatalf("om.Len() = %d, want %d", got, want) t.Fatalf("om.Len() = %d, want %d", got, want)
} }
@ -269,7 +269,7 @@ func TestOutMessageGrow(t *testing.T) {
t.Fatalf("fillWithGarbage: %v", err) t.Fatalf("fillWithGarbage: %v", err)
} }
om.ShrinkTo(OutMessageInitialSize) om.ShrinkTo(OutMessageHeaderSize)
} }
// Call Grow. // Call Grow.
@ -278,7 +278,7 @@ func TestOutMessageGrow(t *testing.T) {
} }
// Check the resulting length in two ways. // Check the resulting length in two ways.
const wantLen = int(payloadSize + OutMessageInitialSize) const wantLen = payloadSize + OutMessageHeaderSize
if got, want := om.Len(), wantLen; got != want { if got, want := om.Len(), wantLen; got != want {
t.Errorf("om.Len() = %d, want %d", got) t.Errorf("om.Len() = %d, want %d", got)
} }
@ -289,7 +289,7 @@ func TestOutMessageGrow(t *testing.T) {
} }
// Check that the payload was zeroed. // Check that the payload was zeroed.
for i, x := range b[OutMessageInitialSize:] { for i, x := range b[OutMessageHeaderSize:] {
if x != 0 { if x != 0 {
t.Fatalf("non-zero byte 0x%02x at payload offset %d", x, i) t.Fatalf("non-zero byte 0x%02x at payload offset %d", x, i)
} }
@ -304,7 +304,7 @@ func BenchmarkOutMessageReset(b *testing.B) {
om.Reset() om.Reset()
} }
b.SetBytes(int64(unsafe.Offsetof(om.storage)) + int64(om.offset)) b.SetBytes(int64(unsafe.Offsetof(om.payload)))
}) })
// Many megabytes worth of buffers, which should defeat the CPU cache. // Many megabytes worth of buffers, which should defeat the CPU cache.
@ -321,7 +321,7 @@ func BenchmarkOutMessageReset(b *testing.B) {
oms[i%numMessages].Reset() oms[i%numMessages].Reset()
} }
b.SetBytes(int64(unsafe.Offsetof(oms[0].storage)) + int64(oms[0].offset)) b.SetBytes(int64(unsafe.Offsetof(oms[0].payload)))
}) })
} }
@ -331,7 +331,7 @@ func BenchmarkOutMessageGrowShrink(b *testing.B) {
var om OutMessage var om OutMessage
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
om.Grow(MaxReadSize) om.Grow(MaxReadSize)
om.ShrinkTo(OutMessageInitialSize) om.ShrinkTo(OutMessageHeaderSize)
} }
b.SetBytes(int64(MaxReadSize)) b.SetBytes(int64(MaxReadSize))
@ -349,7 +349,7 @@ func BenchmarkOutMessageGrowShrink(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
oms[i%numMessages].Grow(MaxReadSize) oms[i%numMessages].Grow(MaxReadSize)
oms[i%numMessages].ShrinkTo(OutMessageInitialSize) oms[i%numMessages].ShrinkTo(OutMessageHeaderSize)
} }
b.SetBytes(int64(MaxReadSize)) b.SetBytes(int64(MaxReadSize))