Skip to content

Commit

Permalink
Update WzBinaryReader - further speed improvements for large array wi…
Browse files Browse the repository at this point in the history
…th ArrayPool

Small Arrays (256 bytes):
-Stackalloc and unsafe stackalloc are fastest (~117-119ns)
-Regular byte[] arrays slightly slower (~129ns)
-ArrayPool slightly slower still (~140ns)
-Memory allocation: Only byte[] allocates heap memory (280B)

Medium Arrays (1KB):
-All methods perform similarly (~460-508ns)
-Stackalloc and ArrayPool slightly faster than byte[]
-Memory allocation: Only byte[] allocates heap memory (1048B)

Large Arrays (1MB):
-ArrayPool significantly outperforms byte[] (~465μs vs ~843μs)
-byte[] causes significant GC pressure with Gen0/1/2 collections
-Memory allocation: byte[] allocates ~1MB, ArrayPool minimal allocation

Very Large Arrays (10MB):
-ArrayPool maintains its advantage (~4.5ms vs ~6.3ms)
-Even larger difference in memory pressure
-byte[] allocates ~10MB and triggers GC collections
-ArrayPool maintains minimal allocations

prev improvement: b2ae668
  • Loading branch information
lastbattle committed Oct 26, 2024
1 parent 69370db commit 5889a4b
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 57 deletions.
4 changes: 4 additions & 0 deletions MapleLib/MapleLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<PublishAot>true</PublishAot>
<OptimizationPreference>Speed</OptimizationPreference>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<IlcPgoOptimize>true</IlcPgoOptimize>
</PropertyGroup>
<PropertyGroup>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
Expand Down
238 changes: 181 additions & 57 deletions MapleLib/WzLib/Util/WzBinaryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
using System.Text;
using MapleLib.MapleCryptoLib;
using MapleLib.PacketLib;
using System.Buffers;
using System.Runtime.InteropServices;

