Repository: goccy/go-json
Pull request #310 (2021-12-27 02:16:14)

Improve map encoding performance
Reuse mapIter object and use `runtime.mapiterinit` instead of `reflect.mapiterinit`

### BEFORE

```
$ cd benchmarks && go test -bench Encode_MapInterface_GoJson
```

```
goos: darwin
goarch: amd64
pkg: benchmark
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
Benchmark_Encode_MapInterface_GoJson-16          1504303               822.3 ns/op           144 B/op          2 allocs/op
```

### AFTER

```
$ cd benchmarks && go test -bench Encode_MapInterface_GoJson
```

```
goos: darwin
goarch: amd64
pkg: benchmark
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
Benchmark_Encode_MapInterface_GoJson-16          1682722               694.4 ns/op            48 B/op          1 allocs/op
PASS
```


Patch:
diff --git a/internal/cmd/generator/vm.go.tmpl b/internal/cmd/generator/vm.go.tmpl
index b78b5eb..4be6b80 100644
--- a/internal/cmd/generator/vm.go.tmpl
+++ b/internal/cmd/generator/vm.go.tmpl
@@ -403,48 +403,41 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				break
 			}
 			b = appendStructHead(ctx, b)
-			iter := mapiterinit(code.Type, uptr)
-			ctx.KeepRefs = append(ctx.KeepRefs, iter)
-			store(ctxptr, code.ElemIdx, 0)
-			store(ctxptr, code.Length, uintptr(mlen))
-			store(ctxptr, code.MapIter, uintptr(iter))
+			mapCtx := encoder.NewMapContext(mlen)
+			mapiterinit(code.Type, uptr, &mapCtx.Iter)
+			store(ctxptr, code.Idx, uintptr(unsafe.Pointer(mapCtx)))
+			ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendMapKeyIndent(ctx, code.Next, b)
 			} else {
-				mapCtx := encoder.NewMapContext(mlen)
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
-				store(ctxptr, code.End.MapPos, uintptr(unsafe.Pointer(mapCtx)))
+				mapCtx.Start = len(b)
+				mapCtx.First = len(b)
 			}
-			key := mapiterkey(iter)
+			key := mapiterkey(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(key))
 			code = code.Next
 		case encoder.OpMapKey:
-			idx := load(ctxptr, code.ElemIdx)
-			length := load(ctxptr, code.Length)
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
+			idx := mapCtx.Idx
 			idx++
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
-				if idx < length {
+				if idx < mapCtx.Len {
 					b = appendMapKeyIndent(ctx, code, b)
-					store(ctxptr, code.ElemIdx, idx)
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					key := mapiterkey(iter)
+					mapCtx.Idx = int(idx)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
 					b = appendObjectEnd(ctx, code, b)
+					encoder.ReleaseMapContext(mapCtx)
 					code = code.End.Next
 				}
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				if idx < length {
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					store(ctxptr, code.ElemIdx, idx)
-					key := mapiterkey(iter)
+				mapCtx.Slice.Items[mapCtx.Idx].Value = b[mapCtx.Start:len(b)]
+				if idx < mapCtx.Len {
+					mapCtx.Idx = int(idx)
+					mapCtx.Start = len(b)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
@@ -452,46 +445,27 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				}
 			}
 		case encoder.OpMapValue:
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendColon(ctx, b)
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
+				mapCtx.Slice.Items[mapCtx.Idx].Key = b[mapCtx.Start:len(b)]
+				mapCtx.Start = len(b)
 			}
-			ptr := load(ctxptr, code.MapIter)
-			iter := ptrToUnsafePtr(ptr)
-			value := mapitervalue(iter)
+			value := mapitervalue(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(value))
-			mapiternext(iter)
+			mapiternext(&mapCtx.Iter)
 			code = code.Next
 		case encoder.OpMapEnd:
 			// this operation only used by sorted map.
