internal/compile: build index of local vars (#576)

This eliminates the linear scan over locals in the interpreter
when processing named arguments. This reduces the new benchmark
from 1081ns to 843ns (-22%).

(This was inspired by recent work in the Java-based implementation,
but findParams seemed to be a bigger hit in that case, possibly
because of the two extra levels of indirection involved in the
length comparison (hot path) of a Java array of Java strings,
versus Go where they are all in the same cache line.)
diff --git a/internal/compile/compile.go b/internal/compile/compile.go
index b257d70..bc38200 100644
--- a/internal/compile/compile.go
+++ b/internal/compile/compile.go
@@ -345,6 +345,8 @@
 
 	lntOnce sync.Once
 	lnt     []pclinecol // decoded line number table
+
+	localsMap map[string]int
 }
 
 type pclinecol struct {
@@ -403,6 +405,24 @@
 	line, col int32
 }
 
+// finish is called to populate derived fields of Funcode.
+func (fn *Funcode) finish() {
+	// (We could populate this map on first use,
+	// but the sync adds 33% to bench_calling_kwargs.)
+	fn.localsMap = make(map[string]int, len(fn.Locals))
+	for index, binding := range fn.Locals {
+		fn.localsMap[binding.Name] = index
+	}
+}
+
+// Local returns the index of the named local, or -1.
+func (fn *Funcode) Local(name string) int {
+	if i, ok := fn.localsMap[name]; ok {
+		return i
+	}
+	return -1
+}
+
 // Position returns the source position for program counter pc.
 func (fn *Funcode) Position(pc uint32) syntax.Position {
 	fn.lntOnce.Do(fn.decodeLNT)
@@ -680,6 +700,7 @@
 		fmt.Fprintf(os.Stderr, "end function(%s @ %s)\n", name, pos)
 	}
 
+	fcomp.fn.finish()
 	return fn
 }
 
diff --git a/internal/compile/serial.go b/internal/compile/serial.go
index 0dbae47..6131322 100644
--- a/internal/compile/serial.go
+++ b/internal/compile/serial.go
@@ -380,7 +380,7 @@
 	numKwonlyParams := d.int()
 	hasVarargs := d.int() != 0
 	hasKwargs := d.int() != 0
-	return &Funcode{
+	fn := &Funcode{
 		// Prog is filled in later.
 		Pos:             id.Pos,
 		Name:            id.Name,
@@ -396,4 +396,6 @@
 		HasVarargs:      hasVarargs,
 		HasKwargs:       hasKwargs,
 	}
+	fn.finish()
+	return fn
 }
diff --git a/starlark/eval.go b/starlark/eval.go
index be3eb61..a4640e4 100644
--- a/starlark/eval.go
+++ b/starlark/eval.go
@@ -1491,10 +1491,9 @@
 	}
 
 	// Bind keyword arguments to parameters.
-	paramIdents := fn.funcode.Locals[:nparams]
 	for _, pair := range kwargs {
 		k, v := pair[0].(String), pair[1]
-		if i := findParam(paramIdents, string(k)); i >= 0 {
+		if i := fn.funcode.Local(string(k)); i >= 0 && i < nparams {
 			if locals[i] != nil {
 				return fmt.Errorf("function %s got multiple values for parameter %s", fn.Name(), k)
 			}
@@ -1515,6 +1514,8 @@
 	if n < nparams || fn.NumKwonlyParams() > 0 {
 		m := nparams - len(fn.defaults) // first default
 
+		paramIdents := fn.funcode.Locals[:nparams]
+
 		// Report errors for missing required arguments.
 		var missing []string
 		var i int
@@ -1544,15 +1545,6 @@
 	return nil
 }
 
-func findParam(params []compile.Binding, name string) int {
-	for i, param := range params {
-		if param.Name == name {
-			return i
-		}
-	}
-	return -1
-}
-
 // https://github.com/google/starlark-go/blob/master/doc/spec.md#string-interpolation
 func interpolate(format string, x Value) (Value, error) {
 	buf := new(strings.Builder)
diff --git a/starlark/testdata/benchmark.star b/starlark/testdata/benchmark.star
index c448889..d41c000 100644
--- a/starlark/testdata/benchmark.star
+++ b/starlark/testdata/benchmark.star
@@ -25,6 +25,15 @@
     for _ in range(b.n):
         f()
 
+# Test the efficiency of named arguments.
+def bench_calling_kwargs(b):
+    def f(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p):
+        pass
+
+    for _ in range(b.n):
+    	f(a=1, b=2, c=3, d=4, e=5, f=7, g=7, h=8,
+          i=9, j=10, k=11, l=12, m=13, n=14, o=15, p=16)
+
 # Measure overhead of calling a trivial built-in method.
 emptydict = {}
 range1000 = range(1000)