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

Resolved Is it possible to burst this? (delegates)

Discussion in 'Entity Component System' started by illinar, May 3, 2023.

  1. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    I'm testing the idea of making an interpreted OOP scripting language that operates on DOTS ECS.

    The script would be parsed and boiled down to an array of operators executing consecutively. There are a lot of ways to do it, but I can't find one that could be fully burstable.

    Code (CSharp):
    1.     public struct Operator : IComponentData
    2.     {
    3.         public int OperatorCode;
    4.         public Entity OperandA;
    5.         public Entity OperandB;
    6.         public Entity Output;
    7.     }
    8.  
    9.     public struct IntOperand : IComponentData
    10.     {
    11.         public int Value;
    12.     }
    13.  
    14.     public struct OperandLookups
    15.     {
    16.         public ComponentLookup<IntOperand> Ints;
    17.     }
    18.  
    19.     [BurstCompile]
    20.     [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
    21.     public partial struct ExecuteOperatorsSystem: ISystem
    22.     {
    23.         delegate void OperatorDelegate(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output);
    24.         OperatorDelegate[] operatorDelegates;
    25.  
    26.         [BurstCompile]
    27.         public void OnCreate(ref SystemState state)
    28.         {
    29.             operatorDelegates = new OperatorDelegate[] {
    30.                 ExecuteAddIntInt
    31.             };
    32.         }
    33.  
    34.         [BurstCompile]
    35.         public void OnUpdate(ref SystemState state)
    36.         {
    37.             OperandLookups lookups = new OperandLookups
    38.             {
    39.                 Ints = state.GetComponentLookup<IntOperand>()
    40.             };
    41.             foreach (var @operator in SystemAPI.Query<RefRW<Operator>>())
    42.             {
    43.                 var Operator = @operator.ValueRO;
    44.                 operatorDelegates[Operator.OperatorCode](ref lookups, ref Operator.OperandA, ref Operator.OperandB, ref Operator.Output);
    45.             }
    46.         }
    47.  
    48.         [BurstCompile]
    49.         void ExecuteAddIntInt(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output)
    50.         {
    51.             lookups.Ints[Output] = new IntOperand
    52.             {
    53.                 Value = lookups.Ints[OperandA].Value + lookups.Ints[OperandB].Value
    54.             };
    55.         }
    56.     }
    I'd love to make everything burstable but all the valid ways to execute the script (execute all operators in order) is by having delegates, or using interfaces, basically having managed code. In this particular case Burst doesn't like the array of delegates.

    Is there any way I could iterate through all the operators and execute them, given that they will all have different input data and different logic? And there will probably be hundreds of them?

    I guess I could just turn the scripts into a string of delegates and run them, but again, I wonder if there is a burstable option.
     
  2. Saniell

    Saniell

    Joined:
    Oct 24, 2015
    Posts:
    167
  3. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Thanks! That looks exactly like what I'm looking for!

    I got stuck, however, with
    'MonoPInvokeCallbackAttribute' is inaccessible due to its protection level
    error... Can't figure out why.

    Code (CSharp):
    1.         public delegate void OperatorDelegate(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output);
    2.  
    3.         [BurstCompile]
    4.         [MonoPInvokeCallback(typeof(OperatorDelegate))]
    5.         public static void ExecuteAddIntInt(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output)
    6.         {
    7.             lookups.Ints[Output] = new IntOperand
    8.             {
    9.                 Value = lookups.Ints[OperandA].Value + lookups.Ints[OperandB].Value
    10.             };
    11.         }
    12.  
    13.         [BurstCompile]
    14.         [MonoPInvokeCallback(typeof(OperatorDelegate))]
    15.         public static void ExecuteSubtractIntInt(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output)
    16.         {
    17.             lookups.Ints[Output] = new IntOperand
    18.             {
    19.                 Value = lookups.Ints[OperandA].Value - lookups.Ints[OperandB].Value
    20.             };
    21.         }
    This is quite annoying.

    upload_2023-5-4_0-50-26.png
     
    Last edited: May 3, 2023
  4. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    257
    You can ignore that the atttribute "MonoPInvokeCallback" is internal. It still works without the attribute. Tested it some time ago. It may be important for IL2CPP, I guess.
     
  5. Saniell

    Saniell

    Joined:
    Oct 24, 2015
    Posts:
    167
    What namespace is that from? I don't know which version of unity you're using but for 2021.3 (global::)AOT.MonoPInvokeCallback works just fine
     
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    I don't recommend function pointers for this, as the overhead could eliminate the benefits of using Burst. I would suggest modeling how actual CPUs parse and execute instructions, by defining a custom instruction set and using a switch case to parse and apply operations.
     
  7. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Yeah I noticed that the usage examples don't even use that.

    So, I got it bursted and it looks like it is gonna be slow X) There is no way I can get this vectorized. But maybe I will be able to process multiple scripts at the same time on multiple threads.

    Code (CSharp):
    1. using System.Diagnostics;
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6.  
    7. namespace ScriptingTest
    8. {
    9.     public struct Operator : IComponentData
    10.     {
    11.         public int FunctionCode;
    12.         public Entity OperandA;
    13.         public Entity OperandB;
    14.         public Entity Output;
    15.     }
    16.  
    17.     public struct IntOperand : IComponentData
    18.     {
    19.         public int Value;
    20.     }
    21.  
    22.     public struct OperandLookups
    23.     {
    24.         public ComponentLookup<IntOperand> Ints;
    25.     }
    26.  
    27.     [BurstCompile]
    28.     public class OperatorFunctions
    29.     {
    30.         public delegate void OperatorDelegate(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output);
    31.  
    32.         [BurstCompile]
    33.         public static void AddIntInt(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output)
    34.         {
    35.             lookups.Ints[Output] = new IntOperand
    36.             {
    37.                 Value = lookups.Ints[OperandA].Value + lookups.Ints[OperandB].Value
    38.             };
    39.         }
    40.  
    41.         [BurstCompile]
    42.         public static unsafe void SubtractIntInt(ref OperandLookups lookups, ref Entity OperandA, ref Entity OperandB, ref Entity Output)
    43.         {
    44.             lookups.Ints[Output] = new IntOperand
    45.             {
    46.                 Value = lookups.Ints[OperandA].Value - lookups.Ints[OperandB].Value
    47.             };
    48.         }
    49.     }
    50.  
    51.     [BurstCompile]
    52.     struct ExecuteOperatorsJob : IJob
    53.     {
    54.         public NativeArray<FunctionPointer<OperatorFunctions.OperatorDelegate>> OperatorFunctionPointers;
    55.         public OperandLookups OperandLookups;
    56.         public NativeArray<Operator> Operators;
    57.  
    58.         public void Execute()
    59.         {
    60.             for (int i = 0; i < Operators.Length; i++)
    61.             {
    62.                 var op = Operators[i];
    63.                 OperatorFunctionPointers[op.FunctionCode].Invoke(ref OperandLookups, ref op.OperandA, ref op.OperandB, ref op.Output);
    64.             }
    65.         }
    66.     }
    67.  
    68.     [BurstCompile]
    69.     [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
    70.     public partial struct ExecuteOperatorsSystem : ISystem
    71.     {
    72.         private NativeArray<FunctionPointer<OperatorFunctions.OperatorDelegate>> OperatorFunctions;
    73.         private readonly static OperatorFunctions.OperatorDelegate mulFunctionPointerInvoke = BurstCompiler.CompileFunctionPointer<OperatorFunctions.OperatorDelegate>(ScriptingTest.OperatorFunctions.AddIntInt).Invoke;
    74.  
    75.         [BurstCompile]
    76.         public void OnCreate(ref SystemState state)
    77.         {
    78.             OperatorFunctions = new NativeArray<FunctionPointer<OperatorFunctions.OperatorDelegate>>(2, Allocator.Persistent);
    79.             OperatorFunctions[0] = BurstCompiler.CompileFunctionPointer<OperatorFunctions.OperatorDelegate>(ScriptingTest.OperatorFunctions.AddIntInt);
    80.             OperatorFunctions[1] = BurstCompiler.CompileFunctionPointer<OperatorFunctions.OperatorDelegate>(ScriptingTest.OperatorFunctions.SubtractIntInt);
    81.             CreateTestOperators(state, 10000);
    82.         }
    83.  
    84.         [BurstCompile]
    85.         public void OnUpdate(ref SystemState state)
    86.         {
    87.             OperandLookups operandLookups = new OperandLookups
    88.             {
    89.                 Ints = state.GetComponentLookup<IntOperand>()
    90.             };
    91.             var operators = state.GetEntityQuery(ComponentType.ReadWrite<Operator>()).ToComponentDataArray<Operator>(Allocator.TempJob);
    92.             var job = new ExecuteOperatorsJob
    93.             {
    94.                 OperatorFunctionPointers = OperatorFunctions,
    95.                 OperandLookups = operandLookups,
    96.                 Operators = operators
    97.             };
    98.             UnityEngine.Debug.Log(operators.Length);
    99.  
    100.             JobHandle handle = job.Schedule();
    101.  
    102.             Stopwatch stopwatch = new Stopwatch();
    103.             stopwatch.Start();
    104.  
    105.             handle.Complete();
    106.  
    107.             stopwatch.Stop();
    108.             long elapsedTimeMs = stopwatch.ElapsedMilliseconds;
    109.  
    110.             UnityEngine.Debug.Log("Job execution time: " + elapsedTimeMs + " ms");
    111.         }
    112.  
    113.         [BurstCompile]
    114.         public void CreateTestOperators(SystemState state, int amount)
    115.         {
    116.             var operatorType = new NativeArray<ComponentType>(1, Allocator.Temp);
    117.             operatorType[0] = ComponentType.ReadOnly<Operator>();
    118.             var operatorArchetype = state.EntityManager.CreateArchetype(operatorType);
    119.             var operatorEntities = state.EntityManager.CreateEntity(operatorArchetype, amount, Allocator.Temp);
    120.  
    121.             var operandType = new NativeArray<ComponentType>(1, Allocator.Temp);
    122.             operandType[0] = ComponentType.ReadOnly<IntOperand>();
    123.             var operandArchetype = state.EntityManager.CreateArchetype(operandType);
    124.             var operandEntities = state.EntityManager.CreateEntity(operandArchetype, amount * 3, Allocator.Temp);
    125.  
    126.  
    127.             for (int i = 0, j = 0; i < operatorEntities.Length; i++, j += 3)
    128.             {
    129.                 var op = new Operator
    130.                 {
    131.                     OperandA = operandEntities[j],
    132.                     OperandB = operandEntities[j + 1],
    133.                     Output = operandEntities[j + 2],
    134.                 };
    135.                 state.EntityManager.SetComponentData<Operator>(operatorEntities[i], op);
    136.             }
    137.         }
    138.     }
    139. }
    Absolutely horrific performance. 30MS for 10000 operations.
     
    Last edited: May 4, 2023
  8. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Yes I might have to do that. The performance of functions was horrible. I was already considering branching switch statements.

    1) Branch on operation type: add, subtract, multiply, assign, execute, etc.
    2) Branch on operand types. Probably unify a lot of types as float and int. I wish I could use some bit blobs and convert them into necessary types.
    3) Do more extra branching to narrow down the operation faster.

    Disadvantage: operation count will reduce performance.

    In the end of the day I don't know what the parsing performance would be like in the "compile stage", and what it is like with an actual compiler and code generation. Because the only advantage of custom interpreted script over code generation is faster "compilation". Maybe I just need to emit code. I need to test Roslyn compiler.

    I will look into how machine code or CPU instructions work though. I only saw a glimpse of it before.
     
  9. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    MonoPInvokeCallbackAttribute is from Entities namespace.
     
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    I wouldn't even bother with vectorization for a scripting layer. There are so many more things you can do that will have a way bigger impact on performance. Good memory layout (those lookups on operands are not helping), better branching (one large switch on a value is often better than multiple smaller switch statements because it becomes a single jump table jump), and minimizing copies of large structs.
     
    bb8_1, Occuros and One1Guy like this.
  11. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Yes, I know, my memory layout is currently as bad as it can be. I do want to at least store raw data in native arrays and to have it ordered so it can be prefetched. Some of it or a lot of it will be looking up variables, but at least variable addresses will be ordered and prefetched.

    Interesting. I feel like the idea of such scripting system might be dum to begin with, but at least it is very interesting :)

    Using an existing scripting library or compiling C# in runtime does seem smarter.
     
    Last edited: May 4, 2023
    apkdev likes this.
  12. OUTTAHERE

    OUTTAHERE

    Joined:
    Sep 23, 2013
    Posts:
    656
    I think a DSL (or scripting language) for ECS code is actually what we need (instead of Burst, which I consider, despite its performance characteristics, to be a dead end at best and a downgrade from C# at worst)

    I'm fine with compile time scripting, it does not need interpreting at all.

    It just needs less and more consistent boilerplate than whatever it is we have now with SystemBase / ISystem / ...

    Does Burst transpile from IL or from C#? I think C#. But maybe .NET's newer AoT compilation features can be a silver lining on a dark horizon here.
     
    Last edited: May 6, 2023
  13. Greexonn

    Greexonn

    Joined:
    Mar 10, 2020
    Posts:
    19
    upload_2023-5-6_16-18-11.png

    It's IL
     
    OUTTAHERE likes this.
  14. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Just so everyone knows, that was a bad idea, but a good experiment. I got almost 30x better performance by processing operators as instructions in a switch expression. It also scales pretty well. But this is still poor performance. It could suffice for some rudimentary scripting, or arythmetic operations.

    A lot of the code is nonsense, it is there for test purposes:

    Code (CSharp):
    1. namespace BranchedScriptingTest
    2. {
    3.     public enum Instruction
    4.     {
    5.         Read,
    6.         Write,
    7.         Set,
    8.         Copy,
    9.         Add,
    10.         Multiply,
    11.         Return,
    12.         Call,
    13.         O1,
    14.         O2,
    15.         O3,
    16.         O4,
    17.     }
    18.  
    19.     public struct Operation : IComponentData
    20.     {
    21.         public Instruction Instruction;
    22.         public int Operand;
    23.     }
    24.  
    25.     public struct Variables
    26.     {
    27.         public NativeArray<int> Ints;
    28.         public NativeArray<float> Floats;
    29.     }
    30.  
    31.     [BurstCompile]
    32.     struct ExecuteOperationsJob : IJob
    33.     {
    34.         public Variables Variables;
    35.         public NativeArray<Operation> Operations;
    36.  
    37.         public void Execute()
    38.         {
    39.             int intaccumulator = 0;
    40.             for (int i = 0; i < Operations.Length; i++)
    41.             {
    42.                 var op = Operations[i];
    43.                 switch (op.Instruction)
    44.                 {
    45.                     case Instruction.Read:
    46.                         intaccumulator = Variables.Ints[op.Operand];
    47.                         break;
    48.                     case Instruction.Write:
    49.                         intaccumulator += Variables.Ints[op.Operand];
    50.                         break;
    51.                     case Instruction.Set:
    52.                         intaccumulator += Variables.Ints[op.Operand];
    53.                         break;
    54.                     case Instruction.Copy:
    55.                         intaccumulator += Variables.Ints[op.Operand]; ;
    56.                         break;
    57.                     case Instruction.Add:
    58.                         intaccumulator -= Variables.Ints[op.Operand];
    59.                         break;
    60.                     case Instruction.Multiply:
    61.                         intaccumulator -= Variables.Ints[op.Operand];
    62.                         break;
    63.                     case Instruction.Return:
    64.                         intaccumulator -= op.Operand;
    65.                         break;
    66.                     case Instruction.O1:
    67.                         intaccumulator -= Variables.Ints[op.Operand];
    68.                         break;
    69.                     case Instruction.O2:
    70.                         intaccumulator += Variables.Ints[op.Operand];
    71.                         break;
    72.                     case Instruction.O3:
    73.                         intaccumulator -= op.Operand;
    74.                         break;
    75.                     case Instruction.O4:
    76.                         intaccumulator -= op.Operand;
    77.                         break;
    78.                     default:
    79.                         intaccumulator += op.Operand;
    80.                         break;
    81.                 }
    82.             }
    83.         }
    84.     }
    85.  
    86.     [BurstCompile]
    87.     public partial struct ExecuteOperationsSystem : ISystem
    88.     {
    89.         private Variables _variables;
    90.  
    91.         [BurstCompile]
    92.         public void OnCreate(ref SystemState state)
    93.         {
    94.             CreateTestOperators(state, 100000);
    95.             InitializeVariables(1000);
    96.         }
    97.  
    98.         [BurstCompile]
    99.         public void OnUpdate(ref SystemState state)
    100.         {
    101.             var operations = state.GetEntityQuery(ComponentType.ReadWrite<Operation>()).ToComponentDataArray<Operation>(Allocator.TempJob);
    102.             var job = new ExecuteOperationsJob
    103.             {
    104.                 Variables = _variables,
    105.                 Operations = operations,
    106.             };
    107.  
    108.             Stopwatch stopwatch = new Stopwatch();
    109.             JobHandle handle = job.Schedule();
    110.             stopwatch.Start();
    111.             handle.Complete();
    112.             stopwatch.Stop();
    113.             long elapsedTimeMs = stopwatch.ElapsedMilliseconds;
    114.             UnityEngine.Debug.Log($"Switch job execution time for {operations.Length} operations: {elapsedTimeMs} ms");
    115.         }
    116.  
    117.         public void CreateTestOperators(SystemState state, int amount)
    118.         {
    119.             var operatorType = new NativeArray<ComponentType>(1, Allocator.Temp);
    120.             operatorType[0] = ComponentType.ReadOnly<Operation>();
    121.             var operatorArchetype = state.EntityManager.CreateArchetype(operatorType);
    122.             var operatorEntities = state.EntityManager.CreateEntity(operatorArchetype, amount, Allocator.Temp);
    123.  
    124.             for (int i = 0; i < operatorEntities.Length; i++)
    125.             {
    126.                 var op = new Operation
    127.                 {
    128.                     Instruction = (Instruction)UnityEngine.Random.Range(0, (int)Instruction.Call),
    129.                     Operand = UnityEngine.Random.Range(0, 999),
    130.                 };
    131.                 state.EntityManager.SetComponentData<Operation>(operatorEntities[i], op);
    132.             }
    133.         }
    134.  
    135.         public void InitializeVariables(int amount)
    136.         {
    137.             _variables = new Variables
    138.             {
    139.                 Ints = new NativeArray<int>(1000, Allocator.Persistent),
    140.                 Floats = new NativeArray<float>(1000, Allocator.Persistent),
    141.             };
    142.             for (int i = 0; i < amount; i++)
    143.             {
    144.                 _variables.Ints[i] = UnityEngine.Random.Range(0, 999);
    145.                 _variables.Floats[i] = UnityEngine.Random.Range(0f, 999f);
    146.             }
    147.         }
    148.     }
    149. }
     
    apkdev and bb8_1 like this.
  15. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    943
    I don't think this is a good idea. It will be more dead end than the current. You're going to further split the community than it already is and the ecosystem is not there. Just the IDE support alone would be severely lacking. The use case is very limited that it can only be used in Unity development so other non Unity devs won't jump on this to help the community grow. It's not like Rust or Go.

    While I agree that the boilerplate is not desirable, there's really no better alternative. This is like having a C++ environment but in C#. I'll take it over writing raw C++.
     
    Laicasaane likes this.