language: fix performance bugs

- Match should not allocate
- Prevent allocation by not including a string in an invalid Extension.

Change-Id: Icfe091121d99945b70084214ae9e76c79d6ec5b8
Reviewed-on: https://go-review.googlesource.com/30271
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/language/language.go b/language/language.go
index 3c19041..5eecceb 100644
--- a/language/language.go
+++ b/language/language.go
@@ -593,7 +593,7 @@
 			return Extension{ext}, true
 		}
 	}
-	return Extension{string(x)}, false
+	return Extension{}, false
 }
 
 // Extensions returns all extensions of t.
diff --git a/language/match.go b/language/match.go
index eec72bc..8ad9505 100644
--- a/language/match.go
+++ b/language/match.go
@@ -396,8 +396,8 @@
 // matchHeader has the lists of tags for exact matches and matches based on
 // maximized and canonicalized tags for a given language.
 type matchHeader struct {
-	exact []haveTag
-	max   []haveTag
+	exact []*haveTag
+	max   []*haveTag
 }
 
 // haveTag holds a supported Tag and its maximized script and region. The maximized
@@ -457,7 +457,7 @@
 		}
 	}
 	if exact {
-		h.exact = append(h.exact, n)
+		h.exact = append(h.exact, &n)
 	}
 	// Allow duplicate maximized tags, but create a linked list to allow quickly
 	// comparing the equivalents and bail out.
@@ -472,7 +472,7 @@
 			break
 		}
 	}
-	h.max = append(h.max, n)
+	h.max = append(h.max, &n)
 }
 
 // header returns the matchHeader for the given language. It creates one if
@@ -503,7 +503,7 @@
 		pair, _ := makeHaveTag(tag, i)
 		m.header(tag.lang).addIfNew(pair, true)
 	}
-	m.default_ = &m.header(supported[0].lang).exact[0]
+	m.default_ = m.header(supported[0].lang).exact[0]
 	for i, tag := range supported {
 		pair, max := makeHaveTag(tag, i)
 		if max != tag.lang {
@@ -520,7 +520,8 @@
 				return
 			}
 			hw := m.header(langID(want))
-			for _, v := range hh.max {
+			for _, ht := range hh.max {
+				v := *ht
 				if conf < v.conf {
 					v.conf = conf
 				}
@@ -580,7 +581,7 @@
 				continue
 			}
 			for i := range h.exact {
-				have := &h.exact[i]
+				have := h.exact[i]
 				if have.tag.equalsRest(w) {
 					return have, w, Exact
 				}
@@ -591,7 +592,7 @@
 			// Base language is not defined.
 			if h != nil {
 				for i := range h.exact {
-					have := &h.exact[i]
+					have := h.exact[i]
 					if have.tag.equalsRest(w) {
 						return have, w, Exact
 					}
@@ -609,11 +610,11 @@
 		}
 		// Check for match based on maximized tag.
 		for i := range h.max {
-			have := &h.max[i]
+			have := h.max[i]
 			best.update(have, w, max.script, max.region)
 			if best.conf == Exact {
 				for have.nextMax != 0 {
-					have = &h.max[have.nextMax]
+					have = h.max[have.nextMax]
 					best.update(have, w, max.script, max.region)
 				}
 				return best.have, best.want, High
diff --git a/language/match_test.go b/language/match_test.go
index 57b1644..9d87a09 100644
--- a/language/match_test.go
+++ b/language/match_test.go
@@ -10,6 +10,8 @@
 	"fmt"
 	"strings"
 	"testing"
+
+	"golang.org/x/text/internal/testtext"
 )
 
 var verbose = flag.Bool("verbose", false, "set to true to print the internal tables of matchers")
@@ -246,22 +248,23 @@
 	return fmt.Sprintf("%v:%d:%v:%v-%v|%v", t.tag, t.index, t.conf, t.maxRegion, t.maxScript, t.altScript)
 }
 
+func parseSupported(list string) (out []Tag) {
+	for _, s := range strings.Split(list, ",") {
+		out = append(out, mk(strings.TrimSpace(s)))
+	}
+	return out
+}
+
 // The test set for TestBestMatch is defined in data_test.go.
 func TestBestMatch(t *testing.T) {
-	parse := func(list string) (out []Tag) {
-		for _, s := range strings.Split(list, ",") {
-			out = append(out, mk(strings.TrimSpace(s)))
-		}
-		return out
-	}
 	for i, tt := range matchTests {
-		supported := parse(tt.supported)
+		supported := parseSupported(tt.supported)
 		m := newMatcher(supported)
 		if *verbose {
 			fmt.Printf("%s:\n%v\n", tt.comment, m)
 		}
 		for _, tm := range tt.test {
-			tag, _, conf := m.Match(parse(tm.desired)...)
+			tag, _, conf := m.Match(parseSupported(tm.desired)...)
 			if tag.String() != tm.match {
 				t.Errorf("%d:%s: find %s in %q: have %s; want %s (%v)\n", i, tt.comment, tm.desired, tt.supported, tag, tm.match, conf)
 			}
@@ -269,6 +272,18 @@
 	}
 }
 
+func TestBestMatchAlloc(t *testing.T) {
+	m := NewMatcher(parseSupported("en sr nl"))
+	// Go allocates when creating a list of tags from a single tag!
+	list := []Tag{English}
+	avg := testtext.AllocsPerRun(1, func() {
+		m.Match(list...)
+	})
+	if avg > 0 {
+		t.Errorf("got %f; want 0", avg)
+	}
+}
+
 var benchHave = []Tag{
 	mk("en"),
 	mk("en-GB"),