profile: avoid unused label map allocations and clear labelX in postDecode (#1064)

For every sample with labels, `postDecode` previously allocated all three
label maps (`Label`, `NumLabel`, and `NumUnit`) sized to `len(s.labelX)`,
assigned an empty `NumUnit` map even when numeric labels carried no units,
and never cleared the raw `s.labelX` slice after populating the maps.

Count string, numeric, and unit entries first so only non-empty maps are
allocated with appropriate capacity hints, leave `s.NumUnit` nil when no
units are present, and clear `s.labelX` consistently with the other `*X`
fields in `postDecode`.

```
name                                old time/op         new time/op         delta
ParseLabels/num_label-64                   35.5ms ± 4%         32.8ms ± 5%   -7.54%  (p=0.009 n=6+6)
ParseLabels/num_label_with_unit-64         41.9ms ± 2%         39.7ms ± 5%   -5.13%  (p=0.009 n=5+6)
ParseLabels/string_label-64                35.5ms ± 4%         34.4ms ± 4%     ~     (p=0.132 n=6+6)
ParseLabels/many_labels-64                 80.7ms ± 2%         62.3ms ± 3%  -22.86%  (p=0.002 n=6+6)

name                                old inuse-alloc/op  new inuse-alloc/op  delta
ParseLabels/num_label-64                   20.1MB ± 0%         18.4MB ± 0%   -8.14%  (p=0.000 n=5+6)
ParseLabels/num_label_with_unit-64         27.6MB ± 0%         26.9MB ± 0%   -2.37%  (p=0.008 n=5+5)
ParseLabels/string_label-64                19.3MB ± 0%         18.6MB ± 0%   -3.39%  (p=0.002 n=6+6)
ParseLabels/many_labels-64                 70.9MB ± 0%         28.2MB ± 0%  -60.16%  (p=0.002 n=6+6)

name                                old alloc/op        new alloc/op        delta
ParseLabels/num_label-64                   33.9MB ± 0%         31.9MB ± 0%   -5.79%  (p=0.002 n=6+6)
ParseLabels/num_label_with_unit-64         41.4MB ± 0%         40.4MB ± 0%   -2.37%  (p=0.002 n=6+6)
ParseLabels/string_label-64                34.1MB ± 0%         32.1MB ± 0%   -5.76%  (p=0.002 n=6+6)
ParseLabels/many_labels-64                 94.5MB ± 0%         62.3MB ± 0%  -34.06%  (p=0.004 n=5+6)

name                                old allocs/op       new allocs/op       delta
ParseLabels/num_label-64                     297k ± 0%           257k ± 0%  -13.74%  (p=0.002 n=6+6)
ParseLabels/num_label_with_unit-64           338k ± 0%           318k ± 0%   -6.04%  (p=0.002 n=6+6)
ParseLabels/string_label-64                  297k ± 0%           257k ± 0%  -13.74%  (p=0.002 n=6+6)
ParseLabels/many_labels-64                   706k ± 0%           543k ± 0%  -23.15%  (p=0.026 n=5+6)
```
diff --git a/profile/encode.go b/profile/encode.go
index 8ce9d3c..3af74d4 100644
--- a/profile/encode.go
+++ b/profile/encode.go
@@ -321,9 +321,29 @@
 
 	for _, s := range p.Sample {
 		if len(s.labelX) > 0 {
-			labels := make(map[string][]string, len(s.labelX))
-			numLabels := make(map[string][]int64, len(s.labelX))
-			numUnits := make(map[string][]string, len(s.labelX))
+			var numStr, numNum, numUnit int
+			for _, l := range s.labelX {
+				if l.strX != 0 {
+					numStr++
+				} else if l.numX != 0 || l.unitX != 0 {
+					numNum++
+					if l.unitX != 0 {
+						numUnit++
+					}
+				}
+			}
+			var labels map[string][]string
+			if numStr > 0 {
+				labels = make(map[string][]string, numStr)
+			}
+			var numLabels map[string][]int64
+			if numNum > 0 {
+				numLabels = make(map[string][]int64, numNum)
+			}
+			var numUnits map[string][]string
+			if numUnit > 0 {
+				numUnits = make(map[string][]string, numUnit)
+			}
 			for _, l := range s.labelX {
 				var key, value string
 				key, err = getString(p.stringTable, &l.keyX, err)
@@ -332,10 +352,10 @@
 					labels[key] = append(labels[key], value)
 				} else if l.numX != 0 || l.unitX != 0 {
 					numValues := numLabels[key]
-					units := numUnits[key]
 					if l.unitX != 0 {
 						var unit string
 						unit, err = getString(p.stringTable, &l.unitX, err)
+						units := numUnits[key]
 						units = padStringArray(units, len(numValues))
 						numUnits[key] = append(units, unit)
 					}
@@ -347,14 +367,17 @@
 			}
 			if len(numLabels) > 0 {
 				s.NumLabel = numLabels
-				for key, units := range numUnits {
-					if len(units) > 0 {
-						numUnits[key] = padStringArray(units, len(numLabels[key]))
+				if len(numUnits) > 0 {
+					for key, units := range numUnits {
+						if len(units) > 0 {
+							numUnits[key] = padStringArray(units, len(numLabels[key]))
+						}
 					}
+					s.NumUnit = numUnits
 				}
-				s.NumUnit = numUnits
 			}
 		}
+		s.labelX = nil
 
 		s.Location = locBuffer[:len(s.locationIDX)]
 		locBuffer = locBuffer[len(s.locationIDX):]
diff --git a/profile/profile_test.go b/profile/profile_test.go
index cbe7e0b..3089747 100644
--- a/profile/profile_test.go
+++ b/profile/profile_test.go
@@ -23,6 +23,7 @@
 	"os"
 	"path/filepath"
 	"reflect"
+	"runtime"
 	"strings"
 	"sync"
 	"testing"
@@ -1956,6 +1957,84 @@
 	}
 }
 
+func BenchmarkParseLabels(b *testing.B) {
+	base, err := ParseData(proftest.LargeProfile(b))
+	if err != nil {
+		b.Fatal(err)
+	}
+	for _, tc := range []struct {
+		name   string
+		mutate func(*Profile)
+	}{
+		{
+			name: "num_label",
+			mutate: func(p *Profile) {
+				for _, s := range p.Sample {
+					s.NumLabel = map[string][]int64{"bytes": {512}}
+				}
+			},
+		},
+		{
+			name: "num_label_with_unit",
+			mutate: func(p *Profile) {
+				p.SetNumLabel("bytes", []int64{512}, []string{"bytes"})
+			},
+		},
+		{
+			name: "string_label",
+			mutate: func(p *Profile) {
+				p.SetLabel("key", []string{"value"})
+			},
+		},
+		{
+			name: "many_labels",
+			mutate: func(p *Profile) {
+				for i := 0; i < 8; i++ {
+					p.SetLabel(fmt.Sprintf("key%d", i), []string{"value"})
+				}
+				for _, s := range p.Sample {
+					s.NumLabel = map[string][]int64{"bytes": {512}}
+				}
+			},
+		},
+	} {
+		b.Run(tc.name, func(b *testing.B) {
+			p := base.Copy()
+			tc.mutate(p)
+			var buf bytes.Buffer
+			if err := p.WriteUncompressed(&buf); err != nil {
+				b.Fatal(err)
+			}
+			raw := buf.Bytes()
+
+			b.ReportAllocs()
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				if _, err := ParseData(raw); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.StopTimer()
+
+			runtime.GC()
+			runtime.GC()
+			var m1, m2 runtime.MemStats
+			runtime.ReadMemStats(&m1)
+			parsed, err := ParseData(raw)
+			if err != nil {
+				b.Fatal(err)
+			}
+			runtime.GC()
+			runtime.GC()
+			runtime.ReadMemStats(&m2)
+			runtime.KeepAlive(parsed)
+			if m2.HeapAlloc > m1.HeapAlloc {
+				b.ReportMetric(float64(m2.HeapAlloc-m1.HeapAlloc), "inuse-B/op")
+			}
+		})
+	}
+}
+
 func BenchmarkWrite(b *testing.B) {
 	p, err := Parse(bytes.NewBuffer(proftest.LargeProfile(b)))
 	if err != nil {
diff --git a/profile/proto_test.go b/profile/proto_test.go
index e030bb2..3051eec 100644
--- a/profile/proto_test.go
+++ b/profile/proto_test.go
@@ -152,6 +152,13 @@
 				"size": {"bytes"},
 			},
 		},
+		{
+			Location: []*Location{testL[0]},
+			Value:    []int64{50, 60},
+			NumLabel: map[string][]int64{
+				"bytes": {512},
+			},
+		},
 	},
 	Function: testF,
 	Mapping:  testM,
@@ -168,6 +175,12 @@
 		t.Fatal(err)
 	}
 
+	for i, s := range all2.Sample {
+		if s.labelX != nil {
+			t.Errorf("sample %d: labelX not cleared after decode: %v", i, s.labelX)
+		}
+	}
+
 	js1 := proftest.EncodeJSON(&all)
 	js2 := proftest.EncodeJSON(&all2)
 	if string(js1) != string(js2) {