Search Unity

  1. The 2022.1 beta is now available for testing. To find out what's new, have a look at our 2022.1 beta blog post.
    Dismiss Notice

NetworkSerialize with PreCheck

Discussion in 'Netcode for GameObjects' started by firaui, Oct 4, 2021.

  1. firaui

    firaui

    Joined:
    Nov 14, 2018
    Posts:
    12
    Hello

    I successfully serialize class with NetworkSerialize like this

    Code (CSharp):
    1. public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    2. {
    3.     serializer.SerializeValue(ref user_id);
    4.     serializer.SerializeValue(ref nickname);
    5.     serializer.SerializeValue(ref currentRank);
    6.     serializer.SerializeValue(ref tier);
    7.     serializer.SerializeValue(ref icon);
    8. }
    But I want to use PreCheck, because I read it is faster to just allocate first, and just read the value without check the boundary. So far my attempt is this
    Code (CSharp):
    1. public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    2. {
    3.     int size = FastBufferWriter.GetWriteSize(user_id) + FastBufferWriter.GetWriteSize(nickname) + FastBufferWriter.GetWriteSize(currentRank) + FastBufferWriter.GetWriteSize(tier) + FastBufferWriter.GetWriteSize(icon);
    4.  
    5.     if (!serializer.PreCheck(size))
    6.     {
    7.         throw new OverflowException("Attempted to write but overflow the bounds.");
    8.     }
    9.  
    10.     serializer.SerializeValuePreChecked(ref user_id);
    11.     serializer.SerializeValuePreChecked(ref nickname);
    12.     serializer.SerializeValuePreChecked(ref currentRank);
    13.     serializer.SerializeValuePreChecked(ref tier);
    14.     serializer.SerializeValuePreChecked(ref icon);
    15. }
    I get error "Attempted to read without first calling TryBeginRead()", but I already did call the PreCheck. Anyone has any idea?
     
  2. UnityJaedyn

    UnityJaedyn

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    4
    Hi firaui,

    First off, I want to clear up a small misunderstanding about PreCheck: It doesn't necessarily save on allocations. We don't allocate one write at a time. It just reduces the number of times it has to check whether an allocation is needed. The buffer starts at 1300 bytes and grows by double only if a write would pass that boundary. The (micro-)optimization it provides is solely, in this case, that it'll incur one branch instead of five. Either way you do it, it's highly unlikely an allocation will be needed at all, and if it is, it'll only allocate once regardless (and the allocation itself is done through Allocator.TempJob, which is quite fast).

    On your specific issue, it sounds like there's a bug somewhere on the reader side that's not present on the writer side. Could you provide me with the types of those five fields so I can try to reproduce it?

    Thanks!
     
  3. UnityJaedyn

    UnityJaedyn

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    4
    Actually I think I just figured out what's going on here. I'm assuming nickname (and possibly also icon) is a string. One thing about FastBufferWriter.GetWriteSize() is that with dynamically-sized data like arrays and strings, it will return the size required to serialize the current value - it doesn't know about what values may be in the buffer. So on the reader side, if you've got an empty string as your current value, FastBufferWriter.GetWriteSize() is going to return a different result than it did with the populated string.

    SerializeValuePreChecked() for strings and arrays is really only useful if they're of a fixed, known size. Which is something that happens rarely enough that there is probably an argument here for deprecating them. Otherwise you have to just use SerializeValue() and let the built-in bounds checking handle it.

    One thing you could do here is use PreCheck() on the values of known size first, then do your strings and arrays after:

    Code (CSharp):
    1. public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    2. {
    3.     // Pre-check the statically-sized fields for optimization
    4.     int size = FastBufferWriter.GetWriteSize(user_id) + FastBufferWriter.GetWriteSize(currentRank) + FastBufferWriter.GetWriteSize(tier);
    5.     if (!serializer.PreCheck(size))
    6.     {
    7.         throw new OverflowException("Attempted to write but overflow the bounds.");
    8.     }
    9.  
    10.     serializer.SerializeValuePreChecked(ref user_id);
    11.     serializer.SerializeValuePreChecked(ref currentRank);
    12.     serializer.SerializeValuePreChecked(ref tier);
    13.  
    14.     // Dynamic sizes, can't pre-check
    15.     serializer.SerializeValue(ref nickname);
    16.     serializer.SerializeValue(ref icon);
    17. }
    Another option is that you could use something like FixedString32 or FixedString64 (or whatever size can properly contain your data). These are of known, fixed size and will always return the correct value from FastBufferWriter.GetWriteSize(), and as an added benefit, the reader side is able to deserialize them without creating any garbage. The downside is that if you have a FixedString64 containing the string "Hello", it'll still serialize 64 bytes even though you're only using 5, so you'll be wasting 59 bytes of bandwidth.

    The good news is that the difference in performance between using PreCheck() and not using PreCheck() is pretty small... you probably won't actually notice in practice.

    Hope that helps!
     
    firaui likes this.
  4. firaui

    firaui

    Joined:
    Nov 14, 2018
    Posts:
    12
    Hello Jaedyn,

    Thank you that's actually helpful. So the difference is actually small.
    One more thing, is this actually the same case for NetworkVariable? In FastBufferWriter also has WriteValue and WriteValueSafe. Is the performance difference also negligible?
     
  5. UnityJaedyn

    UnityJaedyn

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    4
    Hi firaui,

    I wouldn't say it's entirely negligible, but I would say you probably won't notice it unless you're doing quite a lot of serialization. WriteValue and WriteValueSafe are what SerializeValuePreChecked and SerializeValue call (respectively) - the defaults are different because FastBufferWriter is what we use internally, so we defaulted it to the faster option, while we wanted BufferSerializer to be more user-friendly, so we defaulted it to the easier-to-use option.

    The difference between the two is this:

    Code (CSharp):
    1. public unsafe void WriteValue<T>(in T value) where T : unmanaged
    2. {
    3.     int len = sizeof(T);
    4.  
    5.     fixed (T* ptr = &value)
    6.     {
    7.         UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len);
    8.     }
    9.     Handle->Position += len;
    10. }
    11.  
    12. public unsafe void WriteValueSafe<T>(in T value) where T : unmanaged
    13. {
    14.     int len = sizeof(T);
    15.  
    16.     if (!TryBeginWriteInternal(len))
    17.     {
    18.         throw new OverflowException("Writing past the end of the buffer");
    19.     }
    20.  
    21.     fixed (T* ptr = &value)
    22.     {
    23.         UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len);
    24.     }
    25.     Handle->Position += len;
    26. }
     
    firaui likes this.
unityunity