Skip to content

Commit

Permalink
Use spans to improve performance in StreamExts.
Browse files Browse the repository at this point in the history
Also avoid ReadBytes calls that allocate a buffer by either updating the stream position (if not interested in the bytes), by reusing an input buffer (if interested in the bytes), or using a stackalloc buffer to avoid the allocation (for small reads).
  • Loading branch information
RoosterDragon committed Nov 4, 2023
1 parent 5157bc3 commit 80a5874
Show file tree
Hide file tree
Showing 17 changed files with 133 additions and 72 deletions.
6 changes: 3 additions & 3 deletions OpenRA.Game/CryptoUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ static int ReadTLVLength(Stream s)
if (length < 0x80)
return length;

var data = new byte[4];
s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4));
return BitConverter.ToInt32(data.ToArray(), 0);
Span<byte> data = stackalloc byte[4];
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]);
return BitConverter.ToInt32(data);
}

static int TripletFullLength(int dataLength)
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Game/FileFormats/Png.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public Png(Stream s)
while (true)
{
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
var type = s.ReadASCII(4);
var content = s.ReadBytes(length);
/*var crc = */s.ReadInt32();

Expand Down
4 changes: 2 additions & 2 deletions OpenRA.Game/FileFormats/ReplayMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public ReplayMetadata(GameInformation info)
throw new NotSupportedException($"Metadata version {version} is not supported");

// Read game info (max 100K limit as a safeguard against corrupted files)
var data = fs.ReadString(Encoding.UTF8, 1024 * 100);
var data = fs.ReadLengthPrefixedString(Encoding.UTF8, 1024 * 100);
GameInfo = GameInformation.Deserialize(data);
}

Expand All @@ -62,7 +62,7 @@ public void Write(BinaryWriter writer)
{
// Write lobby info data
writer.Flush();
dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize());
dataLength += writer.BaseStream.WriteLengthPrefixedString(Encoding.UTF8, GameInfo.Serialize());
}

// Write total length & end marker
Expand Down
5 changes: 1 addition & 4 deletions OpenRA.Game/FileSystem/ZipFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,7 @@ public ReadWriteZipFile(string filename, bool create = false)

void Commit()
{
var pos = pkgStream.Position;
pkgStream.Position = 0;
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
pkgStream.Position = pos;
File.WriteAllBytes(Name, pkgStream.ToArray());
}

public void Update(string filename, byte[] contents)
Expand Down
16 changes: 8 additions & 8 deletions OpenRA.Game/Network/GameSave.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,18 @@ public GameSave(string filepath)
LastSyncFrame = rs.ReadInt32();
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);

var globalSettings = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength));
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);

var slots = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength));
Slots = new Dictionary<string, Session.Slot>();
foreach (var s in slots)
{
var slot = Session.Slot.Deserialize(s.Value);
Slots.Add(slot.PlayerReference, slot);
}

var slotClients = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength));
SlotClients = new Dictionary<string, SlotClient>();
foreach (var s in slotClients)
{
Expand All @@ -144,7 +144,7 @@ public GameSave(string filepath)
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
throw new InvalidDataException("Invalid orasav file");

var traitData = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
var traitData = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength));
foreach (var td in traitData)
TraitData.Add(Exts.ParseInt32Invariant(td.Key), td.Value);

Expand Down Expand Up @@ -294,25 +294,25 @@ public void Save(string path)
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);

var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, globalSettingsNodes.WriteToString());

var slotNodes = Slots
.Select(s => s.Value.Serialize())
.ToList();
file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, slotNodes.WriteToString());

var slotClientNodes = SlotClients
.Select(s => s.Value.Serialize(s.Key))
.ToList();
file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());

var traitDataOffset = file.Length;
file.Write(TraitDataMarker);

var traitDataNodes = TraitData
.Select(kv => new MiniYamlNode(kv.Key.ToStringInvariant(), kv.Value))
.ToList();
file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, traitDataNodes.WriteToString());

file.Write((int)ordersStream.Length);
file.Write((int)traitDataOffset);
Expand Down
4 changes: 2 additions & 2 deletions OpenRA.Game/Network/OrderIO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState)
ms.Write(data.Frame);
ms.WriteByte((byte)OrderType.SyncHash);
ms.Write(data.SyncHash);
ms.WriteArray(BitConverter.GetBytes(data.DefeatState));
ms.Write(data.DefeatState);
return ms.GetBuffer();
}

Expand All @@ -95,7 +95,7 @@ public static byte[] SerializePingResponse(long timestamp, byte queueLength)
var ms = new MemoryStream(14);
ms.Write(0);
ms.WriteByte((byte)OrderType.Ping);
ms.WriteArray(BitConverter.GetBytes(timestamp));
ms.Write(timestamp);
ms.WriteByte(queueLength);
return ms.GetBuffer();
}
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Game/Server/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ static byte[] CreatePingFrame()
ms.Write(0);
ms.Write(0);
ms.WriteByte((byte)OrderType.Ping);
ms.WriteArray(BitConverter.GetBytes(Game.RunTime));
ms.Write(Game.RunTime);
return ms.GetBuffer();
}

