Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Bug Bursted code behaving differently in release build only

Discussion in 'Burst' started by Selmar, Jul 10, 2023.

  1. Selmar

    Selmar

    Joined:
    Sep 13, 2011
    Posts:
    55
    We use a type 'ObservableEvent' that is a fixed-size key-value store using an fixed byte array, mapping semantics to untyped values. The event generation is working fine unless the code is called in a bursted context in a release build; it seems to fill the data with zeroes and not much else (from what I can tell).

    I've tried with various BurstCompile settings on the assembly level to see if I could reproduce the problem in-editor, but I can't. The build logs none of the related errors either. My initial hunch was this has something to do with type sizes/offsets, but I didn't have any real idea yet. After putting debug logs everywhere, things suddenly worked...

    I made several builds to figure out where, and I found that if I put logs in ObservableEventBuilder.Build() (at the end of the code snippets), the code does what it's supposed to do. I suspect burst is perhaps optimizing this code a little too optimistically?
    Code (csharp):
    1.  
    2.            ObservableEvent @event = _event;
    3.            this = default;
    4.            return @event;
    5.  
    I assume this is a burst compiler bug. We're on 1.8.7 currently with Unity 2022.3.4f1. I've submitted a bug report as well: IN-46832

    Code (csharp):
    1.    // the data inserted is non-zero
    2.    ObservableEventBuilder builder = new ObservableEventBuilder();
    3.    builder.SetObservableEventType(ObservableEventType.SnowballCollisionDeath);
    4.    builder.AddData(ESemantic.Source, spawnId.ValueRO);
    5.    builder.AddData(ESemantic.Owner, lastInteraction.ValueRO.Owner);
    6.    builder.AddData(ESemantic.Position, transform.ValueRO.Position);
    7.    builder.AddData(ESemantic.Velocity, velocity.ValueRO.Linear);
    8.    int idx = observableEvents.Add(builder.Build());
    9.    var ev = observableEvents[idx];
    10.    // here, ev contains only zeroes, but in release builds only!
    11.  

    Code (csharp):
    1.    public unsafe struct ObservableEvent : IBufferElementData
    2.    {
    3.        internal const int NUM_VARS = 8;
    4.        internal const int STORAGE_SIZE = 104;
    5.  
    6.        public ObservableEventType Type;
    7.        internal fixed byte _semantics[NUM_VARS];
    8.        internal fixed byte _offsets[NUM_VARS];
    9.        internal fixed byte _eventData[STORAGE_SIZE];
    10.  
    11.        [MethodImpl(MethodImplOptions.AggressiveInlining)]
    12.        internal unsafe int _GetIndex(ESemantic semantic)
    13.        {
    14.            int idx = -1;
    15.            fixed (byte* pSemantics = _semantics)
    16.            {
    17.                for (int i = 0; i < NUM_VARS; ++i)
    18.                {
    19.                    if (pSemantics[i] == (byte)semantic)
    20.                    {
    21.                        idx = i;
    22.                        break;
    23.                    }
    24.                }
    25.            }
    26.  
    27.            return idx;
    28.        }
    29.  
    30.        [MethodImpl(MethodImplOptions.AggressiveInlining)]
    31.        internal unsafe T _GetData<T>(int idx) where T : unmanaged
    32.        {
    33.            T outVariable;
    34.            byte offset;
    35.            fixed (byte* pOffsets = _offsets)
    36.                offset = _offsets[idx];
    37.            Assert.IsTrue(offset + sizeof(T) <= STORAGE_SIZE);
    38.            fixed (byte* pData = _eventData)
    39.                UnsafeUtility.MemCpy(&outVariable, &pData[offset], sizeof(float3));
    40.            return outVariable;
    41.        }
    42.  
    43.        [MethodImpl(MethodImplOptions.AggressiveInlining)]
    44.        public unsafe T Get<T>(ESemantic semantic) where T : unmanaged
    45.        {
    46.            int idx = _GetIndex(semantic);
    47.            if (idx < 0)
    48.            {
    49.                Debug.LogError($"Event does not contain a variable for '{(byte)semantic}'.");
    50.                return default;
    51.            }
    52.  
    53.            if (Hint.Unlikely(
    54.                !ObservableEventUtils.IsSemanticTypePairValid<T>(semantic)
    55.                ))
    56.            {
    57.                FixedString64Bytes nameOfT = nameof(T);
    58.                Debug.LogError($"Invalid semantic '{(byte)semantic}' for type '{nameOfT}'");
    59.                return default;
    60.            }
    61.  
    62.            return _GetData<T>(idx);
    63.        }
    64.  
    65.        [MethodImpl(MethodImplOptions.AggressiveInlining)]
    66.        public bool Has(ESemantic semantic)
    67.        {
    68.            return _GetIndex(semantic) >= 0;
    69.        }
    70.    }
    71.  

    Code (csharp):
    1.    internal struct ObservableEventBuilder
    2.    {
    3.        ObservableEvent _event;
    4.        int _currentOffset;
    5.  
    6.        public ObservableEventBuilder(ObservableEventType type)
    7.        {
    8.            _event.Type = type;
    9.            _currentOffset = 0;
    10.        }
    11.  
    12.        public void SetObservableEventType(ObservableEventType type)
    13.        {
    14.            Assert.IsTrue(_event.Type == ObservableEventType.None);
    15.            _event.Type = type;
    16.            Debug.Log($"ObservableEventBuilder.SetObservableEventType : {(int)_event.Type}");
    17.        }
    18.  
    19.        public unsafe void AddData<T>(ESemantic semantic, T data) where T : unmanaged
    20.        {
    21.            if (!ObservableEventUtils.IsSemanticTypePairValid<T>(semantic))
    22.            {
    23.                FixedString64Bytes nameOfT = nameof(T);
    24.                Debug.LogError($"ObservableEventBuilder: semantic '{semantic}' does not match type '{nameOfT}'.");
    25.                return;
    26.            }
    27.  
    28.            int idx = _event._GetIndex(semantic);
    29.            if(idx >= 0)
    30.            {
    31.                Debug.LogError($"ObservableEventBuilder: event already contains a variable for '{semantic}'.");
    32.                return;
    33.            }
    34.  
    35.            int freeIdx = _event._GetIndex(ESemantic.None);
    36.            if(freeIdx < 0)
    37.            {
    38.                Debug.LogError($"ObservableEventBuilder: event can not store more variables.");
    39.                return;
    40.            }
    41.  
    42.            int offset = _currentOffset;
    43.            int dataSize = sizeof(T);
    44.            int nextOffset = offset + dataSize;
    45.            if (offset + dataSize > ObservableEvent.STORAGE_SIZE)
    46.            {
    47.                FixedString64Bytes nameOfT = nameof(T);
    48.                Debug.LogError($"ObservableEventBuilder: event can not store more data. Current size: '{offset}', size after adding '{nameOfT}': '{nextOffset}'");
    49.                return;
    50.            }
    51.  
    52.            _currentOffset = nextOffset;
    53.  
    54.            fixed (byte* pSemantics = _event._semantics)
    55.                pSemantics[freeIdx] = (byte)semantic;
    56.  
    57.            Assert.IsTrue(nextOffset < byte.MaxValue);
    58.            fixed (byte* pOffsets = _event._offsets)
    59.                pOffsets[freeIdx] = (byte)offset;
    60.  
    61.            Assert.IsTrue(offset + dataSize <= ObservableEvent.STORAGE_SIZE);
    62.            fixed (byte* pData = _event._eventData)
    63.                UnsafeUtility.MemCpy(&pData[offset], &data, dataSize);
    64.        }
    65.  
    66.        public ObservableEvent Build()
    67.        {
    68.            // !! if the two lines commented here are uncommented, the code works!
    69.            //Debug.Log($"ObservableEventBuilder.Build : {(int)_event.Type}");
    70.  
    71.            ObservableEvent @event = _event;
    72.            this = default;
    73.  
    74.            Assert.AreNotEqual((int)@event.Type, (int)ObservableEventType.None);
    75.  
    76.            //Debug.Log($"ObservableEventBuilder.Build : {(int)@event.Type}");
    77.            return @event;
    78.        }
    79.    }
     
  2. tim_jones

    tim_jones

    Unity Technologies

    Joined:
    May 2, 2019
    Posts:
    282
    Hi @Selmar, we'll look out for IN-46832.

    Code (CSharp):
    1. var ev = observableEvents[idx];
    2. // here, ev contains only zeroes, but in release builds only!
    What do you do with `ev` after this?

    In `ObservableEventBuilder.Build()`, what happens if you comment out the `this = default;` line - does that make it work?

    Since this works in Debug but not in Release, my guess is that it's related to optimization, probably not to type sizes / offsets. Specifically, LLVM has perhaps decided that the output of this code isn't actually used, so it can skip some or all of the preceding steps. If you add a `Debug.Log`, that makes the output used so LLVM can't do that optimization. Just a guess.
     
  3. Selmar

    Selmar

    Joined:
    Sep 13, 2011
    Posts:
    55
    In that particular case `ev` is used for logging only.

    Based on my tests I am thinking the same as you. I suspect the reason I didn't reproduce it in a debug build is maybe because of the Assert that I forgot to remove during my tests.

    I don't have the opportunity to test commenting out the `this = default;` line. As a workaround I've just rewritten it as `return _event;` and adapted my API.