Skip to content

feat(label_namer): skip allocation for already-compliant labels#69

Open
aknuds1 wants to merge 2 commits into
mainfrom
arve/label-namer-fast-path
Open

feat(label_namer): skip allocation for already-compliant labels#69
aknuds1 wants to merge 2 commits into
mainfrom
arve/label-namer-fast-path

Conversation

@aknuds1
Copy link
Copy Markdown
Contributor

@aknuds1 aknuds1 commented May 20, 2026

Add a single forward-scan predicate canFastPathLabel that detects when LabelNamer.Build would return its input unchanged. On a hit, Build returns the input string directly with zero heap allocations, addressing the "already-compliant labels are wastefully reallocated" half of #68.

The predicate covers the four LabelNamer config combinations and the reserved-label round-trip (...) where the inner range sanitizes to itself. A property test asserts predicate-Build agreement across every case in labelTestCases × the four configs; testing.AllocsPerRun confirms the fast path is allocation-free.

Please note that there is one case that regresses in CPU utilization (+20.56%, ~18 ns): NormalizeLabel/label_with_foreign_characters. I think foreign characters in OTel metric attributes, while translation is enabled, should be an unusual case though, and the -30% geomean makes the tradeoff worth it.

I realized this simpler optimization could be made while reviewing #68.

goos: darwin
goarch: arm64
pkg: github.com/prometheus/otlptranslator
cpu: Apple M4 Pro
                                                    │ /tmp/main_full.bench.txt │         /tmp/after.bench.txt         │
                                                    │          sec/op          │    sec/op     vs base                │
NormalizeLabel/empty_label-14                                      9.920n ± 3%    9.807n ± 2%        ~ (p=0.853 n=10)
NormalizeLabel/already-valid_label-14                              54.12n ± 3%    15.90n ± 1%  -70.62% (p=0.000 n=10)
NormalizeLabel/label_with_colons-14                                54.10n ± 1%    53.74n ± 1%        ~ (p=0.093 n=10)
NormalizeLabel/label_with_capital_letters-14                       68.88n ± 1%    17.58n ± 6%  -74.48% (p=0.000 n=10)
NormalizeLabel/label_with_special_characters-14                    79.08n ± 1%    73.98n ± 1%   -6.44% (p=0.000 n=10)
NormalizeLabel/label_with_foreign_characters-14                    89.62n ± 2%   108.05n ± 1%  +20.56% (p=0.000 n=10)
NormalizeLabel/label_with_dots-14                                  47.88n ± 1%    50.65n ± 1%   +5.80% (p=0.000 n=10)
NormalizeLabel/label_starting_with_digits-14                       42.02n ± 2%    42.28n ± 1%        ~ (p=0.239 n=10)
NormalizeLabel/label_starting_with_underscores-14                  98.92n ± 1%    25.93n ± 2%  -73.79% (p=0.000 n=10)
NormalizeLabel/label_starting_with_2_underscores-14               101.40n ± 1%    99.24n ± 1%   -2.13% (p=0.000 n=10)
NormalizeLabel/reserved_label-14                                   74.03n ± 1%    66.84n ± 1%   -9.71% (p=0.000 n=10)
geomean                                                            57.14n         40.12n       -29.78%

                                                    │ /tmp/main_full.bench.txt │          /tmp/after.bench.txt           │
                                                    │           B/op           │    B/op     vs base                     │
NormalizeLabel/empty_label-14                                       16.00 ± 0%   16.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/already-valid_label-14                               24.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
NormalizeLabel/label_with_colons-14                                 24.00 ± 0%   24.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_with_capital_letters-14                        24.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
NormalizeLabel/label_with_special_characters-14                     32.00 ± 0%   32.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_with_foreign_characters-14                     48.00 ± 0%   48.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_with_dots-14                                   16.00 ± 0%   16.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_starting_with_digits-14                        24.00 ± 0%   24.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_starting_with_underscores-14                   32.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
NormalizeLabel/label_starting_with_2_underscores-14                 48.00 ± 0%   48.00 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/reserved_label-14                                    56.00 ± 0%   32.00 ± 0%   -42.86% (p=0.000 n=10)
geomean                                                             28.78                    ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean

                                                    │ /tmp/main_full.bench.txt │          /tmp/after.bench.txt           │
                                                    │        allocs/op         │ allocs/op   vs base                     │
