Skip to content

Commit a1bcb9c

Browse files
committed
Write into the underlying buffer in SshDataStream
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 4e02502 commit a1bcb9c

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
@@ -417,6 +417,14 @@ async Task<T> WaitCore()
417417
return await completedTask.ConfigureAwait(false);
418418
}
419419
}
420+
421+
extension(Array)
422+
{
423+
internal static int MaxLength
424+
{
425+
get { return 0X7FFFFFC7; }
426+
}
427+
}
420428
#endif
421429
}
422430
}

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)