-			length := int(load(ctxptr, code.Length))
-			ptr := load(ctxptr, code.MapPos)
-			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-			pos := mapCtx.Pos
-			for i := 0; i < length; i++ {
-				startKey := pos[i*2]
-				startValue := pos[i*2+1]
-				var endValue int
-				if i+1 < length {
-					endValue = pos[i*2+2]
-				} else {
-					endValue = len(b)
-				}
-				mapCtx.Slice.Items = append(mapCtx.Slice.Items, encoder.MapItem{
-					Key:   b[startKey:startValue],
-					Value: b[startValue:endValue],
-				})
-			}
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			sort.Sort(mapCtx.Slice)
 			buf := mapCtx.Buf
 			for _, item := range mapCtx.Slice.Items {
 				buf = appendMapKeyValue(ctx, code, buf, item.Key, item.Value)
 			}
 			buf = appendMapEnd(ctx, code, buf)
-			b = b[:pos[0]]
+			b = b[:mapCtx.First]
 			b = append(b, buf...)
 			mapCtx.Buf = buf
 			encoder.ReleaseMapContext(mapCtx)
diff --git a/internal/encoder/encoder.go b/internal/encoder/encoder.go
index b7fa99a..79a3f64 100644
--- a/internal/encoder/encoder.go
+++ b/internal/encoder/encoder.go
@@ -222,33 +222,54 @@ func (m *Mapslice) Swap(i, j int) {
 	m.Items[i], m.Items[j] = m.Items[j], m.Items[i]
 }
 