namespace MapleLib.WzLib.Util
{
public class WzBinaryReader : BinaryReader
public sealed class WzBinaryReader : BinaryReader
{
#region Properties
/// <summary>
Expand All @@ -43,11 +45,15 @@ public class WzBinaryReader : BinaryReader
/// </summary>
private const int STACKALLOC_SIZE_LIMIT_L1 = 10 * 1024; // optimal size is half of CPU's L1 cache.

public WzMutableKey WzKey { get; set; }
public WzMutableKey WzKey { get; init; }
public uint Hash { get; set; }
public WzHeader Header { get; set; }

private readonly long startOffset; // the offset to

private readonly ArrayPool<byte> s_bytePool = ArrayPool<byte>.Shared;
private readonly ArrayPool<char> s_charPool = ArrayPool<char>.Shared;

#endregion

#region Constructors
Expand Down Expand Up @@ -134,18 +140,34 @@ public override string ReadString()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private string DecodeUnicode(int length)
{
Span<char> chars = length <= STACKALLOC_SIZE_LIMIT_L1 ? stackalloc char[length] : new char[length];
ushort mask = 0xAAAA;
char[]? pooledArray = null;

Check warning on line 143 in MapleLib/WzLib/Util/WzBinaryReader.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
try
{
Span<char> chars = length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc char[length]
: (pooledArray = s_charPool.Rent(length)).AsSpan(0, length);

ushort mask = 0xAAAA;
ref char charsRef = ref MemoryMarshal.GetReference(chars);

for (int i = 0; i < length; i++)
for (int i = 0; i < length; i++)
{
ushort encryptedChar = ReadUInt16();
encryptedChar ^= mask;
encryptedChar ^= (ushort)((WzKey[(i * 2 + 1)] << 8) + WzKey[(i * 2)]);
Unsafe.Add(ref charsRef, i) = (char)encryptedChar;
mask++;
}

return new string(chars);
}
finally
{
ushort encryptedChar = ReadUInt16();
encryptedChar ^= mask;
encryptedChar ^= (ushort)((WzKey[(i * 2 + 1)] << 8) + WzKey[(i * 2)]);
chars[i] = (char)encryptedChar;
mask++;
if (pooledArray != null)
{
s_charPool.Return(pooledArray);
}
}
return new string(chars);
}

/// <summary>
Expand All @@ -156,18 +178,34 @@ private string DecodeUnicode(int length)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private string DecodeAscii(int length)
{
Span<byte> bytes = length <= STACKALLOC_SIZE_LIMIT_L1 ? stackalloc byte[length] : new byte[length];
byte mask = 0xAA;
byte[]? pooledArray = null;
try
{
Span<byte> bytes = length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc byte[length]
: (pooledArray = s_bytePool.Rent(length)).AsSpan(0, length);

byte mask = 0xAA;
ref byte bytesRef = ref MemoryMarshal.GetReference(bytes);

for (int i = 0; i < length; i++)
for (int i = 0; i < length; i++)
{
byte encryptedChar = ReadByte();
encryptedChar ^= mask;
encryptedChar ^= (byte)WzKey[i];
Unsafe.Add(ref bytesRef, i) = encryptedChar;
mask++;
}

return Encoding.ASCII.GetString(bytes);
}
finally
{
byte encryptedChar = ReadByte();
encryptedChar ^= mask;
encryptedChar ^= (byte)WzKey[i];
bytes[i] = encryptedChar;
mask++;
if (pooledArray != null)
{
s_bytePool.Return(pooledArray);
}
}
return Encoding.ASCII.GetString(bytes);
}

/// <summary>
Expand All @@ -176,19 +214,65 @@ private string DecodeAscii(int length)
/// <param name="filePath">Length of bytes to read</param>
public string ReadString(int length)
{
return Encoding.ASCII.GetString(ReadBytes(length));
byte[]? pooledArray = null;
try
{
Span<byte> buffer = length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc byte[length]
: (pooledArray = s_bytePool.Rent(length)).AsSpan(0, length);

BaseStream.Read(buffer);
return Encoding.ASCII.GetString(buffer);
}
finally
{
if (pooledArray != null)
{
s_bytePool.Return(pooledArray);
}
}
}

public string ReadNullTerminatedString()
{
using (var memoryStream = new MemoryStream())
const int initialBufferSize = 256;
byte[]? pooledArray = null;
try
{
Span<byte> buffer = stackalloc byte[initialBufferSize];
int position = 0;
byte b;

while ((b = ReadByte()) != 0)
{
memoryStream.WriteByte(b);
if (position == buffer.Length)
{
// Need to expand to array pool
if (pooledArray == null)
{
pooledArray = s_bytePool.Rent(buffer.Length * 2);
buffer.CopyTo(pooledArray);
}
else
{
var newArray = s_bytePool.Rent(pooledArray.Length * 2);
pooledArray.AsSpan(0, position).CopyTo(newArray);
s_bytePool.Return(pooledArray);
pooledArray = newArray;
}
buffer = pooledArray;
}
buffer[position++] = b;
}

return Encoding.UTF8.GetString(buffer.Slice(0, position));
}
finally
{
if (pooledArray != null)
{
s_bytePool.Return(pooledArray);
}
return Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}

Expand Down Expand Up @@ -240,50 +324,74 @@ public long ReadOffset()
/// </summary>
/// <param name="stringToDecrypt"></param>
/// <returns></returns>
public string DecryptString(char[] stringToDecrypt)
public string DecryptString(ReadOnlySpan<char> stringToDecrypt)
{
Span<char> outputChars = stringToDecrypt.Length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc char[stringToDecrypt.Length]
: new char[stringToDecrypt.Length];

for (int i = 0; i < stringToDecrypt.Length; i++)
char[]? pooledArray = null;
try
{
outputChars[i] = (char)(stringToDecrypt[i] ^ ((char)((WzKey[i * 2 + 1] << 8) + WzKey[i * 2])));
}

return new string(outputChars);
}
Span<char> outputChars = stringToDecrypt.Length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc char[stringToDecrypt.Length]
: (pooledArray = s_charPool.Rent(stringToDecrypt.Length)).AsSpan(0, stringToDecrypt.Length);

ref char outputRef = ref MemoryMarshal.GetReference(outputChars);
ref char inputRef = ref MemoryMarshal.GetReference(stringToDecrypt);

public string DecryptNonUnicodeString(char[] stringToDecrypt)
{
Span<char> outputChars = stringToDecrypt.Length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc char[stringToDecrypt.Length]
: new char[stringToDecrypt.Length];
for (int i = 0; i < stringToDecrypt.Length; i++)
{
Unsafe.Add(ref outputRef, i) = (char)(
Unsafe.Add(ref inputRef, i) ^
((char)((WzKey[i * 2 + 1] << 8) + WzKey[i * 2]))
);
}

for (int i = 0; i < stringToDecrypt.Length; i++)
return new string(outputChars);
}
finally
{
outputChars[i] = (char)(stringToDecrypt[i] ^ WzKey[i]);
if (pooledArray != null)
{
s_charPool.Return(pooledArray);
}
}

return new string(outputChars);
}

public string ReadStringBlock(long offset)
public string DecryptNonUnicodeString(ReadOnlySpan<char> stringToDecrypt)
{
switch (ReadByte())
char[]? pooledArray = null;
try
{
case 0:
case WzImage.WzImageHeaderByte_WithoutOffset:
return ReadString();
case 1:
case WzImage.WzImageHeaderByte_WithOffset:
return ReadStringAtOffset(offset + ReadInt32());
default:
return "";
Span<char> outputChars = stringToDecrypt.Length <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc char[stringToDecrypt.Length]
: (pooledArray = s_charPool.Rent(stringToDecrypt.Length)).AsSpan(0, stringToDecrypt.Length);

ref char outputRef = ref MemoryMarshal.GetReference(outputChars);
ref char inputRef = ref MemoryMarshal.GetReference(stringToDecrypt);

for (int i = 0; i < stringToDecrypt.Length; i++)
{
Unsafe.Add(ref outputRef, i) = (char)(Unsafe.Add(ref inputRef, i) ^ WzKey[i]);
}

return new string(outputChars);
}
finally
{
if (pooledArray != null)
{
s_charPool.Return(pooledArray);
}
}
}


[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadStringBlock(long offset) => ReadByte() switch
{
0 or WzImage.WzImageHeaderByte_WithoutOffset => ReadString(),
1 or WzImage.WzImageHeaderByte_WithOffset => ReadStringAtOffset(offset + ReadInt32()),
_ => string.Empty
};

#endregion

#region Tools
Expand Down Expand Up @@ -334,10 +442,26 @@ public WzBinaryReader CreateReaderForSection(long start, int length)
public void PrintHexBytes(int numberOfBytes)
{
#if DEBUG // only debug
string hex = HexTool.ToString(ReadBytes(numberOfBytes));
Debug.WriteLine(hex);
byte[]? pooledArray = null;
try
{
Span<byte> buffer = numberOfBytes <= STACKALLOC_SIZE_LIMIT_L1
? stackalloc byte[numberOfBytes]
: (pooledArray = s_bytePool.Rent(numberOfBytes)).AsSpan(0, numberOfBytes);

BaseStream.Read(buffer);
string hex = HexTool.ToString(buffer.ToArray());
Debug.WriteLine(hex);

this.BaseStream.Position -= numberOfBytes;
BaseStream.Position -= numberOfBytes;
}
finally
{
if (pooledArray != null)
{
s_bytePool.Return(pooledArray);
}
}
#endif
}
#endregion
Expand Down

1 comment on commit 5889a4b

@lastbattle
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Please sign in to comment.