-
Notifications
You must be signed in to change notification settings - Fork 27
Make intern.Table completely lock-free
#670
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
mcy
wants to merge
24
commits into
main
Choose a base branch
from
mcy/intern-amortize
base: main
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.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
9daced2
speedup
mcy bca6388
comments
mcy 437c66c
lint
mcy b32958f
get value to inline
mcy 785c902
wip
mcy 07fa3df
wip
mcy 744163e
use an atomic-ish log
mcy 4683a8f
fixes
mcy d6d38c8
better benchmark
mcy 54dc3f8
lint
mcy b1eed0e
add instrumentation
mcy 8d6a168
fix
mcy cd6ba22
benchmarks
mcy 6b4241c
add an inlining test
mcy 184427c
Merge remote-tracking branch 'origin/main' into mcy/intern-amortize
mcy 926f422
lockless
mcy 05899ca
fix spurious -race failure
mcy cb4a02f
cleanup
mcy a1a2fdf
lint
mcy d68bb4a
lint
mcy 1a7baa1
make misuse of log impossible
mcy 9a313c4
remove data race
mcy a1048db
unbreak
mcy 9eb9d65
ensure exit
mcy 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
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
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 |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // Copyright 2020-2025 Buf Technologies, Inc. | ||
| // | ||
| // 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 synctestx | ||
|
|
||
| import ( | ||
| "runtime" | ||
| "sync" | ||
| ) | ||
|
|
||
| // Hammer runs f across count goroutines, ensuring that f is called | ||
| // simultaneously, simulating a thundering herd. Returns once all spawned | ||
| // goroutines have exited. | ||
| // | ||
| // If count is zero, uses GOMAXPROCS instead. | ||
| func Hammer(count int, f func()) { | ||
| if count == 0 { | ||
| count = runtime.GOMAXPROCS(0) | ||
| } | ||
|
|
||
| start := new(sync.WaitGroup) | ||
| end := new(sync.WaitGroup) | ||
| for range count { | ||
| start.Add(1) | ||
| end.Add(1) | ||
| go func() { | ||
| defer end.Done() | ||
|
|
||
| // This ensures that we have a thundering herd situation: all of | ||
| // these goroutines wake up and hammer f() at the same time. | ||
| start.Done() | ||
| start.Wait() | ||
|
|
||
| f() | ||
| }() | ||
| } | ||
|
|
||
| end.Wait() | ||
| } |
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 |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // Copyright 2020-2025 Buf Technologies, Inc. | ||
| // | ||
| // 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. | ||
|
|
||
| //nolint:revive,predeclared | ||
| package syncx | ||
|
|
||
| import ( | ||
| "errors" | ||
| "math" | ||
| "runtime" | ||
| "sync/atomic" | ||
| "testing" | ||
| "unsafe" | ||
| ) | ||
|
|
||
| var ErrLogExhausted = errors.New("internal/syncx: cannot allocate more than 2^31 elements") | ||
|
|
||
| // Log is an append-only log. | ||
| // | ||
| // Loading and append operations may happen concurrently with each other. | ||
| // Can hold at most 2^31 elements. | ||
| type Log[T any] struct { | ||
| ptr atomic.Pointer[T] | ||
|
|
||
| next, len atomic.Int32 // The next index to fill. | ||
| cap atomic.Int32 // Top bit is used as a spinlock. | ||
| } | ||
|
|
||
| // Load returns the value at the given index. | ||
| // | ||
| // Panics if no value is at that index. | ||
| func (s *Log[T]) Load(idx int) T { | ||
| // Read cap first, which is required before we can read s.ptr. | ||
| len := s.len.Load() | ||
| ptr := s.ptr.Load() | ||
|
|
||
| return unsafe.Slice(ptr, len)[idx] | ||
| } | ||
|
|
||
| // Append adds a new value to this slice. | ||
| // | ||
| // Returns the index of the appended element, which can be looked up with | ||
| // [Log.Load]. | ||
| // | ||
| // Returns an error if indices are exhausted. | ||
| func (s *Log[T]) Append(v T) (int, error) { | ||
| i := s.next.Add(1) | ||
| if i < 0 { | ||
| return 0, ErrLogExhausted | ||
| } | ||
| i-- | ||
|
|
||
| // Wait for the capacity to be large enough for our index, or for us to | ||
| // be responsible for growing it (i == c). | ||
| c := s.cap.Load() | ||
| for i > c { | ||
| runtime.Gosched() | ||
| c = s.cap.Load() | ||
| } | ||
|
|
||
| // Fast path (i < c): slice is already large enough. | ||
| if i < c { | ||
| p := s.ptr.Load() | ||
| unsafe.Slice(p, c)[i] = v | ||
|
|
||
| s.len.Add(1) | ||
| for s.len.Load() <= i { | ||
emcfarlane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Make sure that every index before us also completes, to ensure | ||
| // that Load does not panic. | ||
| runtime.Gosched() | ||
| } | ||
|
|
||
| return int(i), nil | ||
| } | ||
|
|
||
| // Slow path (i == c): we are responsible for growing the slice. Need to | ||
| // wait until all fast-path writers to finish. Any further writers will | ||
| // spin in the i > c loop. | ||
| for s.len.Load() != c { | ||
| runtime.Gosched() | ||
| } | ||
|
|
||
| // Grow the slice. | ||
| // i == c, so we are appending exactly one element right now. | ||
| p := s.ptr.Load() | ||
| slice := append(unsafe.Slice(p, c), v) | ||
|
|
||
| // Publish the new slice to readers and waiting writers. | ||
| // Pointer must be stored before capacity to prevent out-of-bounds panics in Load. | ||
| s.ptr.Store(unsafe.SliceData(slice)) | ||
| s.cap.Store(int32(cap(slice))) | ||
|
|
||
| s.len.Add(1) | ||
| return int(i), nil | ||
| } | ||
|
|
||
| // SetFullForTesting sets this log to full, so that future calls to [Log.Append] | ||
| // panic. | ||
| // | ||
| // Must not be called concurrently. Can only be called from a unit test. | ||
| func (s *Log[T]) SetFullForTesting() { | ||
| if !testing.Testing() { | ||
| panic("called SetFull outside of a test") | ||
| } | ||
| s.next.Store(math.MaxInt32) | ||
| } | ||
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 |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // Copyright 2020-2025 Buf Technologies, Inc. | ||
| // | ||
| // 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 syncx_test | ||
|
|
||
| import ( | ||
| "math/rand" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
|
|
||
| "github.com/bufbuild/protocompile/internal/ext/synctestx" | ||
| "github.com/bufbuild/protocompile/internal/ext/syncx" | ||
| ) | ||
|
|
||
| func TestLog(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| const trials = 1000 | ||
|
|
||
| log := new(syncx.Log[int]) | ||
| synctestx.Hammer(0, func() { | ||
| for range trials { | ||
| n := rand.Int() | ||
| i, _ := log.Append(n) | ||
| assert.Equal(t, n, log.Load(i)) | ||
| } | ||
| }) | ||
|
|
||
| // Verify that mis-using an index panics. | ||
| i, _ := log.Append(0) | ||
| assert.Panics(t, func() { log.Load(i + 1) }) | ||
| } | ||
|
|
||
| func TestExhaust(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| log := new(syncx.Log[int]) | ||
| log.SetFullForTesting() | ||
|
|
||
| _, err := log.Append(0) | ||
| assert.Error(t, err) | ||
| } |
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 |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| // Copyright 2020-2025 Buf Technologies, Inc. | ||
| // | ||
| // 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 inlinetest | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "regexp" | ||
| "runtime/debug" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // AssertInlined returns whether the compiler is willing to inline the given | ||
| // symbols in the package being tested. | ||
| // | ||
| // The symbols must be either a single identifier or of the form Type.Method. | ||
| // Pointer-receiver methods should not use the (*Type).Method syntax. | ||
| func AssertInlined(t *testing.T, symbols ...string) { | ||
| t.Helper() | ||
| for _, symbol := range symbols { | ||
| _, ok := inlined[symbol] | ||
| assert.True(t, ok, "%s is not inlined", symbol) | ||
| } | ||
| } | ||
|
|
||
| var inlined = make(map[string]struct{}) | ||
|
|
||
| func init() { | ||
| if !testing.Testing() { | ||
| panic("inlinetest: cannot import inlinetest except in a test") | ||
| } | ||
|
|
||
| // This is based on a pattern of tests appearing in several places in Go's | ||
| // standard library. | ||
| tool := "go" | ||
| if env, ok := os.LookupEnv("GO"); ok { | ||
| tool = env | ||
| } | ||
|
|
||
| info, ok := debug.ReadBuildInfo() | ||
| if !ok { | ||
| panic(errors.New("inlinetest: could not read build info")) | ||
| } | ||
|
|
||
| //nolint:gosec | ||
| out, err := exec.Command( | ||
| tool, | ||
| "build", | ||
| "--gcflags=-m", // -m records optimization decisions. | ||
| strings.TrimSuffix(info.Path, ".test"), | ||
| ).CombinedOutput() | ||
| if err != nil { | ||
| panic(fmt.Errorf("inlinetest: go build failed: %w, %s", err, out)) | ||
| } | ||
|
|
||
| remarkRe := regexp.MustCompile(`(?m)^\./\S+\.go:\d+:\d+: can inline (.+?)$`) | ||
| ptrRe := regexp.MustCompile(`\(\*(.+)\)\.`) | ||
| for _, match := range remarkRe.FindAllSubmatch(out, -1) { | ||
| match := string(match[1]) | ||
| match = ptrRe.ReplaceAllString(match, "$1.") | ||
| inlined[match] = struct{}{} | ||
| } | ||
| } |
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.
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.