+//nolint:structcheck,unused
+type mapIter struct {
+	key         unsafe.Pointer
+	elem        unsafe.Pointer
+	t           unsafe.Pointer
+	h           unsafe.Pointer
+	buckets     unsafe.Pointer
+	bptr        unsafe.Pointer
+	overflow    unsafe.Pointer
+	oldoverflow unsafe.Pointer
+	startBucket uintptr
+	offset      uint8
+	wrapped     bool
+	B           uint8
+	i           uint8
+	bucket      uintptr
+	checkBucket uintptr
+}
+
 type MapContext struct {
-	Pos   []int
+	Start int
+	First int
+	Idx   int
 	Slice *Mapslice
 	Buf   []byte
+	Len   int
+	Iter  mapIter
 }
 
 var mapContextPool = sync.Pool{
 	New: func() interface{} {
-		return &MapContext{}
+		return &MapContext{
+			Slice: &Mapslice{},
+		}
 	},
 }
 
 func NewMapContext(mapLen int) *MapContext {
 	ctx := mapContextPool.Get().(*MapContext)
-	if ctx.Slice == nil {
-		ctx.Slice = &Mapslice{
-			Items: make([]MapItem, 0, mapLen),
-		}
-	}
-	if cap(ctx.Pos) < (mapLen*2 + 1) {
-		ctx.Pos = make([]int, 0, mapLen*2+1)
-		ctx.Slice.Items = make([]MapItem, 0, mapLen)
+	if len(ctx.Slice.Items) < mapLen {
+		ctx.Slice.Items = make([]MapItem, mapLen)
 	} else {
-		ctx.Pos = ctx.Pos[:0]
-		ctx.Slice.Items = ctx.Slice.Items[:0]
+		ctx.Slice.Items = ctx.Slice.Items[:mapLen]
 	}
 	ctx.Buf = ctx.Buf[:0]
+	ctx.Iter = mapIter{}
+	ctx.Idx = 0
+	ctx.Len = mapLen
 	return ctx
 }
 
@@ -256,17 +277,17 @@ func ReleaseMapContext(c *MapContext) {
 	mapContextPool.Put(c)
 }
 
-//go:linkname MapIterInit reflect.mapiterinit
+//go:linkname MapIterInit runtime.mapiterinit
 //go:noescape
-func MapIterInit(mapType *runtime.Type, m unsafe.Pointer) unsafe.Pointer
+func MapIterInit(mapType *runtime.Type, m unsafe.Pointer, it *mapIter)
 
 //go:linkname MapIterKey reflect.mapiterkey
 //go:noescape
-func MapIterKey(it unsafe.Pointer) unsafe.Pointer
+func MapIterKey(it *mapIter) unsafe.Pointer
 
 //go:linkname MapIterNext reflect.mapiternext
 //go:noescape
-func MapIterNext(it unsafe.Pointer)
+func MapIterNext(it *mapIter)
 
 //go:linkname MapLen reflect.maplen
 //go:noescape
diff --git a/internal/encoder/map112.go b/internal/encoder/map112.go
index 31858d0..e96ffad 100644
--- a/internal/encoder/map112.go
+++ b/internal/encoder/map112.go
@@ -1,3 +1,4 @@
+//go:build !go1.13
 // +build !go1.13
 
 package encoder
@@ -5,4 +6,4 @@ package encoder
 import "unsafe"
 
 //go:linkname MapIterValue reflect.mapitervalue
-func MapIterValue(it unsafe.Pointer) unsafe.Pointer
+func MapIterValue(it *mapIter) unsafe.Pointer
diff --git a/internal/encoder/map113.go b/internal/encoder/map113.go
index f49c27b..9b69dcc 100644
--- a/internal/encoder/map113.go
+++ b/internal/encoder/map113.go
@@ -1,3 +1,4 @@
+//go:build go1.13
 // +build go1.13
 
 package encoder
@@ -5,4 +6,4 @@ package encoder
 import "unsafe"
 
 //go:linkname MapIterValue reflect.mapiterelem
-func MapIterValue(it unsafe.Pointer) unsafe.Pointer
+func MapIterValue(it *mapIter) unsafe.Pointer
diff --git a/internal/encoder/opcode.go b/internal/encoder/opcode.go
index cfd2d5a..903dd6f 100644
--- a/internal/encoder/opcode.go
+++ b/internal/encoder/opcode.go
@@ -39,10 +39,8 @@ type Opcode struct {
 
 	Type       *runtime.Type // go type
 	Jmp        *CompiledCode // for recursive call
-	ElemIdx    uint32        // offset to access array/slice/map elem
-	Length     uint32        // offset to access slice/map length or array length
-	MapIter    uint32        // offset to access map iterator
-	MapPos     uint32        // offset to access position list for sorted map
+	ElemIdx    uint32        // offset to access array/slice elem
+	Length     uint32        // offset to access slice length or array length
 	Indent     uint32        // indent number
 	Size       uint32        // array/slice elem size
 	DisplayIdx uint32        // opcode index
@@ -91,8 +89,6 @@ func (c *Opcode) MaxIdx() uint32 {
 		c.Idx,
 		c.ElemIdx,
 		c.Length,
-		c.MapIter,
-		c.MapPos,
 		c.Size,
 	} {
 		if max < value {
@@ -341,8 +337,6 @@ func copyOpcode(code *Opcode) *Opcode {
 			DisplayKey: c.DisplayKey,
 			ElemIdx:    c.ElemIdx,
 			Length:     c.Length,
-			MapIter:    c.MapIter,
-			MapPos:     c.MapPos,
 			Size:       c.Size,
 			Indent:     c.Indent,
 			Jmp:        c.Jmp,
@@ -448,26 +442,21 @@ func (c *Opcode) dumpHead(code *Opcode) string {
 
 func (c *Opcode) dumpMapHead(code *Opcode) string {
 	return fmt.Sprintf(
-		`[%03d]%s%s ([idx:%d][elemIdx:%d][length:%d][mapIter:%d])`,
+		`[%03d]%s%s ([idx:%d])`,
 		code.DisplayIdx,
 		strings.Repeat("-", int(code.Indent)),
 		code.Op,
 		code.Idx/uintptrSize,
-		code.ElemIdx/uintptrSize,
-		code.Length/uintptrSize,
-		code.MapIter/uintptrSize,
 	)
 }
 
 func (c *Opcode) dumpMapEnd(code *Opcode) string {
 	return fmt.Sprintf(
-		`[%03d]%s%s ([idx:%d][mapPos:%d][length:%d])`,
+		`[%03d]%s%s ([idx:%d])`,
 		code.DisplayIdx,
 		strings.Repeat("-", int(code.Indent)),
 		code.Op,
 		code.Idx/uintptrSize,
-		code.MapPos/uintptrSize,
-		code.Length/uintptrSize,
 	)
 }
 
@@ -504,25 +493,21 @@ func (c *Opcode) dumpField(code *Opcode) string {
 
 func (c *Opcode) dumpKey(code *Opcode) string {
 	return fmt.Sprintf(
-		`[%03d]%s%s ([idx:%d][elemIdx:%d][length:%d][mapIter:%d])`,
+		`[%03d]%s%s ([idx:%d])`,
 		code.DisplayIdx,
 		strings.Repeat("-", int(code.Indent)),
 		code.Op,
 		code.Idx/uintptrSize,
-		code.ElemIdx/uintptrSize,
-		code.Length/uintptrSize,
-		code.MapIter/uintptrSize,
 	)
 }
 
 func (c *Opcode) dumpValue(code *Opcode) string {
 	return fmt.Sprintf(
-		`[%03d]%s%s ([idx:%d][mapIter:%d])`,
+		`[%03d]%s%s ([idx:%d])`,
 		code.DisplayIdx,
 		strings.Repeat("-", int(code.Indent)),
 		code.Op,
 		code.Idx/uintptrSize,
-		code.MapIter/uintptrSize,
 	)
 }
 
@@ -629,19 +614,11 @@ func newArrayElemCode(ctx *compileContext, typ *runtime.Type, head *Opcode, leng
 func newMapHeaderCode(ctx *compileContext, typ *runtime.Type) *Opcode {
 	idx := opcodeOffset(ctx.ptrIndex)
 	ctx.incPtrIndex()
-	elemIdx := opcodeOffset(ctx.ptrIndex)
-	ctx.incPtrIndex()
-	length := opcodeOffset(ctx.ptrIndex)
-	ctx.incPtrIndex()
-	mapIter := opcodeOffset(ctx.ptrIndex)
 	return &Opcode{
 		Op:         OpMap,
 		Type:       typ,
 		Idx:        idx,
 		DisplayIdx: ctx.opcodeIndex,
-		ElemIdx:    elemIdx,
-		Length:     length,
-		MapIter:    mapIter,
 		Indent:     ctx.indent,
 	}
 }
@@ -650,11 +627,8 @@ func newMapKeyCode(ctx *compileContext, typ *runtime.Type, head *Opcode) *Opcode
 	return &Opcode{
 		Op:         OpMapKey,
 		Type:       typ,
-		Idx:        opcodeOffset(ctx.ptrIndex),
+		Idx:        head.Idx,
 		DisplayIdx: ctx.opcodeIndex,
-		ElemIdx:    head.ElemIdx,
-		Length:     head.Length,
-		MapIter:    head.MapIter,
 		Indent:     ctx.indent,
 	}
 }
@@ -663,28 +637,20 @@ func newMapValueCode(ctx *compileContext, typ *runtime.Type, head *Opcode) *Opco
 	return &Opcode{
 		Op:         OpMapValue,
 		Type:       typ,
-		Idx:        opcodeOffset(ctx.ptrIndex),
+		Idx:        head.Idx,
 		DisplayIdx: ctx.opcodeIndex,
-		ElemIdx:    head.ElemIdx,
-		Length:     head.Length,
-		MapIter:    head.MapIter,
 		Indent:     ctx.indent,
 	}
 }
 
 func newMapEndCode(ctx *compileContext, typ *runtime.Type, head *Opcode) *Opcode {
-	mapPos := opcodeOffset(ctx.ptrIndex)
-	ctx.incPtrIndex()
-	idx := opcodeOffset(ctx.ptrIndex)
 	return &Opcode{
 		Op:         OpMapEnd,
 		Type:       typ,
-		Idx:        idx,
-		Next:       newEndOp(ctx, typ),
+		Idx:        head.Idx,
 		DisplayIdx: ctx.opcodeIndex,
-		Length:     head.Length,
-		MapPos:     mapPos,
 		Indent:     ctx.indent,
+		Next:       newEndOp(ctx, typ),
 	}
 }
 
diff --git a/internal/encoder/vm/vm.go b/internal/encoder/vm/vm.go
index b78b5eb..4be6b80 100644
--- a/internal/encoder/vm/vm.go
+++ b/internal/encoder/vm/vm.go
@@ -403,48 +403,41 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				break
 			}
 			b = appendStructHead(ctx, b)
-			iter := mapiterinit(code.Type, uptr)
-			ctx.KeepRefs = append(ctx.KeepRefs, iter)
-			store(ctxptr, code.ElemIdx, 0)
-			store(ctxptr, code.Length, uintptr(mlen))
-			store(ctxptr, code.MapIter, uintptr(iter))
+			mapCtx := encoder.NewMapContext(mlen)
+			mapiterinit(code.Type, uptr, &mapCtx.Iter)
+			store(ctxptr, code.Idx, uintptr(unsafe.Pointer(mapCtx)))
+			ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendMapKeyIndent(ctx, code.Next, b)
 			} else {
-				mapCtx := encoder.NewMapContext(mlen)
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
-				store(ctxptr, code.End.MapPos, uintptr(unsafe.Pointer(mapCtx)))
+				mapCtx.Start = len(b)
+				mapCtx.First = len(b)
 			}
-			key := mapiterkey(iter)
+			key := mapiterkey(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(key))
 			code = code.Next
 		case encoder.OpMapKey:
-			idx := load(ctxptr, code.ElemIdx)
-			length := load(ctxptr, code.Length)
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
+			idx := mapCtx.Idx
 			idx++
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
-				if idx < length {
+				if idx < mapCtx.Len {
 					b = appendMapKeyIndent(ctx, code, b)
-					store(ctxptr, code.ElemIdx, idx)
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					key := mapiterkey(iter)
+					mapCtx.Idx = int(idx)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
 					b = appendObjectEnd(ctx, code, b)
+					encoder.ReleaseMapContext(mapCtx)
 					code = code.End.Next
 				}
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				if idx < length {
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					store(ctxptr, code.ElemIdx, idx)
-					key := mapiterkey(iter)
+				mapCtx.Slice.Items[mapCtx.Idx].Value = b[mapCtx.Start:len(b)]
+				if idx < mapCtx.Len {
+					mapCtx.Idx = int(idx)
+					mapCtx.Start = len(b)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
@@ -452,46 +445,27 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				}
 			}
 		case encoder.OpMapValue:
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendColon(ctx, b)
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
+				mapCtx.Slice.Items[mapCtx.Idx].Key = b[mapCtx.Start:len(b)]
+				mapCtx.Start = len(b)
 			}
-			ptr := load(ctxptr, code.MapIter)
-			iter := ptrToUnsafePtr(ptr)
-			value := mapitervalue(iter)
+			value := mapitervalue(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(value))
-			mapiternext(iter)
+			mapiternext(&mapCtx.Iter)
 			code = code.Next
 		case encoder.OpMapEnd:
 			// this operation only used by sorted map.
-			length := int(load(ctxptr, code.Length))
-			ptr := load(ctxptr, code.MapPos)
-			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-			pos := mapCtx.Pos
-			for i := 0; i < length; i++ {
-				startKey := pos[i*2]
-				startValue := pos[i*2+1]
-				var endValue int
-				if i+1 < length {
-					endValue = pos[i*2+2]
-				} else {
-					endValue = len(b)
-				}
-				mapCtx.Slice.Items = append(mapCtx.Slice.Items, encoder.MapItem{
-					Key:   b[startKey:startValue],
-					Value: b[startValue:endValue],
-				})
-			}
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			sort.Sort(mapCtx.Slice)
 			buf := mapCtx.Buf
 			for _, item := range mapCtx.Slice.Items {
 				buf = appendMapKeyValue(ctx, code, buf, item.Key, item.Value)
 			}
 			buf = appendMapEnd(ctx, code, buf)
-			b = b[:pos[0]]
+			b = b[:mapCtx.First]
 			b = append(b, buf...)
 			mapCtx.Buf = buf
 			encoder.ReleaseMapContext(mapCtx)
diff --git a/internal/encoder/vm_color/vm.go b/internal/encoder/vm_color/vm.go
index c7c6818..b13abe8 100644
--- a/internal/encoder/vm_color/vm.go
+++ b/internal/encoder/vm_color/vm.go
@@ -403,48 +403,41 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				break
 			}
 			b = appendStructHead(ctx, b)
-			iter := mapiterinit(code.Type, uptr)
-			ctx.KeepRefs = append(ctx.KeepRefs, iter)
-			store(ctxptr, code.ElemIdx, 0)
-			store(ctxptr, code.Length, uintptr(mlen))
-			store(ctxptr, code.MapIter, uintptr(iter))
+			mapCtx := encoder.NewMapContext(mlen)
+			mapiterinit(code.Type, uptr, &mapCtx.Iter)
+			store(ctxptr, code.Idx, uintptr(unsafe.Pointer(mapCtx)))
+			ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendMapKeyIndent(ctx, code.Next, b)
 			} else {
-				mapCtx := encoder.NewMapContext(mlen)
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
-				store(ctxptr, code.End.MapPos, uintptr(unsafe.Pointer(mapCtx)))
+				mapCtx.Start = len(b)
+				mapCtx.First = len(b)
 			}
-			key := mapiterkey(iter)
+			key := mapiterkey(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(key))
 			code = code.Next
 		case encoder.OpMapKey:
-			idx := load(ctxptr, code.ElemIdx)
-			length := load(ctxptr, code.Length)
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
+			idx := mapCtx.Idx
 			idx++
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
-				if idx < length {
+				if idx < mapCtx.Len {
 					b = appendMapKeyIndent(ctx, code, b)
-					store(ctxptr, code.ElemIdx, idx)
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					key := mapiterkey(iter)
+					mapCtx.Idx = int(idx)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
 					b = appendObjectEnd(ctx, code, b)
+					encoder.ReleaseMapContext(mapCtx)
 					code = code.End.Next
 				}
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				if idx < length {
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					store(ctxptr, code.ElemIdx, idx)
-					key := mapiterkey(iter)
+				mapCtx.Slice.Items[mapCtx.Idx].Value = b[mapCtx.Start:len(b)]
+				if idx < mapCtx.Len {
+					mapCtx.Idx = int(idx)
+					mapCtx.Start = len(b)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
@@ -452,46 +445,27 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				}
 			}
 		case encoder.OpMapValue:
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendColon(ctx, b)
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
+				mapCtx.Slice.Items[mapCtx.Idx].Key = b[mapCtx.Start:len(b)]
+				mapCtx.Start = len(b)
 			}
-			ptr := load(ctxptr, code.MapIter)
-			iter := ptrToUnsafePtr(ptr)
-			value := mapitervalue(iter)
+			value := mapitervalue(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(value))
-			mapiternext(iter)
+			mapiternext(&mapCtx.Iter)
 			code = code.Next
 		case encoder.OpMapEnd:
 			// this operation only used by sorted map.
-			length := int(load(ctxptr, code.Length))
-			ptr := load(ctxptr, code.MapPos)
-			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-			pos := mapCtx.Pos
-			for i := 0; i < length; i++ {
-				startKey := pos[i*2]
-				startValue := pos[i*2+1]
-				var endValue int
-				if i+1 < length {
-					endValue = pos[i*2+2]
-				} else {
-					endValue = len(b)
-				}
-				mapCtx.Slice.Items = append(mapCtx.Slice.Items, encoder.MapItem{
-					Key:   b[startKey:startValue],
-					Value: b[startValue:endValue],
-				})
-			}
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			sort.Sort(mapCtx.Slice)
 			buf := mapCtx.Buf
 			for _, item := range mapCtx.Slice.Items {
 				buf = appendMapKeyValue(ctx, code, buf, item.Key, item.Value)
 			}
 			buf = appendMapEnd(ctx, code, buf)
-			b = b[:pos[0]]
+			b = b[:mapCtx.First]
 			b = append(b, buf...)
 			mapCtx.Buf = buf
 			encoder.ReleaseMapContext(mapCtx)
diff --git a/internal/encoder/vm_color_indent/vm.go b/internal/encoder/vm_color_indent/vm.go
index 691ffc6..a45aa54 100644
--- a/internal/encoder/vm_color_indent/vm.go
+++ b/internal/encoder/vm_color_indent/vm.go
@@ -403,48 +403,41 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				break
 			}
 			b = appendStructHead(ctx, b)
-			iter := mapiterinit(code.Type, uptr)
-			ctx.KeepRefs = append(ctx.KeepRefs, iter)
-			store(ctxptr, code.ElemIdx, 0)
-			store(ctxptr, code.Length, uintptr(mlen))
-			store(ctxptr, code.MapIter, uintptr(iter))
+			mapCtx := encoder.NewMapContext(mlen)
+			mapiterinit(code.Type, uptr, &mapCtx.Iter)
+			store(ctxptr, code.Idx, uintptr(unsafe.Pointer(mapCtx)))
+			ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendMapKeyIndent(ctx, code.Next, b)
 			} else {
-				mapCtx := encoder.NewMapContext(mlen)
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
-				store(ctxptr, code.End.MapPos, uintptr(unsafe.Pointer(mapCtx)))
+				mapCtx.Start = len(b)
+				mapCtx.First = len(b)
 			}
