Burst: 0.2.4-preview.39
Entities: 0.0.12-preview.21
Incremental Compiler: 0.0.42-preview.31
Unity Version: 2018.2.10f1
The following code does not produce correct result when burst is enabled:
public struct Data : IComponentData
{
public MyVector Value1;
public MyVector Value2;
}
public struct MyVector
{
public long X;
public long Y;
public long Z;
}
public static MyVector UpdateMyVector(MyVector value)
{
value = new MyVector
{
X = value.X + 5678,
};
return value;
}
public class TestBurstComponentSystem : JobComponentSystem
{
[BurstCompile]
private struct Job : IJobProcessComponentData<Data>
{
public void Execute(ref Data data)
{
data.Value2 = UpdateMyVector(data.Value1);
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
return new Job().Schedule(this, inputDeps);
}
}
The only part of interest is the UpdateMyVector method. It takes a MyVector struct as input and produces a MyVector struct as output. It’s expected to return a new struct with modified X value while Y, Z being 0. When burst compiler is disabled, it works as expected. However, when burst compiler is enabled, the method returns the original input value as if the assignment doesn’t exist.
I have put up a simple scene to reproduce this bug here:
https://github.com/xfxyjwf/burstbug
Open “Assets/Scenes/SampleScene.unity” and click play. At the center of the screen you are expected to see the number 5678, but when burst compiler is enabled you will see 0 instead. By toggling on/off burst jobs, you can see different values on the screen.
I have also observed that the bug only happens when the size of the struct is larger than 16 bytes. In the above example, if you remove the unused “Y”, “Z” field from MyVector struct, burst compiler will start to produce correct results.
More info from dnSpy and burst inspector:
Regardless the size of the MyVector struct, the IL instructions for UpdateMyVector (shown in dnSpy) are always :
0 0000 nop
1 0001 ldloca.s V_0 (0)
2 0003 initobj TestBurstBug/MyVector
3 0009 ldloca.s V_0 (0)
4 000B ldarga.s value (0)
5 000D ldfld int64 TestBurstBug/MyVector::X
6 0012 ldc.i4 0x162E
7 0017 conv.i8
8 0018 add
9 0019 stfld int64 TestBurstBug/MyVector::X
10 001E ldloc.0
11 001F starg.s value (0)
12 0021 ldarg.0
13 0022 stloc.1
14 0023 br 15 (0028) ldloc.1
15 0028 ldloc.1
16 0029 ret
However, depending on the size of MyVector, the IL instructions show in burst compiler are different.
When sizeof(MyVector) <= 16:
TestBurstBug/MyVector .TestBurstBug, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null::UpdateMyVector(.TestBurstBug+MyVector, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null)(TestBurstBug/MyVector value)
[0] TestBurstBug/MyVector var.0.
[1] TestBurstBug/MyVector var.1.
[2] TestBurstBug/MyVector var.value <-- From parameter `value` due to instruction `IL_000b: ldarga.s value`
--------------------------------------------
BL.entry: Block
--------------------------------------------
IL_000b: ldarg value
IL_000b: stloc value args(IL_000b) var.value
IL_0000: br BL.0000
--------------------------------------------
BL.0000: Block
--------------------------------------------
IL_0001: ldloca.s V_0 var.0.
IL_0003: initobj TestBurstBug/MyVector args(IL_0001)
IL_0009: ldloca.s V_0 var.0.
IL_000b: ldloca value var.value
IL_000d: ldfld System.Int64 TestBurstBug/MyVector::X args(IL_000b)
IL_0012: ldc.i4 5678
IL_0017: conv.i8 args(IL_0012)
IL_0018: add args(IL_000d, IL_0017)
IL_0019: stfld System.Int64 TestBurstBug/MyVector::X args(IL_0009, IL_0018)
IL_001e: ldloc.0 var.0.
IL_001f: stloc value args(IL_001e) var.value
IL_0021: ldloc var.value
IL_0022: stloc args(IL_0021) var.1.
IL_0023: br BL.0028
--------------------------------------------
BL.0028: Block
--------------------------------------------
IL_0028: ldloc.1 var.1.
IL_0029: ret args(IL_0028)
When sizeof(MyVector) >= 16:
void .TestBurstBug, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null::UpdateMyVector(.TestBurstBug+MyVector, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null)(TestBurstBug/MyVector* $___struct_ret, TestBurstBug/MyVector* value)
[0] TestBurstBug/MyVector var.0.
[1] TestBurstBug/MyVector var.1.
[2] TestBurstBug/MyVector var.value <-- From parameter `value` due to instruction `IL_000b: ldarga.s value`
--------------------------------------------
BL.entry: Block
--------------------------------------------
IL_000b: ldarg value
IL_000b: ldobj value args(IL_000b) <-- load of indirect argument
IL_000b: stloc value args(IL_000b) var.value
IL_0000: br BL.0000
--------------------------------------------
BL.0000: Block
--------------------------------------------
IL_0001: ldloca.s V_0 var.0.
IL_0003: initobj TestBurstBug/MyVector args(IL_0001)
IL_0009: ldloca.s V_0 var.0.
IL_000b: ldarg value <-- load of indirect argument address
IL_000d: ldfld System.Int64 TestBurstBug/MyVector::X args(IL_000b)
IL_0012: ldc.i4 5678
IL_0017: conv.i8 args(IL_0012)
IL_0018: add args(IL_000d, IL_0017)
IL_0019: stfld System.Int64 TestBurstBug/MyVector::X args(IL_0009, IL_0018)
IL_001e: ldloc.0 var.0.
IL_001f: stloc value args(IL_001e) var.value
IL_0021: ldarg.0
IL_0021: ldobj args(IL_0021) <-- load of indirect argument
IL_0022: stloc args(IL_0021) var.1.
IL_0023: br BL.0028
--------------------------------------------
BL.0028: Block
--------------------------------------------
IL_0028: ldloc.1 var.1.
IL_0029: ldarg.0
IL_0029: stobj args(IL_0029, IL_0028) <-- write result to caller allocated struct
IL_0029: ret
It seems to me the burst inspector is not showing the original IL instructions but instead a modified version. In both cases (sizeof(MyVector) <= 16 and sizeof(MyVector) > 16), the modified IL introduced a new local variable “var.value” to be used in place of the argument “value”. The difference between the two lies in instruction IL_0021. The instructions above IL_0021 are writing the newly created MyVector to “var.value”, but in one version, IL_0021 loads from “var.value”, but in the other version, IL_0021 loads from arg.0. The one loading from arg.0 ends up producing incorrect LLVM IR code returning the original input value rather than the modified one. I think the bug lies in the code that transforms original IL to the modified version.