Skip to content

Commit 8c7e111

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 8c7e111

File tree

2 files changed

+80
-21
lines changed

2 files changed

+80
-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: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Buffers;
23
using System.Buffers.Binary;
34
using System.Diagnostics;
45
using System.Globalization;
@@ -59,28 +60,57 @@ public bool IsEndOfData
5960
}
6061
}
6162

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

67-
buffer.CopyTo(sharedBuffer);
78+
private delegate void WriteAction<in TArg>(Span<byte> span, TArg arg)
79+
#if NET9_0_OR_GREATER
80+
where TArg : allows ref struct
81+
#endif
82+
;
6883

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

71-
System.Buffers.ArrayPool<byte>.Shared.Return(sharedBuffer);
91+
if (Capacity < endPosition)
92+
{
93+
var newCapacity = Math.Max(endPosition, Math.Min(2 * (uint)Capacity, Array.MaxLength));
94+
Capacity = checked((int)newCapacity);
95+
}
96+
97+
if (endPosition > Length)
98+
{
99+
SetLength(endPosition);
100+
}
101+
102+
writeAction(GetRemainingBuffer().AsSpan(0, numBytesToWrite), arg);
103+
104+
Position = endPosition;
72105
}
73-
#endif
74106

75107
/// <summary>
76108
/// Writes an <see cref="uint"/> to the SSH data stream.
77109
/// </summary>
78110
/// <param name="value"><see cref="uint"/> data to write.</param>
79111
public void Write(uint value)
80112
{
81-
Span<byte> bytes = stackalloc byte[4];
82-
BinaryPrimitives.WriteUInt32BigEndian(bytes, value);
83-
Write(bytes);
113+
Write(value, 4, static (span, value) => BinaryPrimitives.WriteUInt32BigEndian(span, value));
84114
}
85115

86116
/// <summary>
@@ -89,9 +119,7 @@ public void Write(uint value)
89119
/// <param name="value"><see cref="ulong"/> data to write.</param>
90120
public void Write(ulong value)
91121
{
92-
Span<byte> bytes = stackalloc byte[8];
93-
BinaryPrimitives.WriteUInt64BigEndian(bytes, value);
94-
Write(bytes);
122+
Write(value, 8, static (span, value) => BinaryPrimitives.WriteUInt64BigEndian(span, value));
95123
}
96124

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

105145
WriteBinary(bytes, 0, bytes.Length);
146+
#endif
106147
}
107148

108149
/// <summary>
@@ -129,16 +170,26 @@ public void Write(string s, Encoding encoding)
129170
ArgumentNullException.ThrowIfNull(s);
130171
ArgumentNullException.ThrowIfNull(encoding);
131172

173+
var byteCount = encoding.GetByteCount(s);
132174
#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);
175+
Write((s, byteCount, encoding), 4 + byteCount, static (span, args) =>
176+
{
177+
BinaryPrimitives.WriteUInt32BigEndian(span, (uint)args.byteCount);
178+
179+
var bytesWritten = args.encoding.GetBytes(args.s, span.Slice(4));
180+
181+
Debug.Assert(bytesWritten == span.Length - 4);
182+
});
139183
#else
140-
var bytes = encoding.GetBytes(s);
141-
WriteBinary(bytes, 0, bytes.Length);
184+
var rentedBuffer = ArrayPool<byte>.Shared.Rent(byteCount);
185+
186+
var bytesWritten = encoding.GetBytes(s, 0, s.Length, rentedBuffer, 0);
187+
188+
Debug.Assert(bytesWritten == byteCount);
189+
190+
WriteBinary(rentedBuffer, 0, bytesWritten);
191+
192+
ArrayPool<byte>.Shared.Return(rentedBuffer);
142193
#endif
143194
}
144195

0 commit comments

Comments
 (0)