-			key := mapiterkey(iter)
+			key := mapiterkey(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(key))
 			code = code.Next
 		case encoder.OpMapKey:
-			idx := load(ctxptr, code.ElemIdx)
-			length := load(ctxptr, code.Length)
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
+			idx := mapCtx.Idx
 			idx++
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
-				if idx < length {
+				if idx < mapCtx.Len {
 					b = appendMapKeyIndent(ctx, code, b)
-					store(ctxptr, code.ElemIdx, idx)
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					key := mapiterkey(iter)
+					mapCtx.Idx = int(idx)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
 					b = appendObjectEnd(ctx, code, b)
+					encoder.ReleaseMapContext(mapCtx)
 					code = code.End.Next
 				}
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				if idx < length {
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					store(ctxptr, code.ElemIdx, idx)
-					key := mapiterkey(iter)
+				mapCtx.Slice.Items[mapCtx.Idx].Value = b[mapCtx.Start:len(b)]
+				if idx < mapCtx.Len {
+					mapCtx.Idx = int(idx)
+					mapCtx.Start = len(b)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
@@ -452,46 +445,27 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				}
 			}
 		case encoder.OpMapValue:
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendColon(ctx, b)
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
+				mapCtx.Slice.Items[mapCtx.Idx].Key = b[mapCtx.Start:len(b)]
+				mapCtx.Start = len(b)
 			}
