-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
Currently, if a consumer creating a Measurement
wants to provide tags, several ctor overloads are available. One possibility when working with metrics is that the consumer creates a TagList
that holds tags for use with measurements. The TagList
struct is designed for performance cases and avoids allocating an array if the number of tags is less than nine. When passing a TagList
to the constructor for a new Measurement
, I noticed that it resolves to the IEnumerable<KeyValuePair<string, object?>>
overload. This issue is because it causes the TagList
to be boxed, allocating 160B on the heap.
Since this type is intended to improve performance and reduce allocations, I propose creating a specific ctor overload on Measurement
to avoid the boxing. I also propose we use the in
parameter modifier to avoid copying where possible. I believe ref readonly
would be more correct here, but that would introduce a break for any existing call sites as they would be required to add the ref
keyword to pass their argument.
Based on a local POC, I ran some benchmarks with the proposed API and an initial implementation.
| Method | Size | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---------------- |----- |-----------:|----------:|----------:|-----------:|---------:|--------:|-------:|-------:|----------:|------------:|
| TagListOriginal | 1 | 36.610 ns | 0.7556 ns | 1.2415 ns | 36.396 ns | baseline | | 0.0216 | - | 272 B | |
| TagListNewIn | 1 | 6.684 ns | 0.1508 ns | 0.1613 ns | 6.685 ns | -82% | 3.5% | 0.0032 | - | 40 B | -85% |
| TagListNewNoIn | 1 | 11.576 ns | 0.2476 ns | 0.2316 ns | 11.520 ns | -69% | 3.7% | 0.0032 | - | 40 B | -85% |
| | | | | | | | | | | | |
| TagListOriginal | 8 | 71.095 ns | 1.4430 ns | 1.8249 ns | 70.799 ns | baseline | | 0.0395 | - | 496 B | |
| TagListNewIn | 8 | 34.036 ns | 0.6731 ns | 0.9214 ns | 33.787 ns | -52% | 3.2% | 0.0121 | - | 152 B | -69% |
| TagListNewNoIn | 8 | 39.731 ns | 0.7731 ns | 0.6853 ns | 39.720 ns | -44% | 3.3% | 0.0121 | - | 152 B | -69% |
| | | | | | | | | | | | |
| TagListOriginal | 100 | 165.453 ns | 3.1845 ns | 3.1276 ns | 165.638 ns | baseline | | 0.2742 | 0.0017 | 3440 B | |
| TagListNewIn | 100 | 71.436 ns | 1.1935 ns | 1.2256 ns | 71.489 ns | -57% | 2.7% | 0.1293 | - | 1624 B | -53% |
| TagListNewNoIn | 100 | 78.025 ns | 1.8611 ns | 5.3698 ns | 76.523 ns | -49% | 6.6% | 0.1293 | - | 1624 B | -53% |
/cc @tarekgh
API Proposal
namespace System.Diagnostics.Metrics;
public readonly struct Measurement<T> where T : struct
{
public Measurement(T value) { }
public Measurement(T value, IEnumerable<KeyValuePair<string, object?>>? tags) { }
public Measurement(T value, params KeyValuePair<string, object?>[]? tags) { }
public Measurement(T value, params ReadOnlySpan<KeyValuePair<string, object?>> tags) { }
+ public Measurement(T value, in TagList tags) { }
...
}
API Usage
var tagList = new TagList(
new KeyValuePair<string, object?>("key1", "value1"),
new KeyValuePair<string, object?>("key2", "value2"));
var measurement = new Measurement<long>(10, in tagList);
NOTE: The in
keyword is optional, so non-breaking.
Alternative Designs
Add a factory method to Measurement
to cover this scenario. A disadvantage is that the existing customer code would continue to cause boxing via the IEnumerable
ctor, so there's no "free" performance gain from upgrading to a new version of .NET.
public static Measurement<T> Create(T value, in TagList tags);
Risks
No response