CborWriter: reduce per-call overhead of map and array bookkeeping
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Tái cấu trúc
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- csharp
- Lĩnh vực
- backend, performance
Hướng nghiên cứu
Start by reading CborWriter.cs, CborWriter.Map.cs, CborWriter.Array.cs and CborWriter.String.cs; the issue describes five proposed changes across those files and reports benchmark results. Review the existing writer state management and the relevant benchmark and behavior checks described in the issue. Done means a proposed change preserves the documented behavior while reducing per-call bookkeeping overhead; the issue asks whether the author should open a PR, so implementation scope and review direction still need maintainer agreement.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Terms
main: the unmodifiedCborWritersource from dotnet/runtimemain.- Changed writer: the same source with the five changes listed under Description.
- Minimal writer: a small CBOR writer used only as a reference point, not proposed for anything. It writes headers and UTF-8 bytes straight into an
IBufferWriter<byte>and tracks only which open containers are indefinite-length. Apart from rejecting an end call with no open container, it does no structural validation or conformance checks. - Bookkeeping: the state
CborWriterkeeps to validate structure (current container type, definite length, items written, key and value offsets) and saves and restores for each map or array. - Conversion:
ConvertIndefiniteLengthEncodings = true. Maps and arrays started withWriteStartMap(null)orWriteStartArray(null)are rewritten as definite-length when they close. - Differential fuzz: random sequences of writer calls, including deliberate misuse, run on both
mainand the changed writer. After every call, the two must agree on success or exception (type and message) and on writer state; at the end, on the encoded bytes. Any difference counts as a behavior change. - Boundary test: a fixed, non-random set of documents with container sizes and string lengths around the points where the CBOR length header grows (24 and 256), compared the same way.
- Benchmark names (Text, Pairs, Map1Indef, Map1Def, Doc, DocStrict) are defined in the Data table.
- "Nx faster" / "Nx slower": the ratio of the two times being compared, larger over smaller, both measured in the same session.
Description
In map-heavy writes, most of CborWriter's extra cost over a minimal writer is bookkeeping for each map and array. Writing the one-pair map {"S": v} takes 39.6 ns, while the minimal writer produces the same bytes in 21.6 ns.
I have a set of internal changes, with the same behavior and no public API changes, that make a small nested document 1.48x faster in Lax mode and 1.09x faster in Strict mode. I'd like to open a PR if you're open to it.
This follows up #136164, where I proposed an IBufferWriter<byte> output mode to avoid the Encode() copy. As I noted there, the copy turned out to be negligible: on the Doc benchmark's document (9.0.5 package), Encode(Span<byte>) added 2.5 ns to 1,536 ns, within measurement error. The bookkeeping below accounts for most of the difference from the minimal writer.
For context, I ran into this in the AWS SDK for .NET, which writes Smithy RPC v2 CBOR request bodies with CborWriter. Its CBOR request benchmarks (12 cases) are 1.30x slower than the same requests in JSON with Utf8JsonWriter (default options). Swapping in the changed writer, with nothing else changed, brings that to 1.02x slower, with identical output. Those are geometric means; per case it ranges from 1.13x slower to 1.42x faster. The existing options don't close the gap: on a document with 255 "attrN": {"S": "valueN"} entries, the best combination I found (definite lengths, conversion off) was 1.06x faster than Lax with indefinite-length maps and conversion (11.0 package, Stopwatch timing, best of 9 rounds).
What the changes target. For every map or array, CborWriter saves the current context into a 9-field StackFrame on a Stack<StackFrame> and restores it on close. Four of the context fields are Nullable<T> (CborMajorType? and three int?), so the checks on every item use lifted nullable operations. In Lax mode, the writer also saves, restores and updates state that only the key checks in Strict and the canonical modes read.
Changes and impact. All changes are in CborWriter.cs, CborWriter.Map.cs, CborWriter.Array.cs and CborWriter.String.cs. Speedups are cumulative vs main; each row was measured in its own session, so treat differences of a few percent as noise.
| # | Change | Map1Indef | Doc |
|---|---|---|---|
| 1 | Replace Stack<StackFrame> with a StackFrame[] and a depth counter; push and pop through ref |
1.09x faster | 1.10x faster |
| 2 | Use sentinel values instead of nullable state fields | 1.20x faster | 1.22x faster |
| 3 | Move the key-sorting flag and the two key-range collections, used only by modes that check keys, to a separate array touched only in those modes | 1.28x faster | 1.30x faster |
| 4 | When closing an indefinite-length map or array with conversion and fewer than 24 elements or pairs, write the initial byte directly instead of rewinding and calling WriteUnsignedInteger |
1.31x faster | 1.33x faster |
| 5 | In Lax mode, skip key and value offset tracking, its save and restore, and CompleteMapWrite; cache RequiresUniqueKeys, RequiresSortedKeys, RequiresDefiniteLengthItems and GetUtf8Encoding in readonly fields |
1.57x faster | 1.51x faster |
Change 1 alone made Text 1.02x slower; change 2 more than made up for it. DocStrict measured 1.09x faster after change 4 and 1.07x after change 5 (it wasn't run for changes 1 to 3).
Configuration
BenchmarkDotNet v0.15.2, macOS 26.7.2 (25G332) [Darwin 25.6.0]
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
.NET 10.0.12 (10.0.1226.42308), Arm64 RyuJIT AdvSIMD
WarmupCount=5, IterationCount=15
Both writers are built from main source in the same benchmark project. The only differences are the namespace and small adaptations to build against the 9.0.5 package's public types (resource strings, enums, and WriteEncodedValue validation, which the benchmarks don't call). The source was taken at 38f5cfdad059; the four files are identical in main as of 8cdbec121572 and in release/11.0. The writer is CborConformanceMode.Lax (Strict for DocStrict) with conversion on, reused through Reset(). I've only measured Arm64 on macOS.
Regression?
Not that I know of. In the same session, the 9.0.5 package and the unmodified main source were within about 3% of each other on every benchmark below.
Data
Mean ns, all from one session:
| Benchmark | main |
changed | changed vs main |
minimal writer |
|---|---|---|---|---|
| Text: 64 short strings in an array, per string | 11.2 | 10.0 | 1.12x faster | 10.5 |
| Pairs: 32 short string pairs in a map, per string | 12.5 | 10.0 | 1.25x faster | 10.4 |
Map1Indef: 64 maps {"S": v}, indefinite length, per map |
44.2 | 28.1 | 1.57x faster | |
| Map1Def: same, definite length, per map | 39.6 | 24.8 | 1.60x faster | 21.6 |
| Doc: nested document, see below | 1,578 | 1,070 | 1.48x faster | 1,022 |
| DocStrict: Doc with unique keys, Strict mode | 2,152 | 1,978 | 1.09x faster |
The minimal writer writes definite lengths directly here and has no Encode step.
Doc is {"TableName": "my-table", "Item": {...}}, where Item has 20 short-key entries like k: {"S": v} or k: {"N": v} plus "nested": {"M": {...}} with 4 more k: {"S": v} entries. That's 28 maps, all started with WriteStartMap(null) and converted on close, and 77 short strings: 389 bytes, then Encode(Span<byte>) into a preallocated buffer. Five of Doc's Item keys are "S", which Lax allows; DocStrict uses 20 unique keys.
Allocations don't change: 0 B for Text, Pairs and Map1, and 128 B for Doc and DocStrict with both writers (the four "a" + i keys the benchmark builds).
The Map1Def loop:
// values: 64 ASCII strings of 1 to 9 characters. Reported per map (OperationsPerInvoke = 64).
var writer = new CborWriter(CborConformanceMode.Lax, convertIndefiniteLengthEncodings: true); // created once
writer.Reset();
writer.WriteStartArray(64);
for (int i = 0; i < 64; i++)
{
writer.WriteStartMap(1); // Map1Indef: WriteStartMap(null)
writer.WriteTextString("S");
writer.WriteTextString(values[i]);
writer.WriteEndMap();
}
writer.WriteEndArray();
Analysis
To check that behavior doesn't change, I compared the changed writer with main two ways. Both found no differences.
- Boundary test: maps and arrays (definite and indefinite) with 0 to 30 and 250 to 260 entries, in every conformance mode with conversion on and off, plus text and byte strings of the same lengths. That's 1,512 documents. It caught an off-by-one I introduced on purpose in change 4.
- Differential fuzz: random sequences of the write APIs, with misuse mixed in, run on all four conformance modes with conversion and
AllowMultipleRootLevelValueson and off, using both a reused writer and a new writer per case. After every call it compares success or exception (type and message),CurrentDepth,BytesWrittenandIsWriteCompleted, and at the end theEncode()bytes. In total: 10,000 cases per configuration and 121,581,530 calls per comparison, about 29% of them throwing.- APIs: text and byte strings (including indefinite-length),
WriteInt64(including extremes),WriteUInt64, double, half, bool, null, simple values, tags, and maps and arrays with duplicate, out-of-order and non-text keys. Sizes are mostly small, sometimes around the 24 and 256 header boundaries. - Misuse: extra items, dropped calls, wrong or extra end calls, random extra calls.
- APIs: text and byte strings (including indefinite-length),
- Ngôn ngữ chính
- C#
- Star
- 18.4k
- Fork
- 5.8k
- Merge trung bình
- 2 ngày 14 giờ
- Pull request đã merge (30 ngày)
- 585
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của dotnet/runtime
-
area-Host untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
dotnet/runtime#137187 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ImmutableSortedSet<T>.Builder: enumerating through IEnumerable<T> doesn't throw on modification and can return wrong itemsCó thể đã có người làm @aw0lid đã nhận hôm nay. Đang mởarea-System.Collections
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/runtime#137167 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
DbTransaction.SupportsSavepoints docs say Release/ReleaseAsync are expected to throw, but they are no-ops by designCó thể đã có người làm @alaricd đã nhận 4 ngày trước. Đang mởarea-System.Transactions untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
dotnet/runtime#137040 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
FileSystemGlobbing: Matcher throws ArgumentOutOfRangeException when the rootDir is the same value as the fileCó thể đã có người làm @makazeu đã nhận 5 ngày trước. Đang mởarea-Extensions-FileSystem
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/runtime#136989 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Configuration binder source generator emits unqualified List<T> when binding arrays without implicit usingsCó thể đã có người làm @makazeu đã nhận 5 ngày trước. Đang mởarea-Extensions-Configuration bug source-generator
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/runtime#136972 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của dotnet/runtime
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Issue-Bug Needs-Triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
JiaFeiMiao-K-Cat/DiscImageStudio#281 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
-
area/access-management Frontend onboarding candidate :handshake:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Altinn/altinn-auth#4941 ·
Maintainer thường phản hồi trong vòng 1 ngày