-			ptr := load(ctxptr, code.MapIter)
-			iter := ptrToUnsafePtr(ptr)
-			value := mapitervalue(iter)
+			value := mapitervalue(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(value))
-			mapiternext(iter)
+			mapiternext(&mapCtx.Iter)
 			code = code.Next
 		case encoder.OpMapEnd:
 			// this operation only used by sorted map.
-			length := int(load(ctxptr, code.Length))
-			ptr := load(ctxptr, code.MapPos)
-			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-			pos := mapCtx.Pos
-			for i := 0; i < length; i++ {
-				startKey := pos[i*2]
-				startValue := pos[i*2+1]
-				var endValue int
-				if i+1 < length {
-					endValue = pos[i*2+2]
-				} else {
-					endValue = len(b)
-				}
-				mapCtx.Slice.Items = append(mapCtx.Slice.Items, encoder.MapItem{
-					Key:   b[startKey:startValue],
-					Value: b[startValue:endValue],
-				})
-			}
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			sort.Sort(mapCtx.Slice)
 			buf := mapCtx.Buf
 			for _, item := range mapCtx.Slice.Items {
 				buf = appendMapKeyValue(ctx, code, buf, item.Key, item.Value)
 			}
 			buf = appendMapEnd(ctx, code, buf)
-			b = b[:pos[0]]
+			b = b[:mapCtx.First]
 			b = append(b, buf...)
 			mapCtx.Buf = buf
 			encoder.ReleaseMapContext(mapCtx)
