No Garbage Text ( MgStringBuilder )

This is all the little fixes and changes made since last year to my stringbuilder wrapper class. Numbers converted to strings in c# create garbage that builds up and can create excessive garbage collections. I created this class after finding out that this can often be the number one cause of high frequency’s of garbage collections.

The class itself basically serves as a wrapper around stringbuilder to intercept the numerical appends and decode them to text without creating garbage. It can still do things like sb.Append("some ").Append("text); or drop it directly into a method, as i added the operator overloading for it. You can also yank the stringbuilder out directly if you want to work on it and put it back ect. I have been using it regularly since i made it instead of the regular stringbuilder it’s one of my defacto classes.

       using System;
       using System.Text;
       using System.Collections.Generic;
       using Microsoft.Xna.Framework;

       namespace Microsoft.Xna.Framework
       {
        //No Garbage Text StringBuilder 5th major fix or change Jan 1 2018.
        /// <summary>
        /// No garbage stringbuilder
        /// The purpose of this class is to eliminate garbage collections. 
        /// Performance is to be considered primarily.
        /// 
        /// While this is not created for high precision display. 
        /// Ill attempt to get it into reasonable shape over time.
        /// ...
        /// Change log 2017
        /// ...
        /// March 13 Added chained append funtionality that was getting annoying not having it.
        /// ...
        /// March 14 Cut out excess trailing float and double zeros.
        /// this appears to only be a partial fix for case 1.
        /// ...
        /// March 17 
        /// Fixed a major floating point error. 
        ///  Shifted the remainder of floats doubles into the higher integer range.
        /// Fixed the second case for trailing zeros. 
        ///  (its starting to look a bit better) 
        /// ...
        /// Nov 08 Fixed n = -n; when values were negative in float double appends.
        /// that would lead to a bug were - integer portions didn't get returned.
        /// ...
        /// Dec01 
        /// yanked some redundant stuff for a duncile swap reference hack.
        /// </summary>
        public sealed class MgStringBuilder
        {
            private static char decimalseperator = '.';
            private static char minus = '-';
            private static char plus = '+';

            private StringBuilder sb;
            /// <summary>
            /// It is recommended you avoid this unless needed. 
            /// it is possible to create garbage with it.
            /// </summary>
            public StringBuilder StringBuilder
            {
                get { return sb; }
                private set { if (sb == null) { sb = value; } else { sb.Clear(); sb.Append(value); } }
            }

            public int Length
            {
                get { return StringBuilder.Length; }
                set { StringBuilder.Length = value; }
            }
            public int Capacity
            {
                get { return StringBuilder.Capacity; }
                set { StringBuilder.Capacity = value; }
            }
            public void Clear()
            {
                Length = 0;
                sb.Length = 0;
            }

            // constructors
            public MgStringBuilder()
            {
                StringBuilder = StringBuilder;
                if (sb == null) { sb = new StringBuilder(); }
            }
            public MgStringBuilder(int capacity)
            {
                StringBuilder = new StringBuilder(capacity);
                if (sb == null) { sb = new StringBuilder(); }
            }
            public MgStringBuilder(StringBuilder sb)
            {
                StringBuilder = sb;
                if (sb == null) { sb = new StringBuilder(); }
            }
            public MgStringBuilder(string s)
            {
                StringBuilder = new StringBuilder(s);
                if (sb == null) { sb = new StringBuilder(); }
            }

            public static void CheckSeperator()
            {
                decimalseperator = Convert.ToChar(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
            }

            // operators
            public static implicit operator MgStringBuilder(StringBuilder sb)
            {
                return new MgStringBuilder(sb);
            }
            public static implicit operator StringBuilder(MgStringBuilder msb)
            {
                return msb.StringBuilder;
            }

            public static MgStringBuilder operator +(MgStringBuilder sbm, MgStringBuilder s)
            {
                sbm.StringBuilder.Append(s);
                return sbm;
            }
            //test
            public static MgStringBuilder operator +(MgStringBuilder sbm, string s)
            {
                sbm.StringBuilder.Append(s);
                return sbm;
            }

            public void AppendAt(int index, StringBuilder s)
            {
                int len = this.StringBuilder.Length;
                int reqcapacity = (index + 1 + s.Length) - this.StringBuilder.Capacity;
                if (reqcapacity > 0)
                    this.StringBuilder.Capacity += reqcapacity;

                int initialLength = StringBuilder.Length;
                // If we append near the end we can run out of space in the for loop. 
                // Make sure we allot enough.
                if (StringBuilder.Length < index + s.Length)
                    StringBuilder.Length = index + s.Length;

                // If our appendAt is outside the scope of our current sb's length. We will add spaces.
                if (index > initialLength - 1)
                {
                    for (int j = initialLength - 1; j < index; j++)
                    {
                        StringBuilder[j] = ' ';
                    }
                }
                // perform the append
                for (int i = 0; i < s.Length; i++)
                {
                    this.StringBuilder[i + index] = (char)(s[i]);
                }
            }

            public MgStringBuilder Append(StringBuilder s)
            {
                int len = this.StringBuilder.Length;
                int reqcapacity = (s.Length + len) - this.StringBuilder.Capacity;
                //int reqcapacity = (s.Length + len +1) - this.StringBuilder.Capacity;
                if (reqcapacity > 0)
                    this.StringBuilder.Capacity += reqcapacity;

                this.StringBuilder.Length = len + s.Length;
                for (int i = 0; i < s.Length; i++)
                {
                    this.StringBuilder[i + len] = (char)(s[i]);
                }
                return this;
            }
            public MgStringBuilder Append(string s)
            {
                this.StringBuilder.Append(s);
                return this;
            }
            public MgStringBuilder Append(bool value)
            {
                this.StringBuilder.Append(value);
                return this;
            }
            public MgStringBuilder Append(byte value)
            {
                // basics
                int num = value;
                if (num == 0)
                {
                    sb.Append('0');
                    return this;
                }
                int place = 100;
                if (num >= place * 10)
                {
                    // just append it
                    sb.Append(num);
                    return this;
                }
                // part 1 pull integer digits
                bool addzeros = false;
                while (place > 0)
                {
                    if (num >= place)
                    {
                        addzeros = true;
                        int modulator = place * 10;
                        int val = num % modulator;
                        int dc = val / place;
                        sb.Append((char)(dc + 48));
                    }
                    else
                    {
                        if (addzeros) { sb.Append('0'); }
                    }
                    place = (int)(place * .1);
                }
                return this;
            }
            public MgStringBuilder Append(short value)
            {
                int num = value;
                // basics
                if (num < 0)
                {
                    // Negative.
                    sb.Append(minus);
                    num = -num;
                }
                if (value == 0)
                {
                    sb.Append('0');
                    return this;
                }

                int place = 10000;
                if (num >= place * 10)
                {
                    // just append it, if its this big, this isn't a science calculator, its a edge case.
                    sb.Append(num);
                    return this;
                }
                // part 1 pull integer digits
                bool addzeros = false;
                while (place > 0)
                {
                    if (num >= place)
                    {
                        addzeros = true;
                        int modulator = place * 10;
                        int val = num % modulator;
                        int dc = val / place;
                        sb.Append((char)(dc + 48));
                    }
                    else
                    {
                        if (addzeros) { sb.Append('0'); }
                    }
                    place = (int)(place * .1);
                }
                return this;
            }
            public MgStringBuilder Append(int value)
            {
                // basics
                if (value < 0)
                {
                    // Negative.
                    sb.Append(minus);
                    value = -value;
                }
                if (value == 0)
                {
                    sb.Append('0');
                    return this;
                }

                int place = 1000000000;
                if (value >= place * 10)
                {
                    // just append it
                    sb.Append(value);
                    return this;
                }
                // part 1 pull integer digits
                int n = (int)(value);
                bool addzeros = false;
                while (place > 0)
                {
                    if (n >= place)
                    {
                        addzeros = true;
                        int modulator = place * 10;
                        int val = n % modulator;
                        int dc = val / place;
                        sb.Append((char)(dc + 48));
                    }
                    else
                    {
                        if (addzeros) { sb.Append('0'); }
                    }
                    place = (int)(place * .1);
                }
                return this;
            }
            public MgStringBuilder Append(long value)
            {
                // basics
                if (value < 0)
                {
                    // Negative.
                    sb.Append(minus);
                    value = -value;
                }
                if (value == 0)
                {
                    sb.Append('0');
                    return this;
                }

                long place = 10000000000000000L;
                if (value >= place * 10)
                {
                    // just append it,
                    sb.Append(value);
                    return this;
                }
                // part 1 pull integer digits
                long n = (long)(value);
                bool addzeros = false;
                while (place > 0)
                {
                    if (n >= place)
                    {
                        addzeros = true;
                        long modulator = place * 10L;
                        long val = n % modulator;
                        long dc = val / place;
                        sb.Append((char)(dc + 48));
                    }
                    else
                    {
                        if (addzeros) { sb.Append('0'); }
                    }
                    place = (long)(place * .1);
                }
                return this;
            }

            public MgStringBuilder Append(float value)
            {
                // basics
                bool addZeros = false;
                int n = (int)(value);
                int place = 100000000;
                if (value < 0)
                {
                    // Negative.
                    sb.Append(minus);
                    value = -value;
                    n = -n;
                }
                if (value == 0)
                {
                    sb.Append('0');
                    return this;
                }
                // fix march 18-17
                // values not zero value is at least a integer
                if (value <= -1f || value >= 1f)
                {

                    place = 100000000;
                    if (value >= place * 10)
                    {
                        // just append it, if its this big its a edge case.
                        sb.Append(value);
                        return this;
                    }
                    // part 1 pull integer digits
                    // int n =  // moved
                    addZeros = false;
                    while (place > 0)
                    {
                        if (n >= place)
                        {
                            addZeros = true;
                            int modulator = place * 10;
                            int val = n % modulator;
                            int dc = val / place;
                            sb.Append((char)(dc + 48));
                        }
                        else
                        {
                            if (addZeros) { sb.Append('0'); }
                        }
                        place = (int)(place * .1);
                    }
                }
                else
                    sb.Append('0');

                sb.Append(decimalseperator);

                // part 2 

                // floating point part now it can have about 28 digits but uh ya.. nooo lol
                place = 1000000;
                // pull decimal to integer digits, based on the number of place digits
                int dn = (int)((value - (float)(n)) * place * 10);
                // ... march 17 testing... cut out extra zeros case 1
                if (dn == 0)
                {
                    sb.Append('0');
                    return this;
                }
                addZeros = true;
                while (place > 0)
                {
                    if (dn >= place)
                    {
                        //addzeros = true;
                        int modulator = place * 10;
                        int val = dn % modulator;
                        int dc = val / place;
                        sb.Append((char)(dc + 48));
                        if (val - dc * place == 0) // && trimEndZeros this would be a acstetic
                        {
                            return this;
                        }
                    }
                    else
                    {
                        if (addZeros) { sb.Append('0'); }
                    }
                    place = (int)(place * .1);
                }
                return this;
            }

            public MgStringBuilder Append(double value)
            {
                // basics
                bool addZeros = false;
                long n = (long)(value);
                long place = 10000000000000000L;
                if (value < 0) // is Negative.
                {
                    sb.Append(minus);
                    value = -value;
                    n = -n;
                }
                if (value == 0) // is Zero
                {
                    sb.Append('0');
                    return this;
                }
                if (value <= -1d || value >= 1d) // is a Integer
                {
                    if (value >= place * 10)
                    {
                        sb.Append(value); // is big, just append its a edge case.
                        return this;
                    }
                    // part 1 pull integer digits
                    addZeros = false;
                    while (place > 0)
                    {
                        if (n >= place)
                        {
                            addZeros = true;
                            long modulator = place * 10;
                            long val = n % modulator;
                            long dc = val / place;
                            sb.Append((char)(dc + 48));
                        }
                        else
                            if (addZeros) { sb.Append('0'); }

                        place = (long)(place * .1d);
                    }
                }
                else
                    sb.Append('0');

                sb.Append(decimalseperator);

                // part 2 
                // floating point part now it can have about 28 digits but uh ya.. nooo lol
                place = 1000000000000000L;
                // pull decimal to integer digits, based on the number of place digits
                long dn = (long)((value - (double)(n)) * place * 10);
                if (dn == 0)
                {
                    sb.Append('0');
                    return this;
                }
                addZeros = true;
                while (place > 0)
                {
                    if (dn >= place)
                    {
                        long modulator = place * 10;
                        long val = dn % modulator;
                        long dc = val / place;
                        sb.Append((char)(dc + 48));
                        if (val - dc * place == 0) // && trimEndZeros  aectetic
                        {
                            return this;
                        }
                    }
                    else
                        if (addZeros) { sb.Append('0'); }

                    place = (long)(place * .1);
                }
                return this;
            }

            public MgStringBuilder Append(Vector2 value)
            {
                Append("(");
                Append(value.X);
                Append(", ");
                Append(value.Y);
                Append(")");
                return this;
            }
            public MgStringBuilder Append(Vector3 value)
            {
                Append("(");
                Append(value.X);
                Append(", ");
                Append(value.Y);
                Append(", ");
                Append(value.Z);
                Append(")");
                return this;
            }
            public MgStringBuilder Append(Vector4 value)
            {
                Append("(");
                Append(value.X);
                Append(", ");
                Append(value.Y);
                Append(", ");
                Append(value.Z);
                Append(", ");
                Append(value.W);
                Append(")");
                return this;
            }
            public MgStringBuilder Append(Color value)
            {
                Append("(");
                Append(value.R);
                Append(", ");
                Append(value.G);
                Append(", ");
                Append(value.B);
                Append(", ");
                Append(value.A);
                Append(")");
                return this;
            }

            public MgStringBuilder AppendTrim(float value)
            {
                // basics
                bool addZeros = false;
                int n = (int)(value);
                int place = 100000000;
                if (value < 0)
                {
                    // Negative.
                    sb.Append(minus);
                    value = -value;
                    n = -n;
                }
                if (value == 0)
                {
                    sb.Append('0');
                    return this;
                }
                // fix march 18-17
                // values not zero value is at least a integer
                if (value <= -1f || value >= 1f)
                {

                    place = 100000000;
                    if (value >= place * 10)
                    {
                        // just append it, if its this big its a edge case.
                        sb.Append(value);
                        return this;
                    }
                    // part 1 pull integer digits
                    // int n =  // moved
                    addZeros = false;
                    while (place > 0)
                    {
                        if (n >= place)
                        {
                            addZeros = true;
                            int modulator = place * 10;
                            int val = n % modulator;
                            int dc = val / place;
                            sb.Append((char)(dc + 48));
                        }
                        else
                        {
                            if (addZeros) { sb.Append('0'); }
                        }
                        place = (int)(place * .1);
                    }
                }
                else
                    sb.Append('0');

                sb.Append(decimalseperator);

                // part 2 

                // floating point part now it can have about 28 digits but uh ya.. nooo lol
                place = 100;
                // pull decimal to integer digits, based on the number of place digits
                int dn = (int)((value - (float)(n)) * place * 10);
                // ... march 17 testing... cut out extra zeros case 1
                if (dn == 0)
                {
                    sb.Append('0');
                    return this;
                }
                addZeros = true;
                while (place > 0)
                {
                    if (dn >= place)
                    {
                        //addzeros = true;
                        int modulator = place * 10;
                        int val = dn % modulator;
                        int dc = val / place;
                        sb.Append((char)(dc + 48));
                        if (val - dc * place == 0) // && trimEndZeros this would be a acstetic
                        {
                            return this;
                        }
                    }
                    else
                    {
                        if (addZeros) { sb.Append('0'); }
                    }
                    place = (int)(place * .1);
                }
                return this;
            }

            public MgStringBuilder AppendTrim(double value)
            {
                // basics
                bool addZeros = false;
                long n = (long)(value);
                long place = 10000000000000000L;
                if (value < 0) // is Negative.
                {
                    sb.Append(minus);
                    value = -value;
                    n = -n;
                }
                if (value == 0) // is Zero
                {
                    sb.Append('0');
                    return this;
                }
                if (value <= -1d || value >= 1d) // is a Integer
                {
                    if (value >= place * 10)
                    {
                        sb.Append(value); // is big, just append its a edge case.
                        return this;
                    }
                    // part 1 pull integer digits
                    addZeros = false;
                    while (place > 0)
                    {
                        if (n >= place)
                        {
                            addZeros = true;
                            long modulator = place * 10;
                            long val = n % modulator;
                            long dc = val / place;
                            sb.Append((char)(dc + 48));
                        }
                        else
                            if (addZeros) { sb.Append('0'); }

                        place = (long)(place * .1);
                    }
                }
                else
                    sb.Append('0');

                sb.Append(decimalseperator);

                // part 2 
                // floating point part now it can have about 28 digits but uh ya.. nooo lol
                place = 100L;
                // pull decimal to integer digits, based on the number of place digits
                long dn = (long)((value - (double)(n)) * place * 10);
                if (dn == 0)
                {
                    sb.Append('0');
                    return this;
                }
                addZeros = true;
                while (place > 0)
                {
                    if (dn >= place)
                    {
                        long modulator = place * 10;
                        long val = dn % modulator;
                        long dc = val / place;
                        sb.Append((char)(dc + 48));
                        if (val - dc * place == 0) // && trimEndZeros  aectetic
                        {
                            return this;
                        }
                    }
                    else
                        if (addZeros) { sb.Append('0'); }

                    place = (long)(place * .1);
                }
                return this;
            }

            public MgStringBuilder AppendTrim(Vector2 value)
            {
                Append("(");
                AppendTrim(value.X);
                Append(", ");
                AppendTrim(value.Y);
                Append(")");
                return this;
            }
            public MgStringBuilder AppendTrim(Vector3 value)
            {
                Append("(");
                AppendTrim(value.X);
                Append(", ");
                AppendTrim(value.Y);
                Append(", ");
                AppendTrim(value.Z);
                Append(")");
                return this;
            }
            public MgStringBuilder AppendTrim(Vector4 value)
            {
                Append("(");
                AppendTrim(value.X);
                Append(", ");
                AppendTrim(value.Y);
                Append(", ");
                AppendTrim(value.Z);
                Append(", ");
                AppendTrim(value.W);
                Append(")");
                return this;
            }

            public void AppendLine(StringBuilder s)
            {
                sb.AppendLine();
                Append(s);
            }
            public void AppendLine(string s)
            {
                sb.AppendLine();
                sb.Append(s);
            }
            public MgStringBuilder AppendLine()
            {
                sb.AppendLine();
                return this;
            }

            public MgStringBuilder Insert(int index, StringBuilder s)
            {
                this.StringBuilder.Insert(index, s);
                return this;
            }
            public MgStringBuilder Remove(int index, int length)
            {
                this.StringBuilder.Remove(index, length);
                return this;
            }

            public char[] ToCharArray()
            {
                char[] a = new char[sb.Length];
                sb.CopyTo(0, a, 0, sb.Length);
                return a;
            }
            public override string ToString()
            {
                return sb.ToString();
            }
        }
       }

(Note)

This is a performance related class. If you use really long decimal place numbers like over 10 decimal digits it can create garbage so keep it sane or change the methods themselves in the class to compensate.

While this is a old pic i didn’t feel like making a new one it still generates zero garbage in a basically bare monogame project which just shows that mg itself is pretty free of collections. Well at least till you add the mouse and keyboard stuff as of the time of this posting.

The original post can be found here.

1 Like

Are you planning adding garbage-free Format? I’d really love that!

When this came out, it was a thrill on 360:

but for some reason after NET 4 iirc it stopped working and couldn’t get time to find out why. (actually I had forgotten about it until I read your message)

I have no plans at the moment to do so but your welcome to take a shot at it.

As for this article and the others that go with it.

XNA/C# – A garbage-free StringBuilder Format() method

Im familiar with his work. I tested his class extensions and found they were faulty. Much of his assessment of what was ‘not’ creating garbage was incorrect. So his class extensions never fully worked.

Pretty much all numeric value → to string conversions create garbage in c#.

Internally some character is new’ed then goes out of scope deep down within ToString itself. There is nothing you can do about that, other then simply not calling ToString or allowing it to be invoked implicitly, hence this class and why it does work. I decode the binary representation into a character one digit at a time to string is out of the loop. Provided the stringbuilder isn’t new’ed over and over instead of cleared. As well the object remains in scope to the life of the application your good.

Well i finally put this on github i figure i should link these old posts to that.
As i update that and i have like 3 of these posts scattered around here.