From 45760b25df9cefe0a6542f175a7ab38c3f40f8c7 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Thu, 19 Dec 2024 12:26:05 -0800 Subject: [PATCH 1/2] Reduce allocations during checksum creation. Initially creating this PR as a draft PR to get speedometer results to see if this actually improves things. From the csharp editing scrolling speedometer, I see about 0.3% of total allocations in the VS process occurring in Checksum.Create. A large part of these allocations are stream/writer constructions. Instead, this PR attempts to use a small pool to reuse and reset these objects. --- .../Workspace/Solution/Checksum_Factory.cs | 23 ++++++++++++++----- .../ObjectWriter.WriterReferenceMap.cs | 6 +++++ .../Core/Serialization/ObjectWriter.cs | 13 +++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs index e0804d3d9eb69..fbdeb6dad9008 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs @@ -22,6 +22,8 @@ internal readonly partial record struct Checksum private static readonly ObjectPool s_incrementalHashPool = new(() => new(), size: 20); + private static readonly ObjectPool s_objectWriterPool = + new(() => new(SerializableBytes.CreateWritableStream(), leaveOpen: true, writeValidationBytes: true), size: 4); public static Checksum Create(IEnumerable values) { @@ -57,15 +59,24 @@ public static Checksum Create(Stream stream) public static Checksum Create(T @object, Action writeObject) { - using var stream = SerializableBytes.CreateWritableStream(); + // Obtain a writer from the pool + var objectWriter = s_objectWriterPool.Allocate(); + var stream = objectWriter.BaseStream; - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) - { - writeObject(@object, objectWriter); - } + // Invoke the callback to Write object into objectWriter + writeObject(@object, objectWriter); + // Include validation bytes in the new checksum from the stream stream.Position = 0; - return Create(stream); + var newChecksum = Create(stream); + + // Reset object writer back to it's initial state + objectWriter.Reset(); + + // Release the writer back to the pool + s_objectWriterPool.Free(objectWriter); + + return newChecksum; } public static Checksum Create(Checksum checksum1, Checksum checksum2) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.WriterReferenceMap.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.WriterReferenceMap.cs index d0a4b2a77fbb0..7a93830163835 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.WriterReferenceMap.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.WriterReferenceMap.cs @@ -42,6 +42,12 @@ public readonly void Dispose() } } + public void Reset() + { + _valueToIdMap.Clear(); + _nextId = 0; + } + public bool TryGetReferenceId(string value, out int referenceId) => _valueToIdMap.TryGetValue(value, out referenceId); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs index a485579485382..457125d43889d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs @@ -129,6 +129,19 @@ public void Dispose() public void WriteUInt16(ushort value) => _writer.Write(value); public void WriteString(string? value) => WriteStringValue(value); + public Stream BaseStream => _writer.BaseStream; + + public void Reset(bool includeValidationBytes = true) + { + _stringReferenceMap.Reset(); + _writer.BaseStream.Position = includeValidationBytes ? 2 : 0; + + if (_writer.BaseStream is SerializableBytes.ReadWriteStream pooledStream) + pooledStream.SetLength(_writer.BaseStream.Position, truncate: false); + else + _writer.BaseStream.SetLength(_writer.BaseStream.Position); + } + /// /// Used so we can easily grab the low/high 64bits of a guid for serialization. /// From 909becab995dcc3cd93ce35bb9ef937e07ca7e00 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Thu, 2 Jan 2025 14:58:17 -0800 Subject: [PATCH 2/2] Rewrite the validation bytes instead of setting the stream position after them Added some comments to clarify --- .../Workspace/Solution/Checksum_Factory.cs | 8 ++++++-- .../Compiler/Core/Serialization/ObjectWriter.cs | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs index fbdeb6dad9008..9583c253e5886 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs @@ -22,6 +22,9 @@ internal readonly partial record struct Checksum private static readonly ObjectPool s_incrementalHashPool = new(() => new(), size: 20); + + // Pool of ObjectWriters to reduce allocations. The pool size is intentionally small as the writers are used for such + // a short period that concurrent usage of different items from the pool is infrequent. private static readonly ObjectPool s_objectWriterPool = new(() => new(SerializableBytes.CreateWritableStream(), leaveOpen: true, writeValidationBytes: true), size: 4); @@ -61,17 +64,18 @@ public static Checksum Create(T @object, Action writeObject) { // Obtain a writer from the pool var objectWriter = s_objectWriterPool.Allocate(); - var stream = objectWriter.BaseStream; // Invoke the callback to Write object into objectWriter writeObject(@object, objectWriter); // Include validation bytes in the new checksum from the stream + var stream = objectWriter.BaseStream; stream.Position = 0; var newChecksum = Create(stream); - // Reset object writer back to it's initial state + // Reset object writer back to it's initial state, including the validation bytes objectWriter.Reset(); + objectWriter.WriteValidationBytes(); // Release the writer back to the pool s_objectWriterPool.Free(objectWriter); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs index 457125d43889d..31e1adb719e71 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Serialization/ObjectWriter.cs @@ -131,15 +131,24 @@ public void Dispose() public Stream BaseStream => _writer.BaseStream; - public void Reset(bool includeValidationBytes = true) + public void Reset() { _stringReferenceMap.Reset(); - _writer.BaseStream.Position = includeValidationBytes ? 2 : 0; + + // Reset the position and length back to zero + _writer.BaseStream.Position = 0; if (_writer.BaseStream is SerializableBytes.ReadWriteStream pooledStream) - pooledStream.SetLength(_writer.BaseStream.Position, truncate: false); + { + // ReadWriteStream.SetLength allows us to indicate to not truncate, allowing + // reuse of the backing arrays. + pooledStream.SetLength(0, truncate: false); + } else - _writer.BaseStream.SetLength(_writer.BaseStream.Position); + { + // Otherwise, set the new length via the standard Stream.SetLength + _writer.BaseStream.SetLength(0); + } } ///