diff --git a/internal/encoder/vm_indent/vm.go b/internal/encoder/vm_indent/vm.go
index fcc3b32..d1e0b45 100644
--- a/internal/encoder/vm_indent/vm.go
+++ b/internal/encoder/vm_indent/vm.go
@@ -403,48 +403,41 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				break
 			}
 			b = appendStructHead(ctx, b)
-			iter := mapiterinit(code.Type, uptr)
-			ctx.KeepRefs = append(ctx.KeepRefs, iter)
-			store(ctxptr, code.ElemIdx, 0)
-			store(ctxptr, code.Length, uintptr(mlen))
-			store(ctxptr, code.MapIter, uintptr(iter))
+			mapCtx := encoder.NewMapContext(mlen)
+			mapiterinit(code.Type, uptr, &mapCtx.Iter)
+			store(ctxptr, code.Idx, uintptr(unsafe.Pointer(mapCtx)))
+			ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendMapKeyIndent(ctx, code.Next, b)
 			} else {
-				mapCtx := encoder.NewMapContext(mlen)
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				ctx.KeepRefs = append(ctx.KeepRefs, unsafe.Pointer(mapCtx))
-				store(ctxptr, code.End.MapPos, uintptr(unsafe.Pointer(mapCtx)))
+				mapCtx.Start = len(b)
+				mapCtx.First = len(b)
 			}
