Skip to content

Commit e54a136

Browse files
authored
[patch] add Real Map memory size function (#138)
Signed-off-by: kpango <[email protected]>
1 parent 6cf9f5b commit e54a136

17 files changed

+300
-17
lines changed

.circleci/config.yml

100644100755
File mode changed.

.gitignore

100644100755
File mode changed.

.whitesource

100644100755
File mode changed.

LICENSE

100644100755
File mode changed.

Makefile

100644100755
File mode changed.

README.md

100644100755
File mode changed.

assets/logo.png

100644100755
File mode changed.

example/main.go

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,58 @@ package main
22

33
import (
44
"context"
5+
"encoding/gob"
6+
"encoding/json"
7+
"math/rand"
58
"os"
9+
"runtime"
10+
"strconv"
611
"time"
12+
"unsafe"
713

814
"github.com/kpango/gache/v2"
915
"github.com/kpango/glg"
1016
)
1117

18+
var (
19+
bigData = map[string]string{}
20+
bigDataLen = 2 << 10
21+
bigDataCount = 2 << 11
22+
)
23+
24+
func init() {
25+
for i := 0; i < bigDataCount; i++ {
26+
bigData[randStr(bigDataLen)] = randStr(bigDataLen)
27+
}
28+
}
29+
30+
var randSrc = rand.NewSource(time.Now().UnixNano())
31+
32+
const (
33+
rs6Letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
34+
rs6LetterIdxBits = 6
35+
rs6LetterIdxMask = 1<<rs6LetterIdxBits - 1
36+
rs6LetterIdxMax = 63 / rs6LetterIdxBits
37+
)
38+
39+
func randStr(n int) string {
40+
b := make([]byte, n)
41+
cache, remain := randSrc.Int63(), rs6LetterIdxMax
42+
for i := n - 1; i >= 0; {
43+
if remain == 0 {
44+
cache, remain = randSrc.Int63(), rs6LetterIdxMax
45+
}
46+
idx := int(cache & rs6LetterIdxMask)
47+
if idx < len(rs6Letters) {
48+
b[i] = rs6Letters[idx]
49+
i--
50+
}
51+
cache >>= rs6LetterIdxBits
52+
remain--
53+
}
54+
return *(*string)(unsafe.Pointer(&b))
55+
}
56+
1257
func main() {
1358
var (
1459
key1 = "key1"
@@ -45,18 +90,30 @@ func main() {
4590
return true
4691
})
4792

48-
file, err := os.OpenFile("/tmp/gache-sample.gdb", os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o755)
93+
var m runtime.MemStats
94+
runtime.ReadMemStats(&m)
95+
mbody, err := json.Marshal(m)
96+
if err == nil {
97+
glg.Debugf("memory size: %d, lenght: %d, mem stats: %v", gc.Size(), gc.Len(), string(mbody))
98+
}
99+
path := "/tmp/gache-sample.gdb"
100+
101+
file, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o755)
49102
if err != nil {
50103
glg.Error(err)
51104
return
52105
}
53-
gc.Write(context.Background(), file)
54-
106+
gob.Register(struct{}{})
107+
err = gc.Write(context.Background(), file)
55108
gc.Stop()
56109
file.Close()
110+
if err != nil {
111+
glg.Error(err)
112+
return
113+
}
57114

58115
gcn := gache.New[any]().SetDefaultExpire(time.Minute)
59-
file, err = os.OpenFile("/tmp/gache-sample.gdb", os.O_RDONLY, 0o755)
116+
file, err = os.OpenFile(path, os.O_RDONLY, 0o755)
60117
if err != nil {
61118
glg.Error(err)
62119
return
@@ -88,4 +145,53 @@ func main() {
88145
glg.Debugf("key:\t%v\nval:\t%d", k, v)
89146
return true
90147
})
148+
149+
runtime.GC()
150+
gcs := gache.New[string]()
151+
maxCnt := 10000000
152+
digitLen := len(strconv.Itoa(maxCnt))
153+
for i := 0; i < maxCnt; i++ {
154+
if i%1000 == 0 {
155+
// runtime.ReadMemStats(&m)
156+
// mbody, err := json.Marshal(m)
157+
if err == nil {
158+
// glg.Debugf("before set memory size: %d, lenght: %d, mem stats: %v", gcs.Size(), gcs.Len(), string(mbody))
159+
glg.Debugf("Execution No.%-*d:\tbefore set memory size: %d, lenght: %d", digitLen, i, gcs.Size(), gcs.Len())
160+
}
161+
}
162+
for k, v := range bigData {
163+
gcs.Set(k, v)
164+
}
165+
if i%1000 == 0 {
166+
// runtime.ReadMemStats(&m)
167+
// mbody, err := json.Marshal(m)
168+
if err == nil {
169+
glg.Debugf("Execution No.%-*d:\tafter set memory size: %d, lenght: %d", digitLen, i, gcs.Size(), gcs.Len())
170+
// glg.Debugf("after set memory size: %d, lenght: %d, mem stats: %v", gcs.Size(), gcs.Len(), string(mbody))
171+
}
172+
}
173+
174+
for k := range bigData {
175+
gcs.Get(k)
176+
}
177+
for k := range bigData {
178+
gcs.Delete(k)
179+
}
180+
if i%1000 == 0 {
181+
// runtime.ReadMemStats(&m)
182+
// mbody, err := json.Marshal(m)
183+
if err == nil {
184+
glg.Debugf("Execution No.%-*d:\tafter delete memory size: %d, lenght: %d", digitLen, i, gcs.Size(), gcs.Len())
185+
// glg.Debugf("after delete memory size: %d, lenght: %d, mem stats: %v", gcs.Size(), gcs.Len(), string(mbody))
186+
}
187+
runtime.GC()
188+
// runtime.ReadMemStats(&m)
189+
// mbody, err = json.Marshal(m)
190+
if err == nil {
191+
glg.Debugf("Execution No.%-*d:\tafter gc memory size: %d, lenght: %d", digitLen, i, gcs.Size(), gcs.Len())
192+
// glg.Debugf("after gc memory size: %d, lenght: %d, mem stats: %v", gcs.Size(), gcs.Len(), string(mbody))
193+
}
194+
}
195+
196+
}
91197
}

