Browse Source

Optimised the handling of literal values during compilation. Bumped minimum required Go version to 1.20. Closes #566.

Dmitry Panov 1 year ago
parent
commit
cba40bd09c
7 changed files with 93 additions and 44 deletions
  1. 1 1
      README.md
  2. 27 14
      compiler.go
  3. 13 13
      compiler_expr.go
  4. 31 2
      compiler_test.go
  5. 1 1
      go.mod
  6. 4 2
      vm.go
  7. 16 11
      vm_test.go

+ 1 - 1
README.md

@@ -10,7 +10,7 @@ performance.
 
 
 This project was largely inspired by [otto](https://github.com/robertkrimen/otto).
 This project was largely inspired by [otto](https://github.com/robertkrimen/otto).
 
 
-Minimum required Go version is 1.16.
+The minimum required Go version is 1.20.
 
 
 Features
 Features
 --------
 --------

+ 27 - 14
compiler.go

@@ -67,8 +67,7 @@ type srcMapItem struct {
 // This representation is not linked to a runtime in any way and can be used concurrently.
 // This representation is not linked to a runtime in any way and can be used concurrently.
 // It is always preferable to use a Program over a string when running code as it skips the compilation step.
 // It is always preferable to use a Program over a string when running code as it skips the compilation step.
 type Program struct {
 type Program struct {
-	code   []instruction
-	values []Value
+	code []instruction
 
 
 	funcName unistring.String
 	funcName unistring.String
 	src      *file.File
 	src      *file.File
@@ -88,6 +87,8 @@ type compiler struct {
 	ctxVM  *vm // VM in which an eval() code is compiled
 	ctxVM  *vm // VM in which an eval() code is compiled
 
 
 	codeScratchpad []instruction
 	codeScratchpad []instruction
+
+	stringCache map[unistring.String]Value
 }
 }
 
 
 type binding struct {
 type binding struct {
@@ -384,6 +385,29 @@ func (c *compiler) popScope() {
 	c.scope = c.scope.outer
 	c.scope = c.scope.outer
 }
 }
 
 
+func (c *compiler) emitLiteralString(s String) {
+	key := s.string()
+	if c.stringCache == nil {
+		c.stringCache = make(map[unistring.String]Value)
+	}
+	internVal := c.stringCache[key]
+	if internVal == nil {
+		c.stringCache[key] = s
+		internVal = s
+	}
+
+	c.emit(loadVal{internVal})
+}
+
+func (c *compiler) emitLiteralValue(v Value) {
+	if s, ok := v.(String); ok {
+		c.emitLiteralString(s)
+		return
+	}
+
+	c.emit(loadVal{v})
+}
+
 func newCompiler() *compiler {
 func newCompiler() *compiler {
 	c := &compiler{
 	c := &compiler{
 		p: &Program{},
 		p: &Program{},
@@ -394,23 +418,11 @@ func newCompiler() *compiler {
 	return c
 	return c
 }
 }
 
 
-func (p *Program) defineLiteralValue(val Value) uint32 {
-	for idx, v := range p.values {
-		if v.SameAs(val) {
-			return uint32(idx)
-		}
-	}
-	idx := uint32(len(p.values))
-	p.values = append(p.values, val)
-	return idx
-}
-
 func (p *Program) dumpCode(logger func(format string, args ...interface{})) {
 func (p *Program) dumpCode(logger func(format string, args ...interface{})) {
 	p._dumpCode("", logger)
 	p._dumpCode("", logger)
 }
 }
 
 
 func (p *Program) _dumpCode(indent string, logger func(format string, args ...interface{})) {
 func (p *Program) _dumpCode(indent string, logger func(format string, args ...interface{})) {
-	logger("values: %+v", p.values)
 	dumpInitFields := func(initFields *Program) {
 	dumpInitFields := func(initFields *Program) {
 		i := indent + ">"
 		i := indent + ">"
 		logger("%s ---- init_fields:", i)
 		logger("%s ---- init_fields:", i)
@@ -982,6 +994,7 @@ func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) {
 	}
 	}
 
 
 	scope.finaliseVarAlloc(0)
 	scope.finaliseVarAlloc(0)
+	c.stringCache = nil
 }
 }
 
 
 func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) {
 func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) {

+ 13 - 13
compiler_expr.go

@@ -234,7 +234,7 @@ type compiledOptional struct {
 func (e *defaultDeleteExpr) emitGetter(putOnStack bool) {
 func (e *defaultDeleteExpr) emitGetter(putOnStack bool) {
 	e.expr.emitGetter(false)
 	e.expr.emitGetter(false)
 	if putOnStack {
 	if putOnStack {
-		e.c.emit(loadVal(e.c.p.defineLiteralValue(valueTrue)))
+		e.c.emitLiteralValue(valueTrue)
 	}
 	}
 }
 }
 
 
@@ -373,7 +373,7 @@ func (e *baseCompiledExpr) addSrcMap() {
 func (e *constantExpr) emitGetter(putOnStack bool) {
 func (e *constantExpr) emitGetter(putOnStack bool) {
 	if putOnStack {
 	if putOnStack {
 		e.addSrcMap()
 		e.addSrcMap()
-		e.c.emit(loadVal(e.c.p.defineLiteralValue(e.val)))
+		e.c.emitLiteralValue(e.val)
 	}
 	}
 }
 }
 
 
@@ -1261,7 +1261,7 @@ func (e *compiledAssignExpr) emitGetter(putOnStack bool) {
 
 
 func (e *compiledLiteral) emitGetter(putOnStack bool) {
 func (e *compiledLiteral) emitGetter(putOnStack bool) {
 	if putOnStack {
 	if putOnStack {
-		e.c.emit(loadVal(e.c.p.defineLiteralValue(e.val)))
+		e.c.emitLiteralValue(e.val)
 	}
 	}
 }
 }
 
 
@@ -1272,15 +1272,15 @@ func (e *compiledLiteral) constant() bool {
 func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) {
 func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) {
 	if e.tag == nil {
 	if e.tag == nil {
 		if len(e.elements) == 0 {
 		if len(e.elements) == 0 {
-			e.c.emit(loadVal(e.c.p.defineLiteralValue(stringEmpty)))
+			e.c.emitLiteralString(stringEmpty)
 		} else {
 		} else {
 			tail := e.elements[len(e.elements)-1].Parsed
 			tail := e.elements[len(e.elements)-1].Parsed
 			if len(e.elements) == 1 {
 			if len(e.elements) == 1 {
-				e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(tail))))
+				e.c.emitLiteralString(stringValueFromRaw(tail))
 			} else {
 			} else {
 				stringCount := 0
 				stringCount := 0
 				if head := e.elements[0].Parsed; head != "" {
 				if head := e.elements[0].Parsed; head != "" {
-					e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(head))))
+					e.c.emitLiteralString(stringValueFromRaw(head))
 					stringCount++
 					stringCount++
 				}
 				}
 				e.expressions[0].emitGetter(true)
 				e.expressions[0].emitGetter(true)
@@ -1288,7 +1288,7 @@ func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) {
 				stringCount++
 				stringCount++
 				for i := 1; i < len(e.elements)-1; i++ {
 				for i := 1; i < len(e.elements)-1; i++ {
 					if elt := e.elements[i].Parsed; elt != "" {
 					if elt := e.elements[i].Parsed; elt != "" {
-						e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(elt))))
+						e.c.emitLiteralString(stringValueFromRaw(elt))
 						stringCount++
 						stringCount++
 					}
 					}
 					e.expressions[i].emitGetter(true)
 					e.expressions[i].emitGetter(true)
@@ -1296,7 +1296,7 @@ func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) {
 					stringCount++
 					stringCount++
 				}
 				}
 				if tail != "" {
 				if tail != "" {
-					e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(tail))))
+					e.c.emitLiteralString(stringValueFromRaw(tail))
 					stringCount++
 					stringCount++
 				}
 				}
 				e.c.emit(concatStrings(stringCount))
 				e.c.emit(concatStrings(stringCount))
@@ -2450,7 +2450,7 @@ func (c *compiler) emitThrow(v Value) {
 			c.emit(loadDynamic(t))
 			c.emit(loadDynamic(t))
 			msg := o.self.getStr("message", nil)
 			msg := o.self.getStr("message", nil)
 			if msg != nil {
 			if msg != nil {
-				c.emit(loadVal(c.p.defineLiteralValue(msg)))
+				c.emitLiteralValue(msg)
 				c.emit(_new(1))
 				c.emit(_new(1))
 			} else {
 			} else {
 				c.emit(_new(0))
 				c.emit(_new(0))
@@ -2467,7 +2467,7 @@ func (c *compiler) emitConst(expr compiledExpr, putOnStack bool) {
 	v, ex := c.evalConst(expr)
 	v, ex := c.evalConst(expr)
 	if ex == nil {
 	if ex == nil {
 		if putOnStack {
 		if putOnStack {
-			c.emit(loadVal(c.p.defineLiteralValue(v)))
+			c.emitLiteralValue(v)
 		}
 		}
 	} else {
 	} else {
 		c.emitThrow(ex.val)
 		c.emitThrow(ex.val)
@@ -2633,7 +2633,7 @@ func (e *compiledLogicalOr) emitGetter(putOnStack bool) {
 				e.c.emitExpr(e.right, putOnStack)
 				e.c.emitExpr(e.right, putOnStack)
 			} else {
 			} else {
 				if putOnStack {
 				if putOnStack {
-					e.c.emit(loadVal(e.c.p.defineLiteralValue(v)))
+					e.c.emitLiteralValue(v)
 				}
 				}
 			}
 			}
 		} else {
 		} else {
@@ -2674,7 +2674,7 @@ func (e *compiledCoalesce) emitGetter(putOnStack bool) {
 				e.c.emitExpr(e.right, putOnStack)
 				e.c.emitExpr(e.right, putOnStack)
 			} else {
 			} else {
 				if putOnStack {
 				if putOnStack {
-					e.c.emit(loadVal(e.c.p.defineLiteralValue(v)))
+					e.c.emitLiteralValue(v)
 				}
 				}
 			}
 			}
 		} else {
 		} else {
@@ -2714,7 +2714,7 @@ func (e *compiledLogicalAnd) emitGetter(putOnStack bool) {
 	if e.left.constant() {
 	if e.left.constant() {
 		if v, ex := e.c.evalConst(e.left); ex == nil {
 		if v, ex := e.c.evalConst(e.left); ex == nil {
 			if !v.ToBoolean() {
 			if !v.ToBoolean() {
-				e.c.emit(loadVal(e.c.p.defineLiteralValue(v)))
+				e.c.emitLiteralValue(v)
 			} else {
 			} else {
 				e.c.emitExpr(e.right, putOnStack)
 				e.c.emitExpr(e.right, putOnStack)
 			}
 			}

+ 31 - 2
compiler_test.go

@@ -4,6 +4,9 @@ import (
 	"os"
 	"os"
 	"sync"
 	"sync"
 	"testing"
 	"testing"
+	"unsafe"
+
+	"github.com/dop251/goja/unistring"
 )
 )
 
 
 const TESTLIB = `
 const TESTLIB = `
@@ -4697,9 +4700,15 @@ func TestBadObjectKey(t *testing.T) {
 
 
 func TestConstantFolding(t *testing.T) {
 func TestConstantFolding(t *testing.T) {
 	testValues := func(prg *Program, result Value, t *testing.T) {
 	testValues := func(prg *Program, result Value, t *testing.T) {
-		if len(prg.values) != 1 || !prg.values[0].SameAs(result) {
+		values := make(map[unistring.String]struct{})
+		for _, ins := range prg.code {
+			if lv, ok := ins.(loadVal); ok {
+				values[lv.v.string()] = struct{}{}
+			}
+		}
+		if len(values) != 1 {
 			prg.dumpCode(t.Logf)
 			prg.dumpCode(t.Logf)
-			t.Fatalf("values: %v", prg.values)
+			t.Fatalf("values: %v", values)
 		}
 		}
 	}
 	}
 	f := func(src string, result Value, t *testing.T) {
 	f := func(src string, result Value, t *testing.T) {
@@ -4743,6 +4752,26 @@ func TestConstantFolding(t *testing.T) {
 	})
 	})
 }
 }
 
 
+func TestStringInterning(t *testing.T) {
+	const SCRIPT = `
+	const str1 = "Test";
+	function f() {
+		return "Test";
+	}
+	[str1, f()];
+	`
+	vm := New()
+	res, err := vm.RunString(SCRIPT)
+	if err != nil {
+		t.Fatal(err)
+	}
+	str1 := res.(*Object).Get("0").String()
+	str2 := res.(*Object).Get("1").String()
+	if unsafe.StringData(str1) != unsafe.StringData(str2) {
+		t.Fatal("not interned")
+	}
+}
+
 func TestAssignBeforeInit(t *testing.T) {
 func TestAssignBeforeInit(t *testing.T) {
 	const SCRIPT = `
 	const SCRIPT = `
 	assert.throws(ReferenceError, () => {
 	assert.throws(ReferenceError, () => {

+ 1 - 1
go.mod

@@ -1,6 +1,6 @@
 module github.com/dop251/goja
 module github.com/dop251/goja
 
 
-go 1.16
+go 1.20
 
 
 require (
 require (
 	github.com/dlclark/regexp2 v1.7.0
 	github.com/dlclark/regexp2 v1.7.0

+ 4 - 2
vm.go

@@ -897,10 +897,12 @@ func (vm *vm) toCallee(v Value) *Object {
 	panic(vm.r.NewTypeError("Value is not an object: %s", v.toString()))
 	panic(vm.r.NewTypeError("Value is not an object: %s", v.toString()))
 }
 }
 
 
-type loadVal uint32
+type loadVal struct {
+	v Value
+}
 
 
 func (l loadVal) exec(vm *vm) {
 func (l loadVal) exec(vm *vm) {
-	vm.push(vm.prg.values[l])
+	vm.push(l.v)
 	vm.pc++
 	vm.pc++
 }
 }
 
 

+ 16 - 11
vm_test.go

@@ -22,15 +22,14 @@ func TestVM1(t *testing.T) {
 	vm := r.vm
 	vm := r.vm
 
 
 	vm.prg = &Program{
 	vm.prg = &Program{
-		src:    file.NewFile("dummy", "", 1),
-		values: []Value{valueInt(2), valueInt(3), asciiString("test")},
+		src: file.NewFile("dummy", "", 1),
 		code: []instruction{
 		code: []instruction{
 			&bindGlobal{vars: []unistring.String{"v"}},
 			&bindGlobal{vars: []unistring.String{"v"}},
 			newObject,
 			newObject,
 			setGlobal("v"),
 			setGlobal("v"),
-			loadVal(2),
-			loadVal(1),
-			loadVal(0),
+			loadVal{asciiString("test")},
+			loadVal{valueInt(3)},
+			loadVal{valueInt(2)},
 			add,
 			add,
 			setElem,
 			setElem,
 			pop,
 			pop,
@@ -103,9 +102,7 @@ func BenchmarkVmNOP2(b *testing.B) {
 	r.init()
 	r.init()
 
 
 	vm := r.vm
 	vm := r.vm
-	vm.prg = &Program{
-		values: []Value{intToValue(2), intToValue(3)},
-	}
+	vm.prg = &Program{}
 
 
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
 		vm.pc = 0
 		vm.pc = 0
@@ -152,10 +149,9 @@ func BenchmarkVm1(b *testing.B) {
 	//ins2 := loadVal1(1)
 	//ins2 := loadVal1(1)
 
 
 	vm.prg = &Program{
 	vm.prg = &Program{
-		values: []Value{valueInt(2), valueInt(3)},
 		code: []instruction{
 		code: []instruction{
-			loadVal(0),
-			loadVal(1),
+			loadVal{valueInt(2)},
+			loadVal{valueInt(3)},
 			add,
 			add,
 		},
 		},
 	}
 	}
@@ -278,3 +274,12 @@ func BenchmarkAssertInt(b *testing.B) {
 		}
 		}
 	}
 	}
 }
 }
+
+func BenchmarkLoadVal(b *testing.B) {
+	var ins instruction
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		ins = loadVal{valueInt(1)}
+		_ = ins
+	}
+}