-			key := mapiterkey(iter)
+			key := mapiterkey(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(key))
 			code = code.Next
 		case encoder.OpMapKey:
-			idx := load(ctxptr, code.ElemIdx)
-			length := load(ctxptr, code.Length)
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
+			idx := mapCtx.Idx
 			idx++
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
-				if idx < length {
+				if idx < mapCtx.Len {
 					b = appendMapKeyIndent(ctx, code, b)
-					store(ctxptr, code.ElemIdx, idx)
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					key := mapiterkey(iter)
+					mapCtx.Idx = int(idx)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
 					b = appendObjectEnd(ctx, code, b)
+					encoder.ReleaseMapContext(mapCtx)
 					code = code.End.Next
 				}
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
-				if idx < length {
-					ptr := load(ctxptr, code.MapIter)
-					iter := ptrToUnsafePtr(ptr)
-					store(ctxptr, code.ElemIdx, idx)
-					key := mapiterkey(iter)
+				mapCtx.Slice.Items[mapCtx.Idx].Value = b[mapCtx.Start:len(b)]
+				if idx < mapCtx.Len {
+					mapCtx.Idx = int(idx)
+					mapCtx.Start = len(b)
+					key := mapiterkey(&mapCtx.Iter)
 					store(ctxptr, code.Next.Idx, uintptr(key))
 					code = code.Next
 				} else {
@@ -452,46 +445,27 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
 				}
 			}
 		case encoder.OpMapValue:
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			if (ctx.Option.Flag & encoder.UnorderedMapOption) != 0 {
 				b = appendColon(ctx, b)
 			} else {
-				ptr := load(ctxptr, code.End.MapPos)
-				mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-				mapCtx.Pos = append(mapCtx.Pos, len(b))
+				mapCtx.Slice.Items[mapCtx.Idx].Key = b[mapCtx.Start:len(b)]
+				mapCtx.Start = len(b)
 			}
-			ptr := load(ctxptr, code.MapIter)
-			iter := ptrToUnsafePtr(ptr)
-			value := mapitervalue(iter)
+			value := mapitervalue(&mapCtx.Iter)
 			store(ctxptr, code.Next.Idx, uintptr(value))
-			mapiternext(iter)
+			mapiternext(&mapCtx.Iter)
 			code = code.Next
 		case encoder.OpMapEnd:
 			// this operation only used by sorted map.
-			length := int(load(ctxptr, code.Length))
-			ptr := load(ctxptr, code.MapPos)
-			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(ptr))
-			pos := mapCtx.Pos
-			for i := 0; i < length; i++ {
-				startKey := pos[i*2]
-				startValue := pos[i*2+1]
-				var endValue int
-				if i+1 < length {
-					endValue = pos[i*2+2]
-				} else {
-					endValue = len(b)
-				}
-				mapCtx.Slice.Items = append(mapCtx.Slice.Items, encoder.MapItem{
-					Key:   b[startKey:startValue],
-					Value: b[startValue:endValue],
-				})
-			}
+			mapCtx := (*encoder.MapContext)(ptrToUnsafePtr(load(ctxptr, code.Idx)))
 			sort.Sort(mapCtx.Slice)
 			buf := mapCtx.Buf
 			for _, item := range mapCtx.Slice.Items {
 				buf = appendMapKeyValue(ctx, code, buf, item.Key, item.Value)
 			}
 			buf = appendMapEnd(ctx, code, buf)
-			b = b[:pos[0]]
+			b = b[:mapCtx.First]
 			b = append(b, buf...)
 			mapCtx.Buf = buf
 			encoder.ReleaseMapContext(mapCtx)


Test patch:
diff --git a/size_test.go b/size_test.go
index 3a47195..9804cab 100644
--- a/size_test.go
+++ b/size_test.go
@@ -11,8 +11,8 @@ func TestOpcodeSize(t *testing.T) {
 	const uintptrSize = 4 << (^uintptr(0) >> 63)
 	if uintptrSize == 8 {
 		size := unsafe.Sizeof(encoder.Opcode{})
-		if size != 120 {
-			t.Fatalf("unexpected opcode size: expected 120bytes but got %dbytes", size)
+		if size != 112 {
+			t.Fatalf("unexpected opcode size: expected 112bytes but got %dbytes", size)
 		}
 	}
 }