Search Unity

Suggestion - compile time branching in burst jobs

Discussion in 'Burst' started by JLJac, Aug 14, 2020.

  1. JLJac

    JLJac

    Joined:
    Feb 18, 2014
    Posts:
    36
    Hi! Let me know if this is in the wrong place.

    Imagine you're writing a job that has first some set-up code, then a hot loop. Inside the loop you might want to do either A or B, say call either one method or another.

    You have two alternatives: either you accept a branch in your hot loop, or you write two jobs where everything except that inner method call is identical. This is what I have been doing, but maintaining duplicate code is sub-optimal.

    Instead, I'd like to be able to tell the burst compiler that I promise it a certain variable won't ever change while inside the job, that the variable is fixed on schedule time. This would allow the compiler to get rid of branches based on that variable, and the hot loop could run branch-less.

    Example:
    Code (CSharp):
    1.  
    2. [BurstCompile]
    3. struct BlurJob : IJobParallelForBatch
    4. {
    5.    [ReadOnly] public float valueA;
    6.    [ReadOnly] public float valueB;
    7.    [ReadOnly] public float valueC;
    8.    [ReadOnly] public NativeArray<float4> pixelsIn;
    9.    [WriteOnly] public NativeArray<float4> pixelsOut;
    10.  
    11.    enum FilterType { HQ, LQ };
    12.    [SheduleTimeStatic] public FilterType _fType;
    13.  
    14.    public void Execute(int startIndex, int batchSize) {
    15.       //this set up code is the same for all versions of the job
    16.       float d = valueA + valueB;
    17.       float e = d + valueC;
    18.  
    19.       //so is this loop
    20.       for (int i = startIndex; i < startIndex + batchSize; i++) {
    21.          float f = e + i;
    22.  
    23.          //this is different for each of the two versions of the job. The job is
    24.          //re-compiled for each novel value of this, so the condition is never
    25.          //checked in run-time
    26.          switch(_fType){
    27.             case HQ: pixelsOut[i] = HighQualityBlur(pixelsIn[i], f); break;
    28.             case LQ: pixelsOut[i] = LowQualityBlur(pixelsIn[i], f); break;
    29.          }
    30.        }
    31.    }
    32.  
    33.    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    34.    static float4 HighQualityBlur(float4 in, float something)
    35.    {
    36.    ...
    37.    }
    38.  
    39.    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    40.    static float4 LowQualityBlur(float4 in, float something)
    41.    {
    42.    ...
    43.    }
    44. }
    45.  
    The compiler would basically create 2 different versions of the above job, where only what happened in the loop would be different. It would be able to do this because the [SheduleTimeStatic] tag promises it that the value won't change inside the job, so it could treat it as a compile time constant.

    As you're aware, there are a multitude of other cases where the compiler can do a much better job if it knows what a value is at compile-time. Such as more efficient vectorization if it knows the iteration count when looping over an array, and more efficient division/multiplication if it knows that it's dealing with a power of two.

    If the specific hardware it's compiling for doesn't support this for any reason, it could just fall back on ignoring the [SheduleTimeStatic] symbol. This would mean checking the condition in run-time, which would be slower but produce an identical result.

    Thank you for your time!
     
    ScriptsEngineer and manpower13 like this.
  2. james7132

    james7132

    Joined:
    Mar 6, 2015
    Posts:
    166
    Since the value n the example is a constant for the job, could the condition be moved outside the loop? It'll still be one job, but it will move the jump instructions outside of the hot loop and allow the compiler to vectorize two separate loops.
     
  3. amarcolina

    amarcolina

    Joined:
    Jun 19, 2014
    Posts:
    65
    If you absolutely need compile-time switches, I've found using generics works really well for situations like this.

    Code (CSharp):
    1. public struct BlurJob<BlurT> : IJob where BlurT : IBlur {
    2.  
    3.     public void Execute() {
    4.         default(BlurT).DoBlur(...);
    5.     }
    6.  
    7. }
    8.  
    9. new BlurJob<HQBlur>().Schedule();
    10. new BlurJob<LQBlur>().Schedule();
     
    cultureulterior and burningmime like this.
  4. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    @amarcolina 's solution is the best, but if it doesn't fit neatly into a set of interface methods, you could also do some hack like this (might be some typos, but you get the idea):

    Code (CSharp):
    1. internal interface IMyJobMode { }
    2. internal unsafe struct Mode1 : IMyJobMode { fixed byte unused[16];  }
    3. internal unsafe struct Mode2 : IMyJobMode { fixed byte unused[32];  }
    4.  
    5. internal unsafe struct MyJob<TMode> : IJob where T : unmanaged, IMyJobMode {
    6.     public void Execute() {
    7.         if(sizeof(TMode) == sizeof(Mode1)) {
    8.             /* do thing 1 */ }
    9.         else {
    10.            /* do thing 2 */ } } }
    11.  
    12. new MyJob<Mode1>().Schedule();
    13. new MyJob<Mode2>().Schedule();
     
    amarcolina likes this.
  5. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    Something like this works, but can quickly become unwieldy if it's many values:

    Code (CSharp):
    1. public void Execute () {
    2.     int runTimeValue = ... // 0,1,2,3
    3.     switch (runTimeValue) {
    4.         case 0:
    5.             ActualExecute(0);
    6.             break;
    7.         case 1:
    8.             ActualExecute(1);
    9.             break;
    10.         case 2:
    11.             ActualExecute(2);
    12.             break;
    13.         case 3:
    14.             ActualExecute(3);
    15.             break;
    16.     }
    17. }
    18.  
    19. [MethodImpl(MethodImplOptions.AggressiveInlining)]
    20. void ActualExecute (int COMPILE_TIME) {
    21.     // it's compile time here
    22. }
     
  6. JLJac

    JLJac

    Joined:
    Feb 18, 2014
    Posts:
    36
    Hey this might actually do the trick! Thank you, I'll try it!


    So you're basically using the bit-size of the data type as an enumerator? It's a super cool idea. Would it actually resolve at compile time though?


    This looks like a runtime switch-statement to me?
     
  7. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    It is, but the compiler will inline the methods with the hardcoded argument - effectively lifting the conditional out of any of the loops or whatever is going on inside the complicated job and only evaluating it once at the start outside of the method itself (constant propagation). And all the code is still in one place, which is probably a good thing.

    I'd like to point out that the other suggestions also seem to be runtime conditionals; the point is just to evaluate them once, outside the loop. You can't really make a compile time conditional for a runtime switch.

    amarcolina's solution looks to scale a lot better if you're going to add more optional functionality, so that may be the better solution in general. Has some more boilerplate though if it's just for one bool, so I figured I'd post this option.