-
Notifications
You must be signed in to change notification settings - Fork 450
feat: QoL: Byte Packing of integers [MTT-4924] #2276
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
Conversation
…on it broke some tests...), revert some debug logs
…LogMessage in tools test, because packing makes the actual expected message size non-trivial to calculate.
com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs
Outdated
Show resolved
Hide resolved
@@ -356,11 +342,17 @@ public static unsafe void ReadValueBitPacked(FastBufferReader reader, out ushort | |||
*ptr = *data; | |||
*(ptr + 1) = *(data + 1); | |||
break; | |||
case 3: | |||
// First byte contains no data, it's just a marker. The data is in the remaining two bytes. | |||
*ptr = *(data + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above *(ptr++) = *(data++);
twice, with extra ++data
at beginning ? maybe it's just style.
@@ -500,58 +500,23 @@ public static unsafe void ReadValueBitPacked(FastBufferReader reader, out ulong | |||
*(ptr + 6) = *(data + 6); | |||
*(ptr + 7) = *(data + 7); | |||
break; | |||
case 9: | |||
// First byte contains no data, it's just a marker. The data is in the remaining two bytes. | |||
*ptr = *(data + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a manually unrolled loop. :-) Is there any higher-level construct that will instruct to compiler to copy those 8 bytes that is easier to read and/or doesn't go so squarely against the DRY principle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC when I was writing this, I did performance comparisons between:
- UnsafeUtility.MemCpy
- A
for()
loop - And a manually unrolled loop
And the manually unrolled loop was measurably faster. Normally I'd say "don't worry about that, clarity's more important", but given that this is something we do potentially dozens to hundreds to even thousands of times per frame depending how many NetworkVariables are out there and how much they're changing, this is one area of code where I'm personally inclined to err on the side of optimization over clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
manually unrolled loop was measurably faster
even in standalone release builds too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was testing in both mono and IL2CPP in standalone builds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
approved with some minor stylistic nits
// Note: This value can't be packed because we don't know how large it will be in advance | ||
// we reserve space for it, then write the data, then come back and fill in the space | ||
// to pack here, we'd have to write data to a temporary buffer and copy it in - which | ||
// isn't worth possibly saving one byte IFF the data is less than 63 bytes long... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit-pick or curious what it means:
IFF? abbreviation or misspelled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, sorry, using formal logical language... IFF is shorthand for "If And Only If"
https://en.wikipedia.org/wiki/If_and_only_if
I'll change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -692,6 +699,18 @@ protected void DestroySceneNetworkObjects() | |||
} | |||
} | |||
|
|||
/// <summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😻
Awesome! Nice addition!
@@ -842,7 +841,7 @@ private bool RotationMatchesValue(Vector3 rotationEulerToMatch) | |||
} | |||
if (!nonauthorityIsEqual) | |||
{ | |||
VerboseDebug($"NonAuthority position {nonAuthorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); | |||
VerboseDebug($"NonAuthority rotation {nonAuthorityRotationEuler} != rotation to match: {rotationEulerToMatch}!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice copy paste debug output catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than the following:
- The 8 to 5 bytes for the SceneObject structure
- This could possibly need to be updated as a minor version feat! due to the deprecated public const values.
The rest looks good to me, will review again once you come to a conclusion about the above two items...I will do a second pass over it all once more.
com.unity.netcode.gameobjects/Tests/Runtime/Metrics/ServerLogsMetricTests.cs
Show resolved
Hide resolved
com.unity.netcode.gameobjects/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs
Show resolved
Hide resolved
com.unity.netcode.gameobjects/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs
Show resolved
Hide resolved
private const int k_MessageHeaderSize = 2; | ||
private static readonly int k_ServerLogSentMessageOverhead = 2 + k_MessageHeaderSize; | ||
private static readonly int k_ServerLogReceivedMessageOverhead = 2; | ||
// Header is dynamically sized due to packing, will be 3 bytes for all test messages. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: This increased from 2 bytes to 3 bytes here because BytePacker.WriteValueBitPacked was changed to reserve 3 bits instead of 2 for a uint. It used to be that message length <= 63 would take 1 byte, now message length <= 31 does. The message length for a guid is consistently in between those sizes, so the message header for this test is now 3 bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this change. Looks good!
# Conflicts: # com.unity.netcode.gameobjects/CHANGELOG.md # com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Adds byte packing for most integer fields in the SDK (a few, such as hashes, are left un-packed because they're likely to be large integers and not benefit - and may possibly actually be harmed - by variable-length encoding). Integers in `RPC`s and `NetworkVariable`s are also packed properly. No functionality has been added to allow a value to be sent unpacked in `RPC`s or `NetworkVariable`s in cases where packing may be detrimental, but this can be achieved as follows: ```csharp public struct UnpackedInt32 : INetworkSerializeByMemcpy { int Value; } public NetworkVariable<UnpackedInt32> MyVariable; ```` In addition to byte packing most integer fields, this commit updates `WriteValueBitPacked()` to be able to handle the full range of its type (now using an extra byte - and thus ending up making the representation larger instead of smaller - for values at the extreme ends of the size), at the cost of using one extra bit to encode the size of the type.
Adds byte packing for most integer fields in the SDK (a few, such as hashes, are left un-packed because they're likely to be large integers and not benefit - and may possibly actually be harmed - by variable-length encoding). Integers in
RPC
s andNetworkVariable
s are also packed properly. No functionality has been added to allow a value to be sent unpacked inRPC
s orNetworkVariable
s in cases where packing may be detrimental, but this can be achieved as follows:In addition to byte packing most integer fields, this PR updates
WriteValueBitPacked()
to be able to handle the full range of its type (now using an extra byte - and thus ending up making the representation larger instead of smaller - for values at the extreme ends of the size), at the cost of using one extra bit to encode the size of the type.WriteValueBitPacked
shrinks values as follows:Because this provides better compression than
WriteValuePacked
in all cases and no longer has any size limits, the old implementation forWriteValuePacked
has been removed and all calls toWriteValuePacked
now simply forward toWriteValueBitPacked
.This scheme was chosen over the standard Variable-Length Quantity representation for two reasons:
Here are some graphs that show the comparisons (the lower the value the better the compression; the further to the right the value changed, the better the compression scales).
WriteValuePacked
here represents the old method, which has been removed in this PR.Changelog
Testing and Documentation