Skip to content

Commit cbbbdde

Browse files
authored
Write into the underlying buffer in SshDataStream (#1739)
Several commonly used Write methods on SshDataStream end up calling Write(ReadOnlySpan) on the base MemoryStream. But since SshDataStream is a derived type, that method just rents a buffer and hands it to Write(byte[], int, int), which defeats any stackalloc'ing or renting that SshDataStream does itself. Instead, with a bit extra accounting we can just write directly into the underlying buffer.
1 parent bafd867 commit cbbbdde

File tree

2 files changed

+79
-21
lines changed

2 files changed

+79
-21
lines changed

src/Renci.SshNet/Common/Extensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,14 @@ async Task<T> WaitCore()
418418
}
419419
}
420420

421+
extension(Array)
422+
{
423+
internal static int MaxLength
424+
{
425+
get { return 0X7FFFFFC7; }
426+
}
427+
}
428+
421429
extension(Task t)
422430
{
423431
internal bool IsCompletedSuccessfully

src/Renci.SshNet/Common/SshDataStream.cs

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,57 @@ public bool IsEndOfData
5959
}
6060
}
6161

62-
#if !NET
63-
private void Write(ReadOnlySpan<byte> buffer)
62+
// Because this type derives from MemoryStream, the base Write(ReadOnlySpan) chooses
63+
// to rent an array, copy the data in and delegate to Write(byte[], int, int) for
64+
// backwards compatibility.
65+
// With a bit of extra ceremony, we can instead allow the various Write methods here
66+
// to write directly into the underlying buffer without the need for any intermediate
67+
// arrays (rented or otherwise).
68+
69+
#if NET9_0_OR_GREATER
70+
/// <inheritdoc/>
71+
public override void Write(ReadOnlySpan<byte> buffer)
6472
{
65-
var sharedBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
73+
Write(buffer, buffer.Length, static (span, buffer) => buffer.CopyTo(span));
74+
}
75+
#endif
76+
77+
private delegate void WriteAction<in TArg>(Span<byte> span, TArg arg)
78+
#if NET9_0_OR_GREATER
79+
where TArg : allows ref struct
80+
#endif
81+
;
6682

67-
buffer.CopyTo(sharedBuffer);
83+
private void Write<TArg>(TArg arg, int numBytesToWrite, WriteAction<TArg> writeAction)
84+
#if NET9_0_OR_GREATER
85+
where TArg : allows ref struct
86+
#endif
87+
{
88+
var endPosition = Position + numBytesToWrite;
6889

69-
Write(sharedBuffer, 0, buffer.Length);
90+
if (Capacity < endPosition)
91+
{
92+
var newCapacity = Math.Max(endPosition, Math.Min(2 * (uint)Capacity, Array.MaxLength));
93+
Capacity = checked((int)newCapacity);
94+
}
7095

71-
System.Buffers.ArrayPool<byte>.Shared.Return(sharedBuffer);
96+
if (endPosition > Length)
97+
{
98+
SetLength(endPosition);
99+
}
100+
101+
writeAction(GetRemainingBuffer().AsSpan(0, numBytesToWrite), arg);
102+
103+
Position = endPosition;
72104
}
73-
#endif
74105

75106
/// <summary>
76107
/// Writes an <see cref="uint"/> to the SSH data stream.
77108
/// </summary>
78109
/// <param name="value"><see cref="uint"/> data to write.</param>
79110
public void Write(uint value)
80111
{
81-
Span<byte> bytes = stackalloc byte[4];
82-
BinaryPrimitives.WriteUInt32BigEndian(bytes, value);
83-
Write(bytes);
112+
Write(value, 4, static (span, value) => BinaryPrimitives.WriteUInt32BigEndian(span, value));
84113
}
85114

86115
/// <summary>
@@ -89,9 +118,7 @@ public void Write(uint value)
89118
/// <param name="value"><see cref="ulong"/> data to write.</param>
90119
public void Write(ulong value)
91120
{
92-
Span<byte> bytes = stackalloc byte[8];
93-
BinaryPrimitives.WriteUInt64BigEndian(bytes, value);
94-
Write(bytes);
121+
Write(value, 8, static (span, value) => BinaryPrimitives.WriteUInt64BigEndian(span, value));
95122
}
96123

97124
/// <summary>
@@ -100,9 +127,22 @@ public void Write(ulong value)
100127
/// <param name="data">The <see cref="BigInteger" /> to write.</param>
101128
public void Write(BigInteger data)
102129
{
130+
#if NET
131+
var byteCount = data.GetByteCount();
132+
133+
Write((data, byteCount), 4 + byteCount, static (span, args) =>
134+
{
135+
BinaryPrimitives.WriteUInt32BigEndian(span, (uint)args.byteCount);
136+
137+
var success = args.data.TryWriteBytes(span.Slice(4), out var bytesWritten, isBigEndian: true);
138+
139+
Debug.Assert(success && bytesWritten == span.Length - 4);
140+
});
141+
#else
103142
var bytes = data.ToByteArray(isBigEndian: true);
104143

105144
WriteBinary(bytes, 0, bytes.Length);
145+
#endif
106146
}
107147

108148
/// <summary>
@@ -129,16 +169,26 @@ public void Write(string s, Encoding encoding)
129169
ArgumentNullException.ThrowIfNull(s);
130170
ArgumentNullException.ThrowIfNull(encoding);
131171

172+
var byteCount = encoding.GetByteCount(s);
132173
#if NET
133-
ReadOnlySpan<char> value = s;
134-
var count = encoding.GetByteCount(value);
135-
var bytes = count <= 256 ? stackalloc byte[count] : new byte[count];
136-
encoding.GetBytes(value, bytes);
137-
Write((uint)count);
138-
Write(bytes);
174+
Write((s, byteCount, encoding), 4 + byteCount, static (span, args) =>
175+
{
176+
BinaryPrimitives.WriteUInt32BigEndian(span, (uint)args.byteCount);
177+
178+
var bytesWritten = args.encoding.GetBytes(args.s, span.Slice(4));
179+
180+
Debug.Assert(bytesWritten == span.Length - 4);
181+
});
139182
#else
140-
var bytes = encoding.GetBytes(s);
141-
WriteBinary(bytes, 0, bytes.Length);
183+
var rentedBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(byteCount);
184+
185+
var bytesWritten = encoding.GetBytes(s, 0, s.Length, rentedBuffer, 0);
186+
187+
Debug.Assert(bytesWritten == byteCount);
188+
189+
WriteBinary(rentedBuffer, 0, bytesWritten);
190+
191+
System.Buffers.ArrayPool<byte>.Shared.Return(rentedBuffer);
142192
#endif
143193
}
144194

0 commit comments

Comments
 (0)