Expand Down
95 changes: 73 additions & 22 deletions OpenRA.Game/StreamExts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ namespace OpenRA
{
public static class StreamExts
{
public static void ReadBytes(this Stream s, Span<byte> dest)
{
while (dest.Length > 0)
{
var bytesRead = s.Read(dest);
if (bytesRead == 0)
throw new EndOfStreamException();
dest = dest[bytesRead..];
}
}

public static byte[] ReadBytes(this Stream s, int count)
{
if (count < 0)
Expand All @@ -35,8 +46,8 @@ public static void ReadBytes(this Stream s, byte[] buffer, int offset, int count
throw new ArgumentOutOfRangeException(nameof(count), "Non-negative number required.");
while (count > 0)
{
int bytesRead;
if ((bytesRead = s.Read(buffer, offset, count)) == 0)
var bytesRead = s.Read(buffer, offset, count);
if (bytesRead == 0)
throw new EndOfStreamException();
offset += bytesRead;
count -= bytesRead;
Expand All @@ -62,47 +73,79 @@ public static byte ReadUInt8(this Stream s)

public static ushort ReadUInt16(this Stream s)
{
return (ushort)(s.ReadUInt8() | s.ReadUInt8() << 8);
Span<byte> buffer = stackalloc byte[2];
s.ReadBytes(buffer);
return BitConverter.ToUInt16(buffer);
}

public static short ReadInt16(this Stream s)
{
return (short)(s.ReadUInt8() | s.ReadUInt8() << 8);
Span<byte> buffer = stackalloc byte[2];
s.ReadBytes(buffer);
return BitConverter.ToInt16(buffer);
}

public static uint ReadUInt32(this Stream s)
{
return (uint)(s.ReadUInt8() | s.ReadUInt8() << 8 | s.ReadUInt8() << 16 | s.ReadUInt8() << 24);
Span<byte> buffer = stackalloc byte[4];
s.ReadBytes(buffer);
return BitConverter.ToUInt32(buffer);
}

public static int ReadInt32(this Stream s)
{
return s.ReadUInt8() | s.ReadUInt8() << 8 | s.ReadUInt8() << 16 | s.ReadUInt8() << 24;
Span<byte> buffer = stackalloc byte[4];
s.ReadBytes(buffer);
return BitConverter.ToInt32(buffer);
}

public static void Write(this Stream s, int value)
{
s.WriteArray(BitConverter.GetBytes(value));
Span<byte> buffer = stackalloc byte[4];
BitConverter.TryWriteBytes(buffer, value);
s.Write(buffer);
}

public static void Write(this Stream s, long value)
{
Span<byte> buffer = stackalloc byte[8];
BitConverter.TryWriteBytes(buffer, value);
s.Write(buffer);
}

public static void Write(this Stream s, ulong value)
{
Span<byte> buffer = stackalloc byte[8];
BitConverter.TryWriteBytes(buffer, value);
s.Write(buffer);
}

public static void Write(this Stream s, float value)
{
s.WriteArray(BitConverter.GetBytes(value));
Span<byte> buffer = stackalloc byte[4];
BitConverter.TryWriteBytes(buffer, value);
s.Write(buffer);
}

public static float ReadFloat(this Stream s)
public static float ReadSingle(this Stream s)
{
return BitConverter.ToSingle(s.ReadBytes(4), 0);
Span<byte> buffer = stackalloc byte[4];
s.ReadBytes(buffer);
return BitConverter.ToSingle(buffer);
}

public static double ReadDouble(this Stream s)
{
return BitConverter.ToDouble(s.ReadBytes(8), 0);
Span<byte> buffer = stackalloc byte[8];
s.ReadBytes(buffer);
return BitConverter.ToDouble(buffer);
}

public static string ReadASCII(this Stream s, int length)
{
return new string(Encoding.ASCII.GetChars(s.ReadBytes(length)));
Span<byte> buffer = length < 128 ? stackalloc byte[length] : new byte[length];
s.ReadBytes(buffer);
return Encoding.ASCII.GetString(buffer);
}

public static string ReadASCIIZ(this Stream s)
Expand All @@ -111,7 +154,12 @@ public static string ReadASCIIZ(this Stream s)
byte b;
while ((b = s.ReadUInt8()) != 0)
bytes.Add(b);
return new string(Encoding.ASCII.GetChars(bytes.ToArray()));

#if NET5_0_OR_GREATER
return Encoding.ASCII.GetString(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(bytes));
#else
return Encoding.ASCII.GetString(bytes.ToArray());
#endif
}

public static string ReadAllText(this Stream s)
Expand All @@ -137,11 +185,9 @@ public static byte[] ReadAllBytes(this Stream s)
}
}

// Note: renamed from Write() to avoid being aliased by
// System.IO.Stream.Write(System.ReadOnlySpan) (which is not implemented in Mono)
public static void WriteArray(this Stream s, byte[] data)
{
s.Write(data, 0, data.Length);
s.Write(data);
}

public static IEnumerable<string> ReadAllLines(this Stream s)
Expand Down Expand Up @@ -210,19 +256,24 @@ public static IEnumerable<ReadOnlyMemory<char>> ReadAllLinesAsMemory(this Stream
}
}

// The string is assumed to be length-prefixed, as written by WriteString()
public static string ReadString(this Stream s, Encoding encoding, int maxLength)
/// <summary>
/// The string is assumed to be length-prefixed, as written by <see cref="WriteLengthPrefixedString"/>.
/// </summary>
public static string ReadLengthPrefixedString(this Stream s, Encoding encoding, int maxLength)
{
var length = s.ReadInt32();
if (length > maxLength)
throw new InvalidOperationException($"The length of the string ({length}) is longer than the maximum allowed ({maxLength}).");

return encoding.GetString(s.ReadBytes(length));
Span<byte> buffer = length < 128 ? stackalloc byte[length] : new byte[length];
s.ReadBytes(buffer);
return encoding.GetString(buffer);
}

// Writes a length-prefixed string using the specified encoding and returns
// the number of bytes written.
public static int WriteString(this Stream s, Encoding encoding, string text)
/// <summary>
/// Writes a length-prefixed string using the specified encoding and returns the number of bytes written.
/// </summary>
public static int WriteLengthPrefixedString(this Stream s, Encoding encoding, string text)
{
byte[] bytes;

Expand Down
15 changes: 13 additions & 2 deletions OpenRA.Mods.Cnc/FileFormats/AudReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ sealed class WestwoodCompressedAudStream : ReadOnlyAdapterStream
{
readonly int outputSize;
int dataSize;
byte[] inputBuffer;
byte[] outputBuffer;

public WestwoodCompressedAudStream(Stream stream, int outputSize, int dataSize)
: base(stream)
Expand All @@ -167,8 +169,10 @@ protected override bool BufferData(Stream baseStream, Queue<byte> data)

var chunk = AudChunk.Read(baseStream);

var input = baseStream.ReadBytes(chunk.CompressedSize);
var output = new byte[chunk.OutputSize];
var input = EnsureArraySize(ref inputBuffer, chunk.CompressedSize);
var output = EnsureArraySize(ref outputBuffer, chunk.OutputSize);

baseStream.ReadBytes(input);
WestwoodCompressedReader.DecodeWestwoodCompressedSample(input, output);

foreach (var b in output)
Expand All @@ -178,6 +182,13 @@ protected override bool BufferData(Stream baseStream, Queue<byte> data)

return dataSize <= 0;
}

static Span<byte> EnsureArraySize(ref byte[] array, int desiredSize)
{
if (array == null || array.Length < desiredSize)
array = new byte[desiredSize];
return array.AsSpan(..desiredSize);
}
}
}
}
2 changes: 1 addition & 1 deletion OpenRA.Mods.Cnc/FileFormats/HvaReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public HvaReader(Stream s, string fileName)
Transforms[c + 15] = 1;