gache.go

100644100755
Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type (
3232
SetWithExpire(string, V, time.Duration)
3333
StartExpired(context.Context, time.Duration) Gache[V]
3434
Len() int
35+
Size() uintptr
3536
ToMap(context.Context) *sync.Map
3637
ToRawMap(context.Context) map[string]V
3738
Write(context.Context, io.Writer) error
@@ -58,18 +59,18 @@ type (
5859

5960
// gache is base instance type
6061
gache[V any] struct {
61-
expFuncEnabled bool
62-
expire int64
63-
l uint64
62+
shards [slen]*Map[string, *value[V]]
6463
cancel atomic.Pointer[context.CancelFunc]
6564
expChan chan string
6665
expFunc func(context.Context, string)
67-
shards [slen]*Map[string, *value[V]]
66+
expFuncEnabled bool
67+
expire int64
68+
l uint64
6869
}
6970

7071
value[V any] struct {
71-
expire int64
7272
val V
73+
expire int64
7374
}
7475
)
7576

@@ -83,6 +84,8 @@ const (
8384

8485
// NoTTL can be use for disabling ttl cache expiration
8586
NoTTL time.Duration = -1
87+
88+
maxHashKeyLength = 256
8689
)
8790

8891
// New returns Gache (*gache) instance
@@ -103,8 +106,8 @@ func newMap[V any]() (m *Map[string, *value[V]]) {
103106
}
104107

105108
func getShardID(key string) (id uint64) {
106-
if len(key) > 128 {
107-
return xxh3.HashString(key[:128]) & mask
109+
if len(key) > maxHashKeyLength {
110+
return xxh3.HashString(key[:maxHashKeyLength]) & mask
108111
}
109112
return xxh3.HashString(key) & mask
110113
}
@@ -142,7 +145,8 @@ func (g *gache[V]) SetExpiredHook(f func(context.Context, string)) Gache[V] {
142145
func (g *gache[V]) StartExpired(ctx context.Context, dur time.Duration) Gache[V] {
143146
go func() {
144147
tick := time.NewTicker(dur)
145-
ctx, cancel := context.WithCancel(ctx)
148+
var cancel context.CancelFunc
149+
ctx, cancel = context.WithCancel(ctx)
146150
g.cancel.Store(&cancel)
147151
for {
148152
select {
@@ -193,7 +197,8 @@ func (g *gache[V]) ToRawMap(ctx context.Context) map[string]V {
193197
// get returns value & exists from key
194198
func (g *gache[V]) get(key string) (v V, expire int64, ok bool) {
195199
var val *value[V]
196-
val, ok = g.shards[getShardID(key)].Load(key)
200+
shard := g.shards[getShardID(key)]
201+
val, ok = shard.Load(key)
197202
if !ok {
198203
return v, 0, false
199204
}
@@ -222,7 +227,8 @@ func (g *gache[V]) set(key string, val V, expire int64) {
222227
if expire > 0 {
223228
expire = fastime.UnixNanoNow() + expire
224229
}
225-
_, loaded := g.shards[getShardID(key)].Swap(key, &value[V]{
230+
shard := g.shards[getShardID(key)]
231+
_, loaded := shard.Swap(key, &value[V]{
226232
expire: expire,
227233
val: val,
228234
})
@@ -313,6 +319,19 @@ func (g *gache[V]) Len() int {
313319
return *(*int)(unsafe.Pointer(&l))
314320
}
315321

322+
func (g *gache[V]) Size() (size uintptr) {
323+
size += unsafe.Sizeof(g.expFuncEnabled) // bool
324+
size += unsafe.Sizeof(g.expire) // int64
325+
size += unsafe.Sizeof(g.l) // uint64
326+
size += unsafe.Sizeof(g.cancel) // atomic.Pointer[context.CancelFunc]
327+
size += unsafe.Sizeof(g.expChan) // chan string
328+
size += unsafe.Sizeof(g.expFunc) // func(context.Context, string)
329+
for _, shard := range g.shards {
330+
size += shard.Size()
331+
}
332+
return size
333+
}
334+
316335
// Write writes all cached data to writer
317336
func (g *gache[V]) Write(ctx context.Context, w io.Writer) error {
318337
m := g.ToRawMap(ctx)
@@ -329,7 +348,7 @@ func (g *gache[V]) Read(r io.Reader) error {
329348
return err
330349
}
331350
for k, v := range m {
332-
g.Set(k, v)
351+
go g.Set(k, v)
333352
}
334353
return nil
335354
}
@@ -348,3 +367,12 @@ func (g *gache[V]) Clear() {
348367
g.shards[i] = newMap[V]()
349368
}
350369
}
370+
371+
func (v *value[V]) Size() uintptr {
372+
var size uintptr
373+
374+
size += unsafe.Sizeof(v.expire) // int64
375+
size += unsafe.Sizeof(v.val) // V size
376+
377+
return size
378+
}

gache_benchmark_test.go

100644100755
File mode changed.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/kpango/gache/v2
22

3-
go 1.22.3
3+
go 1.23.1
44

55
require (
66
github.com/kpango/fastime v1.1.9

hmap.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) 2009 The Go Authors. All rights resered.
2+
// Modified <Yusuke Kato (kpango)>
3+
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are
6+
// met:
7+
8+
// * Redistributions of source code must retain the above copyright
9+
// notice, this list of conditions and the following disclaimer.
10+
// * Redistributions in binary form must reproduce the above
11+
// copyright notice, this list of conditions and the following disclaimer
12+
// in the documentation and/or other materials provided with the
13+
// distribution.
14+
// * Neither the name of Google Inc. nor the names of its
15+
// contributors may be used to endorse or promote products derived from
16+
// this software without specific prior written permission.
17+
18+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
package gache
31+
32+
import "unsafe"
33+
34+
// A header for a Go map.
35+
type hmap struct {
36+
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
37+
// Make sure this stays in sync with the compiler's definition.
38+
count int // # live cells == size of map. Must be first (used by len() builtin)
39+
flags uint8
40+
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
41+
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
42+
hash0 uint32 // hash seed
43+
44+
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
45+
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
46+
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
47+
}
48+
49+
const bucketCnt = 8
50+
51+
// A bucket for a Go map.
52+
type bmap struct {
53+
// tophash generally contains the top byte of the hash value
54+
// for each key in this bucket. If tophash[0] < minTopHash,
55+
// tophash[0] is a bucket evacuation state instead.
56+
tophash [bucketCnt]uint8
57+
// Followed by bucketCnt keys and then bucketCnt elems.
58+
// NOTE: packing all the keys together and then all the elems together makes the
59+
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
60+
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
61+
// Followed by an overflow pointer.
62+
}
63+
64+
var singleBucketSize = unsafe.Sizeof(bmap{})
65+
66+
func mapSize[K comparable, V any](m map[K]V) (size uintptr) {
67+
h := (*hmap)(*(*unsafe.Pointer)(unsafe.Pointer(&m)))
68+
if h == nil {
69+
return 0
70+
}
71+
var (
72+
zeroK K
73+
zeroV V
74+
)
75+
return h.Size(unsafe.Sizeof(zeroK), unsafe.Sizeof(zeroV))
76+
}
77+
78+
func (b *bmap) Size() (size uintptr) {
79+
return unsafe.Sizeof(b.tophash)
80+
}
81+
82+
func (h *hmap) Size(kSize, vSize uintptr) (size uintptr) {
83+
size += unsafe.Sizeof(h.count)
84+
size += unsafe.Sizeof(h.flags)
85+
size += unsafe.Sizeof(h.B)
86+
size += unsafe.Sizeof(h.noverflow)
87+
size += unsafe.Sizeof(h.hash0)
88+
size += unsafe.Sizeof(h.buckets)
89+
size += unsafe.Sizeof(h.oldbuckets)
90+
size += unsafe.Sizeof(h.nevacuate)
91+
92+
if h.B == 0 {
93+
return size
94+
}
95+
bucketSize := singleBucketSize + (bucketCnt * (kSize + vSize))
96+
if h.buckets != nil {
97+
size += uintptr(1<<h.B) * bucketSize
98+
}
99+
if h.oldbuckets != nil && h.B > 1 {
100+
size += uintptr(1<<(h.B-1)) * bucketSize
101+
}
102+
return size
103+
}

0 commit comments

Comments
 (0)