Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Extensions.SequenceEqualFast, and use it to speed up TextureStorage #69

Merged
merged 6 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions doc/KKAPI.Utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Static Methods
| `Transform` | GetTopmostParent(this `Transform` src) | Get the topmost parent of Transform that this this Component is attached to. |
| `Boolean` | IsDestroyed(this `Object` obj) | Return true if the object is a "fake" null (i.e. it was destroyed). |
| `void` | MarkXuaIgnored(this `Component` target) | Mark GameObject of this Component as ignored by AutoTranslator. Prevents AutoTranslator from trying to translate custom UI elements. |
| `Boolean` | SequenceEqualFast(this `byte[]` a, `Byte[]` b) | This method compares two byte arrays for equality, returning true if they are identical and false otherwise. This method is optimized for high performance. |
| `Boolean` | SetFieldValue(this `Object` self, `String` name, `Object` value) | Set value of a field through reflection |
| `Boolean` | SetPropertyValue(this `Object` self, `String` name, `Object` value) | Set value of a property through reflection |
| `ReadOnlyDictionary<TKey, TValue>` | ToReadOnlyDictionary(this `IDictionary<TKey, TValue>` original) | Wrap this dictionary in a read-only wrapper that will prevent any changes to it. Warning: Any reference types inside the dictionary can still be modified. |
Expand Down Expand Up @@ -434,5 +435,3 @@ Static Methods
| Type | Name | Summary |
| --- | --- | --- |
| `Int32` | LogicalCompare(`String` x, `String` y) | Compare two strings with rules used by Windows Explorer to logically sort files. |


121 changes: 121 additions & 0 deletions src/Shared.Core/Utilities/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,127 @@ internal static void SafeInvokeWithLogging<T>(this T handler, Action<T> invokeCa
{
KoikatuAPI.Logger.LogError("Unexpected crash when running events, some events might have been skipped! Reason: " + e);
}
}

/// <summary>
/// Compares two byte arrays for equality in a high-performance manner using unsafe code.
/// </summary>
/// <param name="a">The first byte array to compare.</param>
/// <param name="b">The second byte array to compare.</param>
/// <returns>True if the byte arrays are equal, false otherwise.</returns>
static public bool SequenceEqualFast(this byte[] a, byte[] b)
ManlyMarco marked this conversation as resolved.
Show resolved Hide resolved
{
// Check if both references are the same, if so, return true.
if (System.Object.ReferenceEquals(a, b))
return true;

if (a == null || b == null)
return false;

int bytes = a.Length;

if (bytes != b.Length)
return false;

if (bytes <= 0)
return true;

unsafe
{
// Fix the memory locations of the arrays to prevent the garbage collector from moving them.
fixed (byte* pA = &a[0])
fixed (byte* pB = &b[0])
{
int offset = 0;

// If both pointers are 8-byte aligned, use 64-bit comparison.
if (((int)pA & 7) == 0 && ((int)pB & 7) == 0 && bytes >= 32)
{
offset = bytes & ~31; // Round down to the nearest multiple of 32.

byte* pA_ = pA;
byte* pB_ = pB;
byte* pALast = pA + offset;

do
{
if (*(ulong*)pA_ != *(ulong*)pB_)
goto NotEquals;

pA_ += 8;
pB_ += 8;

if (*(ulong*)pA_ != *(ulong*)pB_)
goto NotEquals;

pA_ += 8;
pB_ += 8;

if (*(ulong*)pA_ != *(ulong*)pB_)
goto NotEquals;

pA_ += 8;
pB_ += 8;

if (*(ulong*)pA_ != *(ulong*)pB_)
goto NotEquals;

pA_ += 8;
pB_ += 8;
}
while (pA_ != pALast);
}
// If both pointers are 4-byte aligned, use 32-bit comparison.
else if (((int)pA & 3) == 0 && ((int)pB & 3) == 0 && bytes >= 16)
{
offset = bytes & ~15; // Round down to the nearest multiple of 16.

byte* pA_ = pA;
byte* pB_ = pB;
byte* pALast = pA + offset;

do
{
if (*(uint*)pA_ != *(uint*)pB_)
goto NotEquals;

pA_ += 4;
pB_ += 4;

if (*(uint*)pA_ != *(uint*)pB_)
goto NotEquals;

pA_ += 4;
pB_ += 4;

if (*(uint*)pA_ != *(uint*)pB_)
goto NotEquals;

pA_ += 4;
pB_ += 4;

if (*(uint*)pA_ != *(uint*)pB_)
goto NotEquals;

pA_ += 4;
pB_ += 4;
}
while (pA_ != pALast);
}

// Compare remaining bytes one by one.
for (int i = offset; i < bytes; ++i)
if (pA[i] != pB[i])
goto NotEquals;
}
}

return true;

NotEquals:
// Return false indicating arrays are not equal.
// Note: Using a return statement in the loop can potentially degrade performance due to the generated binary code,
return false;
}
}
}
2 changes: 1 addition & 1 deletion src/Shared.Core/Utilities/TextureStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public int StoreTexture(byte[] tex)
if (tex == null) throw new ArgumentNullException(nameof(tex));
lock (_data)
{
var existing = _data.FirstOrDefault(x => x.Value != null && x.Value.Data.SequenceEqual(tex));
var existing = _data.FirstOrDefault(x => x.Value != null && x.Value.Data.SequenceEqualFast(tex));
if (existing.Value != null)
{
Console.WriteLine("StoreTexture - Texture already exists, reusing it");
Expand Down