for (var k = 0; k < 12; k++)
Transforms[c + ids[k]] = s.ReadFloat();
Transforms[c + ids[k]] = s.ReadSingle();

Array.Copy(Transforms, 16 * (LimbCount * j + i), testMatrix, 0, 16);
if (Util.MatrixInverse(testMatrix) == null)
Expand Down
9 changes: 4 additions & 5 deletions OpenRA.Mods.Cnc/FileFormats/VqaVideo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,15 @@ void CollectAudioData()
rawAudio = stream.ReadBytes((int)length / 2);
audio2.WriteArray(rawAudio);
if (length % 2 != 0)
stream.ReadBytes(2);
stream.Position += 2;
}

compressed = type == "SND2";
break;
default:
if (length + stream.Position > stream.Length)
throw new NotSupportedException($"Vqa uses unknown Subtype: {type}");
stream.ReadBytes((int)length);
stream.Position += length;
break;
}

Expand Down Expand Up @@ -308,7 +308,7 @@ void LoadFrame()
break;
default:
// Don't parse sound here.
stream.ReadBytes((int)length);
stream.Position += length;
break;
}

Expand Down Expand Up @@ -382,8 +382,7 @@ 0rrrrrgg gggbbbbb
// frame-modifier chunk
case "CBP0":
case "CBPZ":
var bytes = s.ReadBytes(subchunkLength);
bytes.CopyTo(cbp, chunkBufferOffset);
s.ReadBytes(cbp, chunkBufferOffset, subchunkLength);
chunkBufferOffset += subchunkLength;
currentChunkBuffer++;
cbpIsCompressed = type == "CBPZ";
Expand Down

0 comments on commit 80a5874

Please sign in to comment.