Search Unity

StringBuilder problem

Discussion in 'Scripting' started by Muckel, Feb 6, 2012.

  1. Muckel

    Muckel

    Joined:
    Mar 26, 2009
    Posts:
    471
    Hello,
    well i want to display some Raycast Values with SpriteText.
    Now the normal way is:
    Code (csharp):
    1. RaycastHit hit;
    2.     if (Physics.Raycast (SpaceShip.transform.position, -Vector3.up,out hit)) {
    3.    distogroundText.Text = ("Ground " + hit.distance.ToString("000") + " ft") ;
    4.  
    this seams to produce some overhead garbage ... suck's...
    So i want to use the StringBuilder Class ...
    If i do this:
    Code (csharp):
    1. RaycastHit hit;
    2.     if (Physics.Raycast (SpaceShip.transform.position, -Vector3.up,out hit)) {
    3.         StringBuilder sb = new StringBuilder("Ground ");
    4.     sb.AppendFormat("{0:F2} ", hit.distance);
    5.     distogroundText.Text = sb.ToString() ;
    6.  
    there is no difference in Profiler... hmmm did i made something wrong ?

    thank you for taking the time!
     
  2. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    you should initialize the string builder variable with a initial size. otherwise it will start containing ground and need to reallocate memory same as string does.
    but i think you definitely waste your time at a wrong point. this calculation is usually done once per frame and the effort is not worth the runtime. you only optimize when there is a real bootleneck (which has been proven by profiler). so when you do heavy string operations in a loop you can use string builder but for gui + debug messages its not worth it imo.
     
  3. tonyd

    tonyd

    Joined:
    Jun 2, 2009
    Posts:
    1,224
  4. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    The code looks fine, so I think the problem is your { } after the if statement. You havn't copied in all your code, so its hard to be sure.

    If your'e getting garbage, it probably means the hit is null.
     
  5. Muckel

    Muckel

    Joined:
    Mar 26, 2009
    Posts:
    471
    Hello,
    well many thxx for the reply !
    i was on the wrong way ! Someone told me to use StringBuilder because of Performance but it only makes sense if you have real Strings no int's or float's....
    So i found out if i Display in my GUI a RayCast with SpriteText it takes over 8% and 123 GC Alloc... in Profiler.
    If i sent the Value to a HealthBar or similar... it takes 0.2 % and NO GC !
    That means to Display Text and update it is very heavy .... nothing change here... i thought there make some Optimizations in Unity 3.5 EZ GUI but it is not true...
    If i open my Old Project with Unity3D 3.x.x i get the same and in older Version too....

    So Conclusion is : Do not use Dynamic Text ... it still kill's a lot of Performance.... damn or did i miss something ?

    thank you
     
  6. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    I know this post is old but I wanted to point out the problem with the OP's use of StringBuilder.

    Calling 'new' is always going to allocate memory and seeing as they are creating a new StringBuilder for every ray cast hit, memory is going to be allocated.

    The correct way to use StringBuilder is
    Code (CSharp):
    1. StringBuilder sb;
    2.  
    3. void Start()
    4. {
    5.      sb = new StringBuilder(32, 32);//initialize to a usable value so you only use what you need no more no less
    6. }
    7.  
    8. void SomeFunction(string input)
    9. {
    10.     sb.Append(input);
    11. }
    The version of StringBuilder being used with Unity is however still going to generate garbage with certain calls.

    - AppendFormat() will generate gabrage as it calls ToString() on any object passed in. Latest versions of Mono and .NET still have this issue(to my knowledge).
    - ToString() will generate garbage. Latest versions of Mono and .NET apparently don't on StringBuilder(have not tested)
    There are probably more functions but these are the two I needed to use.

    There are some workarounds you can do extending the StringBuilder to include garbage free string use.

    There is no Clear() function in this version of Unity/Mono, and I have yet to get it to clear the StringBuilder properly to make it of any use(still trying at it). I have read somewhere that Unity 5 will update to a newer version of Mono and will have better GC and other much needed fixes.
     
  7. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    Clear is added in a later .Net Version (4.0 iirc) so I made an Extension Method with the same name and once mono is updated you can simply remove the extension code and your client-code should require no change.

    Code (csharp):
    1.  
    2. public static class ExtensionMethods_String
    3. {
    4.    public static void Clear ( this StringBuilder instance )
    5.    {// #Hack this shall reset the stringbuilder without releasing the memory, .net 4.0 adds a clear method which is not available here, maybe when unity's mono is updated, at least it can be made compatible by removing this method source: http://www.mindfiresolutions.com/A-better-way-to-Clear-a-StringBuilder-951.php
    6.      instance.Length = 0;
    7.    }
    8. }
    9.  
    Thanks for the hint. I'll look into it.

    First post with some usefull contribution. Rarely seen these days here. Kudos for that.
     
  8. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261

    Thanks for the kudos :). I think community means that we work together towards the same common goal, forums are communities; and if we don't help each other how will we ever get better?

    Your extension method works if the strings are going to be the same size or larger every time you clear StringBuilder. If the old string is larger than the new, you will get the new string plus what is left over from the old string. Setting the length to 0 only sets the cursor to 0. The rest of the old data is still in there.

    I use this method to clear
    Code (CSharp):
    1. public static void Clear(this StringBuilder sb)
    2. {
    3.     sb.Length = 0;//set to zero to put cursor at the beginning.
    4.    
    5.         //blank out the entire capacity of the StringBuilder
    6.     for (int i = 0; i < sb.Capacity; ++i)
    7.     {
    8.         sb.Append(" ");
    9.     }
    10.    
    11.     sb.Length = 0;//Set cursor back at the beginning.
    12. }
    Now I just need to get the formatting to work correctly.
     
  9. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    While your method is good if you NEED the content to be cleared everytime you call clear, it's inefficient in comparison to just zeroing the length, which yes only sets the cursor point, but that's all you need to do if you don't need to clear the data. The data will be overwritten when you assign/append new values. :) Hope this helps!
     
  10. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    Yes, but the forum is flooded by leechers who don't want to contribute only take out what they need. thats why i find it important to note that you seem to be one of the rare exceptions to that general observation.

    afaik setting length to zero is also what the built-in net4.0 clear method does. i have only read this and not veryfied but it seems a little overkill to me to overwrite the data twice. i could only imagine that usefull for security/obscurity purposes but not for the general ingame usecase.

    Code (csharp):
    1.  
    2.   public static void Clear(this StringBuilder sb)
    3.   {
    4.   sb.Length = 0;//set to zero to put cursor at the beginning.
    5.    
    6.   //blank out the entire capacity of the StringBuilder
    7.   for (int i = 0; i < sb.Capacity; ++i)
    8.   {
    9.   sb.Append(" ");
    10.   }
    11.    
    12.   sb.Length = 0;//Set cursor back at the beginning.
    13.   }
    14.  
    in your method you overwrite the whole capacity what is a waste when you only use a fraction of that. if you want to keep this "overwrite" feature at least cache the length and only clear until that:
    Code (csharp):
    1.  
    2.   public static void Clear(this StringBuilder sb)
    3.   {
    4.    int oldlength = sb.Length;
    5.   sb.Length = 0;//set to zero to put cursor at the beginning.
    6.    
    7.   //blank out the entire capacity of the StringBuilder
    8.   for (int i = 0; i < oldlength; ++i)
    9.   {
    10.   sb.Append(" ");
    11.   }
    12.    
    13.   sb.Length = 0;//Set cursor back at the beginning.
    14.   }
    15.  
    but as mentioned i find this unneccessary.
     
  11. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    For how long the average string is the clear method is negligible. In practical use setting length to 0 and leaving it at that is sufficient as most numbers being displayed only get bigger, like a score or a dollar value. Even those may need clearing, and if your worried about performance then caching the length and only clearing that is more efficient. How often are you going to be clearing out the score or dollar amount? Pretty sure your not going to be doing it every frame.

    I also seem to find that setting length to 0 even if your original string is smaller will have garbage data in the string that is left over in the memory locations assigned to the string. This could be due the method use to get the string reference from the StringBuilder without generating garbage. I will have to test further to see if that is the case.
     
  12. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    For convenience a nicely optimized Clear extension:
    Code (CSharp):
    1.     public static void Clear(this StringBuilder sb, bool wipeout = false) {
    2.         int oldlength = sb.Length;
    3.         sb.Length = 0;//set to zero to put cursor at the beginning.
    4.  
    5.         if (wipeout) {
    6.             //blank out the entire capacity of the StringBuilder
    7.             sb.Append(new String(' ', oldlength));
    8.  
    9.             sb.Length = 0;//Set cursor back at the beginning.
    10.         }
    11.     }
     
  13. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    I was going to post a similar method to do exactly that. I thought of three different ones after awhile; Reset, Clear and Wipe

    Reset: reset the cursor setting length to 0.
    Clear: only clear out the data that will be replaced.
    Wipe: clear the whole thing.

    Reset used when incoming data is same length.
    Clear used when incoming data is larger.
    Wipe if you need it.

    I also thought about wrapping StringBuilder into another class that allowed for all this functionality to go on mostly behind the scenes. Then that's kinda like reinventing the wheel. I don't feel like reinventing the wheel to display a few strings and when most of this is already done for us, only in a new version of Mono/.net.

    That being said. This might not even be needed, if I am getting the string from the wrong place using the extension method. I might only need to set length to 0, this I still need to test.
    I come from XNA, and didn't use the extension to get the string from the StringBuilder as I only passed it directly to the SpriteBatch to draw the string. In Unity I cannot pass it in anywhere and ToString() generates garbage.

    One little thing about your method.
    This line:
    Code (CSharp):
    1. sb.Append(new String(' ', oldlength);
    This will generate garbage. You do not want to create another object by using new. Might as well create a new StringBuilder when you want to clear it then. I think even in c++, you want to avoid using new during game play, as even allocating memory takes some time, and that can be avoided by pre-instantiating any new objects before game play begins(lots of reusing variables and objects with object pools.)

    As a note these are all small optimizations, that I think we can include into our everyday coding practices, so we don't need to worry about them(I think I read someone else say that.) Quick and dirty is exactly that. Don't slack on code in the beginning of a project as it will only come back to haunt you while debugging/optimizing later.
     
  14. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    So I did some tests looking at the other fields available and got 2 for strings _str and _cached_str.

    _cached_str doesn't work, no matter which way I try to clear.
    _str works which is what I am using.

    Code (CSharp):
    1. public static string GarbageFreeString(this StringBuilder sb)
    2. {
    3.      string str = (string)sb.GetType().GetField(
    4.             "_str",
    5.             System.Reflection.BindingFlags.NonPublic |
    6.             System.Reflection.BindingFlags.Instance).GetValue(sb);
    7.  
    8.      return str;
    9. }
    I did this to get the fields available in the StringBuilder instance.
    Code (CSharp):
    1. Type t = sb.GetType();
    2. foreach (var f in t.GetFields(System.Reflection.BindingFlags.NonPublic |
    3.           System.Reflection.BindingFlags.Instance))
    4.      UnityEngine.Debug.Log(f.Name);
    I'll have to stick to what I'm doing it works fine for me and the simple projects I'm working on. I have no garbage generated at runtime, and it runs 60+ fps on my crappy desktop. Still need to test on Android/iOS but I think it shouldn't be an issue. If I can do it in Flash, sure as hell I can do it in Unity. Unity is using less memory at the moment but I have only added sound effects and not the music.
     
  15. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Thanks for your reply, I had no idea new Generated garbage, that helps me a bit. I mainly avoid writing code that runs slow I sometimes don't know the reasons, such as new generating garbage overhead, interesting. :)
     
  16. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    If so you should think twice before using reflection in frequently called method since this is also slow.
    In general i suggest to avoid such "premature" optimizations. you should only omptimize something which has been identified (by profiling!) as a real bottleneck in an advanced build on the target hardware. its often not worth it to spend time on such things in early development as code is changed anyway in most cases. and optimized code is harder to read and understand.
    so focus on readability and understandability first.
     
    LovesSolvingProblems likes this.
  17. manny003

    manny003

    Joined:
    Mar 18, 2014
    Posts:
    70
    A little late to the Party, I know -- but I just found out that setting SrtringBuilder.Length = 0 also has the effect of clearing the contents.

    I did several tests where I would Append in a long string of characters, perform a Debug.Log(..), set the Length = 0, and Append a shorter string of characters and do another Debug.Log(..).

    The shorter length string printed with no "extra" characters. This works in Unity Editor as well as iOS IL2CPP.

    Using Unity 5.0.2p3

    Manny
     
  18. scone

    scone

    Joined:
    May 21, 2008
    Posts:
    244
  19. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    I have tested the StringBuilder.ToString() and it still generates garbage.

    You also might want to update the incorrect information you have in that blog post. Unity 5.0 is still using .net 2.0, and still requires the work around. IL2CPP seems to be the issue holding them back from upgrading. There was some confusion about what version 5.0 would be using and was clarified here.
     
  20. scone

    scone

    Joined:
    May 21, 2008
    Posts:
    244
    You're right I was confused. Thanks for clearing that up. That said, I did test StringBuilder.ToString() at some point recently but to be honest I can't remember if it was a beta version or what. I did try to make sure that I tested it before updating the article but I also haven't been keeping the code that uses the workaround up-to-date. That said, I'm diving back into that project and once I get to the bottom of the situation I'll revise the post.

    Anyway, I think the fundamental workaround is still valid, if needed. Instantiate the string builder with a buffer large enough to handle any kind of string that you need. Or, just let the one StringBuilder re-allocate every time your string gets bigger, but instead of calling.ToString() access the internal buffer. It's still messy, but you don't have to make the reflection call every time you need to access the string, and you have to blank out extra characters if you buffer isn't exactly the right size, but you can still mess around with strings garbage-free, and it did the job for me while significantly reducing CPU load.
     
  21. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    The cached string doesn't work to well with the new UI. I have to set the text to "" then call cachedTextGenerator.Invalidate(); then set the text to the cached string. Three calls to change the text, plus what ever all that is doing in behind the scenes, I get no garbage and haven't seen much from performance, but it looks ugly code wise. Can't wait for Unity to update the API to .net 4.5.
     
  22. HanzaRu

    HanzaRu

    Joined:
    Aug 10, 2012
    Posts:
    11
    I have created a Special class for high intense use of concatenation between integers and text. It generates no garbage (until the moment i have tested) and the performance is better than StringBuilder.Append(int):

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4. using System.Collections;
    5. using System.Text;
    6.  
    7. public class SpeedString {
    8.  
    9.     public string string_base;
    10.     public StringBuilder string_builder;
    11.     private char[] int_parser = new char[11];
    12.  
    13.     public SpeedString(int capacity){
    14.         string_builder = new StringBuilder(capacity,capacity);
    15.         string_base = (string)string_builder.GetType().GetField(
    16.             "_str",
    17.             System.Reflection.BindingFlags.NonPublic |
    18.             System.Reflection.BindingFlags.Instance).GetValue(string_builder);
    19.     }
    20.  
    21.     private int i;
    22.     public void Clear(){
    23.         string_builder.Length = 0;
    24.         for(i=0;i<string_builder.Capacity;i++){
    25.             string_builder.Append(' ');
    26.         }
    27.         string_builder.Length = 0;
    28.     }
    29.  
    30.     public void Draw(ref Text text){
    31.         text.text = "";
    32.         text.text = string_base;
    33.         text.cachedTextGenerator.Invalidate();
    34.     }
    35.  
    36.     public void Append(string value){
    37.         string_builder.Append(value);
    38.     }
    39.  
    40.     int count;
    41.     public void Append(int value){
    42.         if(value >= 0){
    43.             count = ToCharArray((uint)value,int_parser,0);
    44.         }
    45.         else{
    46.             int_parser[0] = '-';
    47.             count = ToCharArray((uint)-value,int_parser,1)+1;
    48.         }
    49.         for(i=0;i<count;i++){
    50.             string_builder.Append(int_parser[i]);
    51.         }
    52.     }
    53.  
    54.     private static int ToCharArray(uint value, char[] buffer,int bufferIndex) {
    55.         if (value == 0) {
    56.             buffer[bufferIndex] = '0';
    57.             return 1;
    58.         }
    59.         int len = 1;
    60.         for (uint rem = value/10; rem > 0; rem /= 10) {
    61.             len++;
    62.         }
    63.         for (int i = len-1; i>= 0; i--) {
    64.             buffer[bufferIndex+i] = (char)('0'+(value%10));
    65.             value /= 10;
    66.         }
    67.         return len;
    68.     }
    69. }
    An exemple usage for it:

    Code (CSharp):
    1. public class Panel_Test : MonoBehaviour {
    2.  
    3.     public Text velocity;
    4.     public SpeedString velocity_string = new SpeedString(32);
    5.  
    6.     void Update(){
    7.         //Stats
    8.         velocity_string.Clear();
    9.         velocity_string.Append(last_velocity);
    10.         velocity_string.Append(" MPH");
    11.         velocity_string.Draw(ref velocity);
    12.     }
    13. }
    The only problem i can't found nowhere how i can force the Text component to update without disable / enable it again.(This seens slow, not sure), but works better than must solutions i have readed.

    if you know a way to make it better for the community use, please extend it and put it here for us :D

    Edit: Improved Draw function with the comment above
     
    Last edited: Aug 3, 2015
    LovesSolvingProblems likes this.
  23. scone

    scone

    Joined:
    May 21, 2008
    Posts:
    244
  24. BenHymers

    BenHymers

    Joined:
    Dec 16, 2014
    Posts:
    30
    I'm finding that setting StringBuilder.Length allocates an amount proportional to the current capacity! Which is completely nonsensical, and means that StringBuilder just isn't reusable in a garbage-free way. This is with Unity 5.4.0f3, but I remember it happening back to 5.2 or so. I assume this is because of the rubbish old Mono runtime version - there's no logical reason for Length to allocate.

    Is anyone successfully clearing and reusing StringBuilders without them allocating memory, in 5.4 or later?

    For reference, the callstack I see in the Unity Profiler is:
    StringBuilder.set_Length
    StringBuilder.InternalEnsureCapacity
    String.CharCopy
    String.CharCopy
    String.memcpy4
    String.InternalAllocateStr <-- this is the bit that allocates memory.

    Browsing the Mono StringBuilder source confirms this is what's happening; it's far too eager to allocate memory. See here: https://searchcode.com/codesearch/view/7227794/
     
    bezier likes this.
  25. Dave-Carlile

    Dave-Carlile

    Joined:
    Sep 16, 2012
    Posts:
    967