NormalizeLabel/empty_label-14                                       1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/already-valid_label-14                               1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)
NormalizeLabel/label_with_colons-14                                 1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_with_capital_letters-14                        1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)
NormalizeLabel/label_with_special_characters-14                     1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_with_foreign_characters-14                     1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_with_dots-14                                   1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_starting_with_digits-14                        2.000 ± 0%   2.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/label_starting_with_underscores-14                   1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)
NormalizeLabel/label_starting_with_2_underscores-14                 1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
NormalizeLabel/reserved_label-14                                    2.000 ± 0%   1.000 ± 0%   -50.00% (p=0.000 n=10)
geomean                                                             1.134                    ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean

@aknuds1 aknuds1 added the enhancement New feature or request label May 20, 2026
@aknuds1 aknuds1 force-pushed the arve/label-namer-fast-path branch 2 times, most recently from edb64cf to f1569da Compare May 20, 2026 12:37
@aknuds1 aknuds1 requested a review from dashpole May 20, 2026 12:37
@aknuds1 aknuds1 force-pushed the arve/label-namer-fast-path branch from f1569da to 25bd9e7 Compare May 20, 2026 12:42
@aknuds1 aknuds1 marked this pull request as ready for review May 20, 2026 12:43
aknuds1 added 2 commits May 20, 2026 14:45
Add a single forward-scan predicate canFastPathLabel that detects when
LabelNamer.Build would return its input unchanged. On a hit, Build
returns the input string directly with zero heap allocations, addressing
the "already-compliant labels are wastefully reallocated" half of #68.

The predicate covers the four LabelNamer config combinations and the
reserved-label round-trip (__...__) where the inner range sanitizes to
itself. A property test asserts predicate-Build agreement across every
case in labelTestCases × the four configs; testing.AllocsPerRun confirms
the fast path is allocation-free.

Benchmarks on Apple M4 Pro:
  already-valid label:        15.77 ns/op  0 B/op  0 allocs/op
  LabelWithCapitalLetters:    18.67 ns/op  0 B/op  0 allocs/op
  _label_starting_with_underscore (default config):
                              25.27 ns/op  0 B/op  0 allocs/op

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
sanitizeLabelName's reserved-label branch returned
"__" + b.String() + "__", which produced two allocations: one for the
builder buffer and one for the string concatenation that wraps it.
Write the "__" wrappers directly into the strings.Builder before and
after the sanitize loop instead; b.Grow(nameLength) already accounts
for the worst-case length (stripped inner ≤ nameLength-4, plus 4
wrapper bytes).

Add "__reserved__label__name__" as a benchmark input so the change is
visible.

benchstat (Apple M4 Pro, -count=10):
  reserved_label:  84.71 ns/op → 66.84 ns/op  (-21.1%)
                   56 B/op    → 32 B/op       (-42.9%)
                   2 allocs/op → 1 allocs/op  (-50.0%)
Other rows unchanged.

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
@aknuds1 aknuds1 force-pushed the arve/label-namer-fast-path branch from 25bd9e7 to f06b2ef Compare May 20, 2026 12:47
Comment thread strconv.go
// canFastPathLabel reports whether LabelNamer.Build would return label unchanged when UTF8Allowed is false.
// When it returns true, the label can be returned directly. The predicate must remain
// consistent with sanitizeLabelName and the post-sanitize prefix logic in LabelNamer.Build.
func canFastPathLabel(label string, preserveMultipleUnderscores, underscoreLabelSanitization bool) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be delegating any of this logic to the prometheus/common validators? E.g. https://github.com/prometheus/common/blob/0f3c348807322ea84d92fc7688b1b37a08e17d1f/model/metric.go#L175

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a look, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants