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.

Discussion Blaming the software and not the user

Discussion in 'General Discussion' started by impheris, Oct 28, 2022.

  1. impheris

    impheris

    Joined:
    Dec 30, 2009
    Posts:
    1,518
    This is maybe a silly post but i felt the need to create it and tell my recent experience...
    I'm making a low poly game for Android, i have a pretty old device here, which is perfect for me to achieve good performance.
    Anyway i had this problem where the game was running between 30min - 34max fps and for a low poly game that is really bad, so i bring this amazing PDF about mobile game performance:
    https://blog.unity.com/technology/o...ips-on-profiling-memory-and-code-architecture (READ IT BOY)

    With just a half read of that PDF i boosted the fps to 33min - 45max (it will make sense in the end)

    I look for all those weird lines that the profiler showed me on google and everybody was sad and angry about unity's bad performance this days, so i became sad and angry too because one tends to blame the software :mad:.
    Something very curious for me was something that i saw in the profiler... The MainCamera was rendering: object, opaques, transparent objects etc... But i have an empty object with cinemachine that was rendering the same, so i though "meh, a unity thing i guess" but after 2 hours i click the object with chinemachine and -.- there was a camera component there -.-" IDK why or how or what but it was there, after deleting that component the game was perfectly at 60 fps :D (yes i have it unlocked for now)

    Now i'm pretty surprised that unity was fighting to keep the game at 30min with 2 cameras rendering the same thing without any performance work before :cool: that is pretty good in my opinion.

    So, what did we learn today?
    always double check your project, your scripts, read performance tips and tutorials from unity. I've found many people in the forum talking really bad about Unity (me included sometimes) but is really important to understand the software and accept that is probably our fault (in 50% of the cases at least)

    Has something similar had happened to you?

    th-4210480156.jpg
     
    PaulMDev, tmonestudio and Socrates like this.
  2. chingwa

    chingwa

    Joined:
    Dec 4, 2009
    Posts:
    3,784
    Debug.Log("test flag");
    ...This does it to me every time.
     
  3. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,106
    You are right! It's so obvious!

    The next time I have a performance issue, I'll also blame you @impheris
     
    Kiwasi and derkoi like this.
  4. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,538
    Yee, that really makes me wish C# had fully functional preprocessor directives like C++ with replacement capability. Even in super strict MISRA code we are allowed to use them for DEBUG() because it's such a perfect solution.
    Even when you do write your own Debug method with an
    if (debug)
    inside it, the string still takes memory and the "if" wastes a small amount of time. And don't you dare have
    Debug.Log("test Value: " + value)
    to make it even worse with two string allocations Dx
    The C# preprocessor defines do not cover this fully and are clunky to use.
     
  5. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,369
    Strictly speaking, that's not the right way to do it.

    The right way would be having Debug and Release version of Debug.Log where release version does nothing.
    Then, the compiler in turn should realize that the function has no effect and eliminate its arguments and process of computing them.
     
  6. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,240
    Unity provides a property to check if you're in a debug build. Alternatively you could create your own classes for it.
    Code (csharp):
    1. if (Debug.isDebugBuild) Debug.Log("Foo");
     
    Last edited: Oct 29, 2022
    MadeFromPolygons likes this.
  7. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,033
  8. impheris

    impheris

    Joined:
    Dec 30, 2009
    Posts:
    1,518
    Ja, so many info here looks important but my noobness is superior to all of you and i do not understand xD
    :( sorry
     
  9. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,538
    It was just mentioned/implied that having Debug.Log("whatever") can mess you up when it comes to performance. The method is surprisingly slow because Unity has to gather the whole stacktrace, aka the information where the method was called which you can see it in the console.
    Have certainly had it happened to disappointedly find out that the new feature I was developing has a terrible impact on performance - only to realize it's solely because I had a ton of debug logs in.

    So avoid writing logs every frame and also when you use the profiler to investigate performance.

    The talk about completely eliminating the impact in builds is of course something about perfectionism. Usually it will not hurt to have the occasional info log in a build.

    Certainly have had encountered dumb performance hassles like GetComponentInChildren() in Update methods or simply things that got unexpectedly computed way more often than supposed to (without directly causing a bug).
    In every case Unity's Profilers are a blessing as they help you discover this quickly. I miss them whenever I work in more bare-metal environments or other languages.

    Isn't it exactly that what you do in C++?
    You have a makro DEBUG which during development time, has some print method behind it and for the release build, it's a no-op, aka getting replaced with nothing.

    Interestingly enough, the conditional attribute @koirat mentioned would allow you do do pretty much that in C#, if I understood that right.
    Interesting.
    It's still a slight hassle in Unity because as far as I know you always have to go into the editor and preferences to set a "define"; you cannot type it in code.
     
    Last edited: Oct 30, 2022
  10. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    Rogue Debug.Log() calls in release builds are definitely an issue on consoles. It's hilariously expensive on PS4, for example.

    The DEBUG directive is automatically defined in editor and development builds, no need to add/remove it from the project settings.

    I was assuming the Conditional attribute is probably a shortcut to wrapping the method body in a #if CONDITION block and that surely C# compilers should be smart enough to spot unused method arguments and prune any non-side effects producing code that produces them, right? But doubt crept on me, and I decided to actually test it.

    I went to sharplab.io and wrote this:

    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3.  
    4. public static class MyDebug {
    5.     public static void Log(string param)
    6.     {
    7.     }
    8. }
    9.  
    10. public class C {
    11.     public void M() {
    12.         var r = new System.Random(42);
    13.         var a = r.Next();
    14.      
    15.         MyDebug.Log($"The value is {a}");
    16.     }
    17. }
    The MyDebugLog() does absolutely nothing, yet this is the resulting release IL for C.M():

    Code (CSharp):
    1.     .method public hidebysig
    2.         instance void M () cil managed
    3.     {
    4.         // Method begins at RVA 0x2090
    5.         // Code size 56 (0x38)
    6.         .maxstack 3
    7.         .locals init (
    8.             [0] int32 a,
    9.             [1] valuetype [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
    10.         )
    11.  
    12.         IL_0000: ldc.i4.s 42
    13.         IL_0002: newobj instance void [System.Runtime]System.Random::.ctor(int32)
    14.         IL_0007: callvirt instance int32 [System.Runtime]System.Random::Next()
    15.         IL_000c: stloc.0
    16.         IL_000d: ldloca.s 1
    17.         IL_000f: ldc.i4.s 13
    18.         IL_0011: ldc.i4.1
    19.         IL_0012: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::.ctor(int32, int32)
    20.         IL_0017: ldloca.s 1
    21.         IL_0019: ldstr "The value is "
    22.         IL_001e: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
    23.         IL_0023: ldloca.s 1
    24.         IL_0025: ldloc.0
    25.         IL_0026: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
    26.         IL_002b: ldloca.s 1
    27.         IL_002d: call instance string [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::ToStringAndClear()
    28.         IL_0032: call void MyDebug::Log(string)
    29.         IL_0037: ret
    30.     } // end of method C::M
    Even if you're not familiar with IL, it's obvious it's actually doing all the work to build the interpolated string to pass to MyDebug.Log.

    I then added [Conditional("DEBUG")] to the MyDebug.Log method:
    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3.  
    4. public static class MyDebug {
    5.     [Conditional("DEBUG")]
    6.     public static void Log(string param)
    7.     {
    8.     }
    9. }
    10.  
    11. public class C {
    12.     public void M() {
    13.         var r = new System.Random(42);
    14.         var a = r.Next();
    15.      
    16.         MyDebug.Log($"The value is {a}");
    17.     }
    18. }
    Which now results in this IL when compiled in release:
    Code (CSharp):
    1.     .method public hidebysig
    2.         instance void M () cil managed
    3.     {
    4.         // Method begins at RVA 0x2090
    5.         // Code size 14 (0xe)
    6.         .maxstack 8
    7.  
    8.         IL_0000: ldc.i4.s 42
    9.         IL_0002: newobj instance void [System.Runtime]System.Random::.ctor(int32)
    10.         IL_0007: callvirt instance int32 [System.Runtime]System.Random::Next()
    11.         IL_000c: pop
    12.         IL_000d: ret
    13.     } // end of method C::M
    Which actually removes the call to MyDebug.Log. It seems C# compilers don't do any kind of dead code removal even in release mode, unlike C++, and you should be mindful of that. The JIT ASM shows the same extraneous code in it. This means that this will still create the useless string even with a Conditional atribute:

    Code (CSharp):
    1. using System;
    2. using System.Diagnostics;
    3.  
    4. public static class MyDebug {
    5.     [Conditional("DEBUG")]
    6.     public static void Log(string param)
    7.     {
    8.     }
    9. }
    10.  
    11. public class C {
    12.     public void M() {
    13.         var r = new System.Random(42);
    14.         var a = r.Next();
    15.      
    16.         var b = $"The value is {a}";
    17.         MyDebug.Log(b);
    18.     }
    19. }
    There's a small chance IL2CPP might prune dead code when using the highest optimization settings, but since the generated code is full of internal side-effects (allocating objects, etc), I wouldn't count on it.
     
    Last edited: Oct 30, 2022
  11. impheris

    impheris

    Joined:
    Dec 30, 2009
    Posts:
    1,518
    I use Debug.Log a lot, mostly when a want to test if a function or a line of code is working as i want but it has never given me any performance issues but i will keep your info in mind :)
     
  12. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,527
    I use it a lot, too, while I'm working on something new or investigating why certain things are happening. But when I'm done I remove everything except initialisation, error and warning type stuff. Anything else is spam which is likely getting in the way when something is awry and I need to see the important stuff.

    Yeah, I was really surprised when I first found that out. Though in my case it was the thought that "surely stuff like simple vector addition would get inlined?" and then "oh, nope, it's allocating things and calling methods on them".
     
    Kiwasi likes this.
  13. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,538
    Had read up on this once. The thing is, C# and also Java for example have reflection. That disallows the "C# to IL" compiler from completely removing methods because then you could not access them via reflection anymore.
    BUT, that does not mean those languages are terribly slow because they have something else: The just-in-time compiler which processes the IL into actual CPU instructions aka ASM. That one has the liberty to inline and omit empty methods and also do things a C++ compiler could not do like optimization based on the current state.

    Theoretically it should be able to turn a method like:
    Code (CSharp):
    1.  
    2. CustomLog(string str)
    3. {
    4. if (debug)
    5.    Debug.Log(str)
    6. }
    7.  
    into a no-op by knowing that "debug" is constant. Have not investigated that yet.
     
    Last edited: Oct 30, 2022
  14. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    The ASM output in sharplab.io didn't show any sort of removal, but I didn't dig enough to see how representative that is to real world scenarios.

    I wouldn't count on it, however. While JIT is able to use run-time information for optimization which isn't available in AOT (like how "hot" a piece of code really is), the fact it has to do it at runtime limits how much memory and CPU cycles it can use for this stuff. It doesn't help that Mono is aeons behind modern JIT either.

    This is why burst can be such a boost over standard C#, even for code that doesn't vectorize well.
     
    angrypenguin likes this.
  15. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,527
    Right, "theoretically", "should". In the example of the vector operations, though, people found that manually inlining the math gave a significant performance boost*, suggesting that it wasn't getting optimised at any stage of the execution pipeline, or that those optimisations were significantly inferior to doing it yourself.

    One thing to note there was that this was quite some time ago. I believe it was before IL2CPP was in common use, so that may indeed change things, and I've not looked into it too much because it doesn't matter especially often. It's in the micro-optimisation territory.

    * In code where there was lots of vector math going on, e.g. loops with many calculations.
     
    Kiwasi likes this.
  16. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    I need to do some deep testing, but back in Unity 2019 I had mixed results on different platforms with IL2CPP. I remember seeing soem call stacks that hinted at insufficient inlining at some places, for example.

    The generated IL2CPP code is also full of boilerplate required by the dotnet runtime like checking a static variable on every static function call to see if the static class was initialized. This doesn't exist in burst code.
     
    angrypenguin likes this.
  17. derkoi

    derkoi

    Joined:
    Jul 3, 2012
    Posts:
    2,246
    You should stick to 30fps on mobile to prevent devices overheating and excessive battery consumption.
     
  18. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    961
    I've spent a month working on some very intricate systems, and to my dismay there were some reason for Unity crashing in those systems, seemingly without any discernable cause. The weird thing was that sometimes it worked, sometimes it didn't. I feared it would be some illusive bug or unknown limitation inside Unity itself, but no matter how hard I tried to reproduce it in a clean project, I couldn't. It would only happen in the immense complexity of the original project. These issues really are the worst!

    After a lot of sighs and banging my head against the wall, I finally discovered the issue. Somewhere in there I was copying the fields of one compute buffer to another (struct[].vertex -> vertices[]). Turns out I was feeding the source struct buffer to the shader incorrectly. It was expecting the struct[] buffer, but it got a vertices[] buffer instead, causing the operation to exceed bounds when trying to copy the fields into the destination. Apparently there is no memory safety or exceptions for such a violation. Very devious!

    I'm so relieved it got solved, and while better error messaging could have helped, I was still the one to blame for this, not Unity.

    Now I'm always using assertions to make sure this never happens again!

    Code (CSharp):
    1. #if UNITY_EDITOR
    2.     UnityEngine.Assertions.Assert.IsNotNull(meshDataBuffer);
    3.     UnityEngine.Assertions.Assert.IsNotNull(verticesBuffer);
    4.     UnityEngine.Assertions.Assert.IsTrue(meshDataBuffer.stride == Utility.GetSizeOf<MeshData>());
    5.     UnityEngine.Assertions.Assert.IsTrue(verticesBuffer.stride == Utility.GetSizeOf<Vector3>());
    6.     UnityEngine.Assertions.Assert.IsTrue(meshDataBuffer.count == verticesBuffer.count);
    7. #endif
     
    impheris, tmonestudio and Neto_Kokku like this.
  19. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,369
    In C++ it is a good idea to avoid macros, this sort of tool is like using a rusty rusty hatchet or a chainsaw during brain surgery. C++ macros is search/replace, no safety checking whatsoever. They're also a royal pain to debug.

    So, in C++, ideally using DEBUG() macros would be bad, but forcing compiler to eliminate calls to empty function bodies would be good.

    Unfortunately, we do not live in ideal world, and that's why Macros is still part of C++ toolset - it still has its uses.

    C++ compiler has more freedom to exterminate unused code than C# compiler, by the way, as there's no reflection, so if something is not called explicitly, there's high chance it is not useful.