mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-10-28 16:36:04 +08:00
Compress paged link-store references
Dense checker link stores still keep a pointer for every occupied slot. Those tables cost memory and require GC scanning even though values already live in arenas. Use four-byte indexes in the page tables. Retain bounded arena blocks so references stay stable without geometrically over-allocating large stores. Keep map-backed stores unchanged: compressing those did not improve allocation usage.
This commit is contained in:
1 parent
6c976dad5d
commit
455ef7b3ae
3 files changed
+196
-11
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"slices"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -69,3 +70,41 @@ func nextArenaSize[T any](size int) int {
|
||||
size = min(size*2, 256, maxSize)
|
||||
return size
|
||||
}
|
||||
|
||||
// IndexedArena keeps values at stable addresses and references them with uint32
|
||||
// indexes instead of Go pointers. Index zero is nil. Unlike Arena, it retains all
|
||||
// blocks so that an index alone keeps its value reachable through the arena.
|
||||
type IndexedArena[T any] struct {
|
||||
blocks [][]T
|
||||
size uint32
|
||||
}
|
||||
|
||||
const indexedArenaPageShift = 8
|
||||
|
||||
func (a *IndexedArena[T]) New() (uint32, *T) {
|
||||
if a.size == ^uint32(0) {
|
||||
panic("IndexedArena exhausted")
|
||||
}
|
||||
if len(a.blocks) == 0 || len(a.blocks[len(a.blocks)-1]) == cap(a.blocks[len(a.blocks)-1]) {
|
||||
size := 1 << min(len(a.blocks), indexedArenaPageShift)
|
||||
a.blocks = append(a.blocks, make([]T, 0, size))
|
||||
}
|
||||
block := &a.blocks[len(a.blocks)-1]
|
||||
*block = (*block)[:len(*block)+1]
|
||||
a.size++
|
||||
return a.size, &(*block)[len(*block)-1]
|
||||
}
|
||||
|
||||
func (a *IndexedArena[T]) Get(index uint32) *T {
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
// The initial blocks have sizes 1, 2, 4, ..., 128. Subsequent
|
||||
// blocks have 256 entries, avoiding geometric over-allocation.
|
||||
if index < 1<<indexedArenaPageShift {
|
||||
block := bits.Len32(index) - 1
|
||||
return &a.blocks[block][index-(1<<block)]
|
||||
}
|
||||
index -= 1 << indexedArenaPageShift
|
||||
return &a.blocks[indexedArenaPageShift+int(index>>indexedArenaPageShift)][index&((1<<indexedArenaPageShift)-1)]
|
||||
}
|
||||
@@ -40,12 +40,12 @@ const (
|
||||
)
|
||||
|
||||
// PagedLinkStore implements a sparse-array-like structure for storing elements keyed by dense uint64 keys.
|
||||
// Elements are allocated in an area, element references are stored in fixed-size pages of 256 entries, and
|
||||
// an index of pages is maintained in a growable list.
|
||||
// Elements are allocated in an arena, and compressed indexes are stored in fixed-size pages of 256 entries.
|
||||
// The pages contain no Go pointers, halving their size on 64-bit hosts and avoiding GC scans of each entry.
|
||||
|
||||
type PagedLinkStore[V any] struct {
|
||||
pages []*[LinkPageSize]*V
|
||||
arena Arena[V]
|
||||
pages []*[LinkPageSize]uint32
|
||||
arena IndexedArena[V]
|
||||
}
|
||||
|
||||
func (s *PagedLinkStore[V]) Get(key uint64) *V {
|
||||
@@ -56,15 +56,16 @@ func (s *PagedLinkStore[V]) Get(key uint64) *V {
|
||||
}
|
||||
page := s.pages[pageIndex]
|
||||
if page == nil {
|
||||
page = new([LinkPageSize]*V)
|
||||
page = new([LinkPageSize]uint32)
|
||||
s.pages[pageIndex] = page
|
||||
}
|
||||
link := page[key&LinkPageMask]
|
||||
if link == nil {
|
||||
link = s.arena.New()
|
||||
page[key&LinkPageMask] = link
|
||||
index := page[key&LinkPageMask]
|
||||
if index == 0 {
|
||||
id, value := s.arena.New()
|
||||
page[key&LinkPageMask] = id
|
||||
return value
|
||||
}
|
||||
return link
|
||||
return s.arena.Get(index)
|
||||
}
|
||||
|
||||
func (s *PagedLinkStore[V]) Has(key uint64) bool {
|
||||
@@ -75,7 +76,7 @@ func (s *PagedLinkStore[V]) TryGet(key uint64) *V {
|
||||
pageIndex := key >> LinkPageShift
|
||||
if int(pageIndex) < len(s.pages) {
|
||||
if page := s.pages[pageIndex]; page != nil {
|
||||
return page[key&LinkPageMask]
|
||||
return s.arena.Get(page[key&LinkPageMask])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/TypeScript/tsc/internal/core"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestLinkStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
type value struct {
|
||||
number int
|
||||
text *string
|
||||
}
|
||||
var store core.LinkStore[int, value]
|
||||
assert.Assert(t, !store.Has(0))
|
||||
assert.Assert(t, store.TryGet(0) == nil)
|
||||
|
||||
values := make([]*value, 4096)
|
||||
for i := range values {
|
||||
v := store.Get(i)
|
||||
assert.Equal(t, *v, value{})
|
||||
v.number = i
|
||||
v.text = new("retained")
|
||||
values[i] = v
|
||||
}
|
||||
runtime.GC()
|
||||
for i, v := range values {
|
||||
assert.Assert(t, store.Has(i))
|
||||
assert.Equal(t, store.Get(i), v)
|
||||
assert.Equal(t, store.TryGet(i), v)
|
||||
assert.Equal(t, v.number, i)
|
||||
assert.Equal(t, *v.text, "retained")
|
||||
}
|
||||
assert.Assert(t, !store.Has(len(values)))
|
||||
assert.Assert(t, store.TryGet(len(values)) == nil)
|
||||
}
|
||||
|
||||
func TestPagedLinkStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
var store core.PagedLinkStore[int]
|
||||
keys := []uint64{0, 255, 256, 65535, 16777215}
|
||||
values := make([]*int, len(keys))
|
||||
for _, key := range keys {
|
||||
assert.Assert(t, !store.Has(key))
|
||||
}
|
||||
for i, key := range keys {
|
||||
v := store.Get(key)
|
||||
assert.Equal(t, *v, 0)
|
||||
*v = i + 1
|
||||
values[i] = v
|
||||
}
|
||||
runtime.GC()
|
||||
for i, key := range keys {
|
||||
assert.Assert(t, store.Has(key))
|
||||
assert.Equal(t, store.Get(key), values[i])
|
||||
assert.Equal(t, *store.TryGet(key), i+1)
|
||||
}
|
||||
assert.Assert(t, !store.Has(1))
|
||||
assert.Assert(t, store.TryGet(1) == nil)
|
||||
}
|
||||
|
||||
func BenchmarkLinkStore(b *testing.B) {
|
||||
type value struct {
|
||||
typ *int
|
||||
flags uint32
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
var store core.LinkStore[int, value]
|
||||
for i := range 10000 {
|
||||
store.Get(i).flags = uint32(i)
|
||||
}
|
||||
for i := range 10000 {
|
||||
if store.Get(i).flags != uint32(i) {
|
||||
b.Fatal("link changed during arena growth")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedArena(t *testing.T) {
|
||||
t.Parallel()
|
||||
type value struct {
|
||||
number int
|
||||
text *string
|
||||
}
|
||||
var arena core.IndexedArena[value]
|
||||
assert.Assert(t, arena.Get(0) == nil)
|
||||
values := make([]*value, 4096)
|
||||
for i := range values {
|
||||
index, v := arena.New()
|
||||
assert.Equal(t, index, uint32(i+1))
|
||||
assert.Equal(t, *v, value{})
|
||||
v.number = i
|
||||
v.text = new("retained")
|
||||
values[i] = v
|
||||
}
|
||||
runtime.GC()
|
||||
for i, v := range values {
|
||||
assert.Equal(t, arena.Get(uint32(i+1)), v)
|
||||
assert.Equal(t, v.number, i)
|
||||
assert.Equal(t, *v.text, "retained")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedArenaReachability(t *testing.T) {
|
||||
t.Parallel()
|
||||
var arena core.IndexedArena[*string]
|
||||
for i := range 4096 {
|
||||
_, value := arena.New()
|
||||
*value = new(fmt.Sprint(i))
|
||||
}
|
||||
runtime.GC()
|
||||
for i := range 4096 {
|
||||
assert.Equal(t, **arena.Get(uint32(i + 1)), fmt.Sprint(i))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPagedLinkStore(b *testing.B) {
|
||||
for _, stride := range []uint64{1, 16, 256} {
|
||||
b.Run(fmt.Sprint(stride), func(b *testing.B) {
|
||||
type value struct {
|
||||
typ *int
|
||||
flags uint32
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
var store core.PagedLinkStore[value]
|
||||
for i := range 10000 {
|
||||
store.Get(uint64(i) * stride).flags = uint32(i)
|
||||
}
|
||||
for i := range 10000 {
|
||||
if store.Get(uint64(i)*stride).flags != uint32(i) {
|
||||
b.Fatal("link changed during arena growth")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user