-
Notifications
You must be signed in to change notification settings - Fork 483
perf(lib/buffer_readwriter): avoid write lock while writing to buffer to reduce mutex contentions #616
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
Open
sambhav-jain-16
wants to merge
6
commits into
uber:master
Choose a base branch
from
sambhav-jain-16:lock-less-buffer
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+238
−38
Open
perf(lib/buffer_readwriter): avoid write lock while writing to buffer to reduce mutex contentions #616
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5192dbb
add benchmark for the buffer read writer
sambhav-jain-16 f7eb51a
update buffer read writer to use []byte array
sambhav-jain-16 7c1f476
add new test case for checking actual values
sambhav-jain-16 f3785c7
resolve comments
sambhav-jain-16 cc5ac87
fix docs
sambhav-jain-16 190c3fb
add test and update WriteAt
sambhav-jain-16 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,54 +17,96 @@ package base | |
| import ( | ||
| "fmt" | ||
| "io" | ||
|
|
||
| "github.com/aws/aws-sdk-go/aws" | ||
| "sync" | ||
| "sync/atomic" | ||
| ) | ||
|
|
||
| var _ FileReadWriter = &BufferReadWriter{} | ||
|
|
||
| // BufferReadWriter implements FileReadWriter interface for in-memory buffering. | ||
| // BufferReadWriter implements FileReadWriter for in-memory buffering. | ||
| // | ||
| // When created with size > 0, WriteAt takes a fast path using RLock, allowing | ||
| // concurrent goroutines writing to non-overlapping byte ranges to proceed in | ||
| // parallel with no serialization. The maximum written extent is tracked via an | ||
| // atomic so that Bytes(), Size(), Read, and ReadAt return only the data that | ||
| // was actually written. | ||
| // | ||
| // When created with size == 0, every WriteAt that extends the buffer acquires a | ||
| // full write lock to grow the backing slice. | ||
| // | ||
| // Write, Read, ReadAt, and Seek must not be called concurrently with each other | ||
| // or with WriteAt. | ||
| type BufferReadWriter struct { | ||
| buf *aws.WriteAtBuffer | ||
| offset int64 | ||
| mu sync.RWMutex | ||
| buf []byte | ||
| written atomic.Int64 | ||
| offset int64 | ||
| } | ||
|
|
||
| // NewBufferReadWriter creates a new BufferReadWriter with an initial capacity of size bytes. | ||
| // NewBufferReadWriter creates a new BufferReadWriter pre-allocated to size bytes. | ||
| // Pass the exact blob size when known to enable lock-free | ||
| // concurrent WriteAt calls for non-overlapping shard ranges. | ||
| func NewBufferReadWriter(size uint64) *BufferReadWriter { | ||
| bytesSlice := make([]byte, 0, size) | ||
| buf := aws.NewWriteAtBuffer(bytesSlice) | ||
| // Although this is default, this is explicitly set to notify that we are reserving | ||
| // only as much capacity as needed | ||
| buf.GrowthCoeff = 1 | ||
|
|
||
| return &BufferReadWriter{ | ||
| buf: buf, | ||
| offset: 0, | ||
| } | ||
| return &BufferReadWriter{buf: make([]byte, size)} | ||
|
Collaborator
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. Now that we do
|
||
| } | ||
|
sambhav-jain-16 marked this conversation as resolved.
|
||
|
|
||
| // Write implements io.Writer by using WriteAt with current write offset. | ||
| // Write implements io.Writer using the current sequential write offset. | ||
| func (b *BufferReadWriter) Write(p []byte) (n int, err error) { | ||
| n, err = b.buf.WriteAt(p, b.offset) | ||
| n, err = b.WriteAt(p, b.offset) | ||
| b.offset += int64(n) | ||
| return n, err | ||
| } | ||
|
|
||
| // WriteAt implements io.WriterAt for parallel writes. | ||
| func (b *BufferReadWriter) WriteAt(p []byte, off int64) (n int, err error) { | ||
| // WriteAt implements io.WriterAt. | ||
| // | ||
| // Fast path (off+len(p) within pre-allocated buffer): multiple goroutines may | ||
| // call WriteAt concurrently, provided their byte ranges do not overlap. | ||
| // Slow path (write extends beyond current buffer): acquires an exclusive lock | ||
| // to grow the buffer, then writes. | ||
| func (b *BufferReadWriter) WriteAt(p []byte, off int64) (int, error) { | ||
| if off < 0 { | ||
| return 0, fmt.Errorf("negative offset") | ||
| } | ||
| return b.buf.WriteAt(p, off) | ||
| end := off + int64(len(p)) | ||
|
sambhav-jain-16 marked this conversation as resolved.
|
||
| if end < off { | ||
| return 0, fmt.Errorf("write at offset %d length %d overflows int64", off, len(p)) | ||
| } | ||
|
|
||
| b.mu.RLock() | ||
| if end <= int64(len(b.buf)) { | ||
|
sambhav-jain-16 marked this conversation as resolved.
|
||
| n := copy(b.buf[off:], p) | ||
| for { | ||
|
sambhav-jain-16 marked this conversation as resolved.
sambhav-jain-16 marked this conversation as resolved.
sambhav-jain-16 marked this conversation as resolved.
|
||
| cur := b.written.Load() | ||
| if end <= cur || b.written.CompareAndSwap(cur, end) { | ||
| break | ||
| } | ||
| } | ||
| b.mu.RUnlock() | ||
| return n, nil | ||
| } | ||
| b.mu.RUnlock() | ||
|
|
||
| b.mu.Lock() | ||
| defer b.mu.Unlock() | ||
| if end > int64(len(b.buf)) { | ||
| grown := make([]byte, end) | ||
| copy(grown, b.buf) | ||
| b.buf = grown | ||
| } | ||
| n := copy(b.buf[off:], p) | ||
| if end > b.written.Load() { | ||
| b.written.Store(end) | ||
| } | ||
| return n, nil | ||
| } | ||
|
|
||
| // Read implements io.Reader for sequential reads. | ||
| func (b *BufferReadWriter) Read(p []byte) (n int, err error) { | ||
| bufBytes := b.buf.Bytes() | ||
| if b.offset >= int64(len(bufBytes)) { | ||
| written := b.written.Load() | ||
| if b.offset >= written { | ||
| return 0, io.EOF | ||
| } | ||
| n = copy(p, bufBytes[b.offset:]) | ||
| n = copy(p, b.buf[b.offset:written]) | ||
| b.offset += int64(n) | ||
|
sambhav-jain-16 marked this conversation as resolved.
sambhav-jain-16 marked this conversation as resolved.
|
||
| if n < len(p) { | ||
| err = io.EOF | ||
|
|
@@ -77,11 +119,14 @@ func (b *BufferReadWriter) ReadAt(p []byte, off int64) (n int, err error) { | |
| if off < 0 { | ||
| return 0, fmt.Errorf("negative offset") | ||
| } | ||
| bufBytes := b.buf.Bytes() | ||
| if off >= int64(len(bufBytes)) { | ||
| b.mu.RLock() | ||
| buf := b.buf | ||
| written := b.written.Load() | ||
| b.mu.RUnlock() | ||
| if off >= written { | ||
| return 0, io.EOF | ||
| } | ||
| n = copy(p, bufBytes[off:]) | ||
| n = copy(p, buf[off:written]) | ||
| if n < len(p) { | ||
|
sambhav-jain-16 marked this conversation as resolved.
sambhav-jain-16 marked this conversation as resolved.
|
||
| err = io.EOF | ||
| } | ||
|
|
@@ -91,23 +136,19 @@ func (b *BufferReadWriter) ReadAt(p []byte, off int64) (n int, err error) { | |
| // Seek implements io.Seeker. | ||
| func (b *BufferReadWriter) Seek(offset int64, whence int) (int64, error) { | ||
| var newOffset int64 | ||
| bufSize := int64(len(b.buf.Bytes())) | ||
|
|
||
| switch whence { | ||
| case io.SeekStart: | ||
| newOffset = offset | ||
| case io.SeekCurrent: | ||
| newOffset = b.offset + offset | ||
| case io.SeekEnd: | ||
| newOffset = bufSize + offset | ||
| newOffset = b.written.Load() + offset | ||
| default: | ||
| return 0, fmt.Errorf("invalid whence: %d", whence) | ||
| } | ||
|
|
||
| if newOffset < 0 { | ||
| return 0, fmt.Errorf("negative position: %d", newOffset) | ||
| } | ||
|
|
||
| b.offset = newOffset | ||
| return newOffset, nil | ||
| } | ||
|
|
@@ -117,10 +158,8 @@ func (b *BufferReadWriter) Close() error { | |
| return nil | ||
| } | ||
|
|
||
| // Size returns the size of the buffer | ||
| func (b *BufferReadWriter) Size() int64 { | ||
| return int64(len(b.buf.Bytes())) | ||
| } | ||
| // Size returns the largest end offset written so far. | ||
| func (b *BufferReadWriter) Size() int64 { return b.written.Load() } | ||
|
|
||
| // Cancel is no-op | ||
| func (b *BufferReadWriter) Cancel() error { | ||
|
|
@@ -132,7 +171,7 @@ func (b *BufferReadWriter) Commit() error { | |
| return nil | ||
| } | ||
|
|
||
| // Bytes returns the full buffer | ||
| // Bytes returns the bytes that have been written so far. | ||
| func (b *BufferReadWriter) Bytes() []byte { | ||
| return b.buf.Bytes() | ||
| return b.buf[:b.written.Load()] | ||
|
sambhav-jain-16 marked this conversation as resolved.
sambhav-jain-16 marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.