Search Unity

Feature Request Add `BurstCompiler.IsBurstCompiled` to check if a method is Burst-compiled or not.

Discussion in 'Burst' started by Enderlook, May 31, 2023.

  1. Enderlook

    Enderlook

    Joined:
    Dec 4, 2018
    Posts:
    53
    The idea is to allow this pattern with ease:

    Code (CSharp):
    1. public unsafe void MyMethod(MyStruct parameter)
    2. {
    3.      if (BurstCompiler.IsBurstCompiled)
    4.      {
    5.          // Do something....
    6.      }
    7.      else
    8.      {
    9.           fixed (ThisType* this_ = &this)
    10.           {
    11.                MyMethod(this_, &parameter);
    12.           }
    13.      }
    14. }
    15.  
    16. [BurstCompile]
    17. private static unsafe void MyMethod(ThisType* this_, MyStruct * parameter)
    18. {
    19.     this_->MyMethod(*parameter);
    20. }
    At the moment, that property could be replicated with the following:

    Code (CSharp):
    1. public static class BurstUtils
    2. {
    3.     public static bool IsBurstCompiled
    4.     {
    5.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    6.         get
    7.         {
    8.             bool burst = true;
    9.             Managed(ref burst);
    10.             return burst;
    11.  
    12.             [BurstDiscard]
    13.             static void Managed(ref bool burst) => burst= false;
    14.         }
    15.     }
    16. }
    However, it would save us having to create this helper function in each of our projects if Unity could provide it.

    Also, if Unity provides it, it could be more performant.
    In managed code, it can be simply `static bool IsBurstCompiler => false;`, making it small enough for the JIT always to inline it, and probably constant-propagate the boolean.
    In Burst compiled code, the Burst compiler could intersect this call and also replace it with another constant.
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,998
    To be frank, it sounds like a code smell to have code check how it was compiled. Why do you need that, other than for debugging purposes?

    As far as I understand the code snippet, it works as follows:
    • Managed:
      • call to instance MyMethod()
      • it is taking the managed code path and calls bursted static MyMethod
      • bursted static MyMethod calls MyMethod again on the object but this time it is taking the bursted execution path
    • Bursted:
      • call to static MyMethod()
      • it is taking the bursted execution path straight away
    Basically it allows you to call the same method from managed or bursted code. Is that the only purpose?

    In that case, I wager those should actually be two methods:
    • [BurstDiscard] public MyMethod()
      • calls bursted static MyMethod, eg a simple wrapper to execute something in bursted code
    • [BurstCompile] internal/private MyMethod_Bursted()
      • nothing special, this is the meat of the code that only the bursted method should/could call
      • no branching penalty in the bursted method either
    Unless I'm missing something, it seems you do not actually need this kind of check because you merely decide on whether to pass the call through a static bursted method or whether the call was made from the static bursted method. Henceforth, two separate methods is the better and more transparent solution, as indicated by the Burst attributes and the more restrictive access modifier on the bursted version.

    If you want to safeguard calling MyMethodBursted() so it can never be called from managed code I would set it up so the managed code doesn't even have access to it - if possible. Even if not (easily) possible you could simply enfore discipline by the team, typically through naming convention or some check within #if DEBUG #endif so it won't matter in builds.


    The real :rolleyes: I have with this request is this:
    For it to become a nuisance I would say you'd be starting several new projects per month. Which is not unheard of for sure ...

    But that indicates that you are creating enough new projects that it becomes a nuisance to add a single utility method which then makes me wonder why you haven't made your own custom package(s) yet for this and all the other utility methods you need in most projects?

    Just go to Package Manager and add from Git your custom package and you have not one but all of your frequently used utility methods readily available.

    Because there is always a bunch of methods you'll need often that Unity doesn't provide, like DestroyInAnyMode(), DestroyAllChildren() or GetOrAddComponent() and so on. Package Manager makes that pretty easy to manage. ;)
     
  3. Enderlook

    Enderlook

    Joined:
    Dec 4, 2018
    Posts:
    53
    Exactly.
    When writing utility methods that can be executed in both managed and burst-compiled code it came very handy.

    I noticed from the Burst Inspector, that my pattern allows better inlining when called from Burst. Your approach not only requires always taking addresses of the passed parameters, which increases code size as Burst doesn't seem to always optimize them always but also the call to the Burst-compiled method seems to not be always inlined, not sure why it happens but it may be because:
    • There is a call inside another call.
    • Due to taking the address of parameters.
    • Due to how Burst intersects the managed call and replaces it with a Burst-compiled. Since the burst compiled is unmanaged and the other method is managed, code is forced to do a function call to cross the managed boundary. However in Burst to Burst this is not required, yet the function call is preserved rather than inlined.
    Can't be sure why actually happens.

    Not necessary, but the fact that I must add my own boilerplate for such a simple method sounds unnecessary.
    For something like this -which has room to be optimized by Unity intrinsic or similar- it would better fit for them to include it in the Burst package.
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,998
    I understand. Though I would defer that in favor of better code design because it seems like both premature and micro optimization. This is a thing I would - at best - refactor near the end of a project when making that change is giving an ample and much required boost in performance - which I doubt it will (in a release/master build). Until then, the design of the code is way more important. ;)