From 3bee524c6b6a78c04ba3146c1707cccc44c0db09 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sat, 9 Oct 2021 13:47:19 +0100 Subject: [PATCH] Avoid allocation in PackageEntry.HashFilename This is a very frequently called method during loading, and taking some care to avoid allocations improves performance noticeably. --- OpenRA.Mods.Cnc/FileFormats/CRC32.cs | 41 +++++++------------- OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs | 44 +++++++++++++--------- 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/OpenRA.Mods.Cnc/FileFormats/CRC32.cs b/OpenRA.Mods.Cnc/FileFormats/CRC32.cs index d1ae87e6c9..e32bbe22bd 100644 --- a/OpenRA.Mods.Cnc/FileFormats/CRC32.cs +++ b/OpenRA.Mods.Cnc/FileFormats/CRC32.cs @@ -9,6 +9,8 @@ */ #endregion +using System; + namespace OpenRA.Mods.Cnc.FileFormats { /// @@ -20,7 +22,7 @@ namespace OpenRA.Mods.Cnc.FileFormats /// /// The CRC32 lookup table /// - static uint[] lookUp = new uint[256] + static readonly uint[] LookUp = new uint[256] { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, @@ -89,47 +91,32 @@ namespace OpenRA.Mods.Cnc.FileFormats }; /// - /// A fast (native) CRC32 implementation that can be used on a regular byte arrays. + /// A CRC32 implementation that can be used on spans of bytes. /// /// The data from which to calculate the checksum. - /// The polynomial. + /// The polynomial to XOR with. /// /// The calculated checksum. /// - public static uint Calculate(byte[] data, uint polynomial) + public static uint Calculate(ReadOnlySpan data, uint polynomial) { var crc = polynomial; for (var i = 0; i < data.Length; i++) - crc = (crc >> 8) ^ lookUp[(crc & 0xFF) ^ data[i]]; + crc = (crc >> 8) ^ LookUp[(crc & 0xFF) ^ data[i]]; crc ^= polynomial; return crc; } - public static uint Calculate(byte[] data) - { - return Calculate(data, 0xFFFFFFFF); - } - /// - /// A fast (native) CRC32 implementation that can be used on a pinned byte array using - /// default polynomial. + /// A CRC32 implementation that can be used on spans of bytes, with default polynomial. /// - /// [in,out] If non-null, the. - /// The length of the data. - /// The polynomial to XOR with. - /// The calculated checksum. - public static unsafe uint Calculate(byte* data, uint len, uint polynomial) + /// The data from which to calculate the checksum. + /// + /// The calculated checksum. + /// + public static uint Calculate(ReadOnlySpan data) { - var crc = polynomial; - for (var i = 0; i < len; i++) - crc = (crc >> 8) ^ lookUp[(crc & 0xFF) ^ *data++]; - crc ^= polynomial; - return crc; - } - - public static unsafe uint Calculate(byte* data, uint len) - { - return Calculate(data, len, 0xFFFFFFFF); + return Calculate(data, 0xFFFFFFFF); } } } diff --git a/OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs b/OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs index 0ad140d878..9d1bf614fe 100644 --- a/OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs +++ b/OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Text; using OpenRA.Mods.Cnc.FileFormats; @@ -57,40 +58,47 @@ namespace OpenRA.Mods.Cnc.FileSystem public static uint HashFilename(string name, PackageHashType type) { + var padding = name.Length % 4 != 0 ? 4 - name.Length % 4 : 0; + var paddedLength = name.Length + padding; + + // Avoid stack overflows by only allocating small buffers on the stack, and larger ones on the heap. + // 64 chars covers most real filenames. + var upperPaddedName = paddedLength < 64 ? stackalloc char[paddedLength] : new char[paddedLength]; + name.AsSpan().ToUpperInvariant(upperPaddedName); + switch (type) { case PackageHashType.Classic: { - name = name.ToUpperInvariant(); - if (name.Length % 4 != 0) - name = name.PadRight(name.Length + (4 - name.Length % 4), '\0'); + for (var p = 0; p < padding; p++) + upperPaddedName[paddedLength - 1 - p] = '\0'; + var asciiBytes = paddedLength < 64 ? stackalloc byte[paddedLength] : new byte[paddedLength]; + Encoding.ASCII.GetBytes(upperPaddedName, asciiBytes); + + var data = MemoryMarshal.Cast(asciiBytes); var result = 0u; - var data = Encoding.ASCII.GetBytes(name); - var i = 0; - while (i < data.Length) - { - var next = (uint)(data[i++] | data[i++] << 8 | data[i++] << 16 | data[i++] << 24); + foreach (var next in data) result = ((result << 1) | (result >> 31)) + next; - } return result; } case PackageHashType.CRC32: { - name = name.ToUpperInvariant(); - var l = name.Length; - var a = l >> 2; - if ((l & 3) != 0) + var length = name.Length; + var lengthRoundedDownToFour = length / 4 * 4; + if (length != lengthRoundedDownToFour) { - name += (char)(l - (a << 2)); - var i = 3 - (l & 3); - while (i-- != 0) - name += name[a << 2]; + upperPaddedName[length] = (char)(length - lengthRoundedDownToFour); + for (var p = 1; p < padding; p++) + upperPaddedName[length + p] = upperPaddedName[lengthRoundedDownToFour]; } - return CRC32.Calculate(Encoding.ASCII.GetBytes(name)); + var asciiBytes = paddedLength < 64 ? stackalloc byte[paddedLength] : new byte[paddedLength]; + Encoding.ASCII.GetBytes(upperPaddedName, asciiBytes); + + return CRC32.Calculate(asciiBytes); } default: throw new NotImplementedException($"Unknown hash type `{type}`");