Skip to content

perf: replace the regex in MeterId with a byte loop - #144

Open
andersvw wants to merge 2 commits into
Netflix:mainfrom
andersvw:meter-id-without-regex
Open

andersvw wants to merge 2 commits into
Netflix:mainfrom
andersvw:meter-id-without-regex

Conversation

@andersvw

@andersvw andersvw commented Oct 15, 2026 •

Copy link
Copy Markdown

MeterId's constructor runs std::regex_replace on the name and on every tag key and value to build the spectatord id. This replaces the regex with a byte loop over the same character class ([-._A-Za-z0-9~^]), and builds the id with string appends instead of an ostringstream. The spectatord id is unchanged byte for byte: non-ASCII input is still replaced one byte at a time.

Registry::Create*(name, tags) builds two MeterIds per call, one from the tags and one from WithTags for the configured extra tags. Callers that create meters per measurement pay that cost on every record.

Benchmarks

clang 19, -O2, an 8-vCPU Linux container, main at 656bf58.

performance_tests/performance_test.cpp, unmodified (CreateCounter(name, tags).Increment() with 2 tags), with the UDS writer sending to a socket that discards what it receives. One 2-minute run each:

Writer main This PR Speedup
Buffered (4096 bytes), 4 threads 1.57M iterations/s 2.53M iterations/s 1.60×
Unbuffered, 1 thread 250K iterations/s 360K iterations/s 1.44×

Constructing a MeterId takes, in ns per id (median of 3 runs of 200K):

Tags main This PR Speedup
0 504 55 9.2×
2 1,602 262 6.1×
6 4,608 710 6.5×

Per-thread times with 4 and 8 threads were within 5% of these, so the cost is the regex work itself, not contention.

In NDrive, which creates several meters per FUSE read, this change cut CPU per cached 128 KiB read from 261 µs to 208 µs, even with its meters already cached.

Testing

New tests cover invalid characters in tag keys and values, every valid character, and multi-byte UTF-8. Built standalone with clang 19 against libstdc++, the new tests pass both with and without this change. In that environment, HashSameTags, LookupTags and SpectatordId also fail on main, because they depend on unordered_map iteration order, and they fail the same way with this change. I have not run the gcc-15/conan build.

Originally written by Colin Howes.

🤖 Generated with Claude Code

MeterId's constructor ran std::regex_replace on the name and on every tag key and value. Callers that create meters per measurement, such as NDrive, paid that on every record: building a MeterId with six tags drops from about 4.9 us to 0.8 us with a byte loop over the same character class. The spectatord id is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@andersvw
andersvw marked this pull request as ready for review October 15, 2026 19:56
Comment thread libs/meter/meter_id/meter_id.cpp Outdated
Review feedback: building the id by appending ReplaceInvalidChars results grew the string and made a temporary string per key and value. Each byte maps to one byte, so the size is known up front: reserve it once and append the replaced bytes directly. ToSpectatorId now allocates once regardless of tag count (was 4 with 2 tags, 10 with 6), and a six-tag MeterId drops from about 713 ns to 648 ns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ecbadeaux

ecbadeaux commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

@andersvw @brharrington Can we pause on merging this until I review? I have a branch I wrote 5 months ago that had these exact same changes. I'd like to compare the two before merging.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants