This creates 28B of garbage every frame, how do I do proper conversion from int to string without creating any garbage? I have already scoured the internet, and there was a post that says a way to fix it, but it broke again in Unity 5. So I am using Unity 5, and cannot come up with a solution.
Strings are immutable. That means every time you create a new string, you must abandon the old one to the GC.
There are ways around it. The best is simply not to use and abandon strings in Update. If you were only to use ToString() when the int was changed your garbage would drop dramatically.
If you really want to avoid the string allocation you could create and reuse an array of char.
Also linking the post that supposedly fixed this would help. Most breaking changes between 4.x and 5.x were fairly simple to fix. Unless it relies on wheel physics.
I bet it was an entry similar to the one by Defective Studios that shows using StringBuilder. I havenât really examined or tried out the sample code, but the comments are stating something was changed that prevents the code from working in Unity 5.
Nah, that should still work in unity5. The mscorlib dll that StringBuilder comes from still implements its private fields the same way.
There is one downside, the string youâre extracting is supposed to be immutable. But StringBuilder is treating it as mutable.
How it does this is it reuses the block of memory that represents the string. When you call âToStringâ it actually pulls out the chunk of the string you really need. Iâll give an example:
using UnityEngine;
using System.Collections.Generic;
public class BlarghScript : MonoBehaviour {
void Start()
{
var sb = new System.Text.StringBuilder();
sb.Append("Hello World");
var str = GarbageFreeString(sb);
sb.Length = 0;
sb.Append("Good-bye");
Debug.Log(str);
}
public static string GarbageFreeString(System.Text.StringBuilder sb)
{
string str = (string)typeof(System.Text.StringBuilder).GetField(
"_str",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance).GetValue(sb);
return str;
}
}
We call âGarbageFreeStringâ before modifying the StringBuilder. So youâd think itâd contain âHello Worldâ, but it doesnât.
But its even weirder. We reset the length to 0, and then stuck âGood-byeâ in there. This is 3 characters shorter than âHello Worldâ. What actually gets written isâŠ
âGood-byerldâ
Yeah, the last 3 characters are still there. Because the block of memory that represented âHello Worldâ was just reused. StringBuilder just moved its head position to 0 on Length = 0, then started filling from there with Good-bye. Which didnât reach the end. So it still contains the chars for rld in it.
If you had called âGetStringâ, it would have extracted the substring from â_strâ that should have been used.
Well thatâs not good How about I know the size of the string will always be 3 characters or less. I could premake the length and cache it? So when I convert my â3â it only takes up 1 character.
Itâs going to be weird to pull such a thing off.
And it will have weird implications. Because youâre basically creating a mutable string. So what I described with StringBuilder is an issue. You can only use this string for its thing, and only its thing, and wherever itâs used it shouldnât care what the state of it is⊠and that it can change on a whim.
But yeah I for sure need a better solution that I can use everywhere in my code as I have high values like 10,000 to be converted. Obviously my way will not work.
public StaticString staticString = new StaticString(20);
staticString.Set (33);
Debug.Log ("VALUE: " + staticString.Value);
A bit late to the party haha. Itâs throwing that exception because it canât find the _str Field using reflection anymore. If you iterate through the StringBuilder fields using reflection you can find what the new field is called like this:
var fields = typeof(StringBuilder).GetFields(BindingFlags.NonPublic |
BindingFlags.Instance);
foreach (var field in fields)
{
Debug.Log($"field name {field.Name} type : {field.FieldType}");
}
Results in:
field name m_ChunkChars type : System.Char[ ]
youâll see that the field is now called m_ChunkChars. In the StaticString just changed _str â m_ChunkCars to get it working again.
Yeah, the problem with reflecting things out of encapsulated data types (classes) is that said encapsulation is supposed to allow the developers to modify the internals without support consequence because the way youâre supposed to access it is through the public interface.
The code above using â_strâ was basically a hack.
And as with many hacks, its support will be specific to the version youâre hacking. Future versions may change and the hack is no longer viable. Unity has since moved on to newer versions of .net, and therefore the underlying library that StringBuilder is from has changed.
Yep. To add to that, this code no longer works. Value is always returned as null as the cast from StringBuilder to string always falls/ returns null. Iâd recommend to anyone wanting to do something this is, itâs better to just used a fixed length char[ ] and TextMeshProGUI. TextMeshProGUI can take a char[ ] using
SetCharArray so you donât have to set it with a string using .text = myScore.ToString() for example. This gives the added benefit of saving additional string allocations inside TextMeshProGUI. Even states it in the old documentation TextMesh Pro Documentation:.
If anyone is interested, hereâs the same class just adjusted a fit to use char array, I havenât cleaned it up, just a quick hack.
public class StaticString
{
public enum CharAlignment
{
Left = 0,
Right = 1
}
#region CONSTRUCTOR
public StaticString(int size, char fillCharacter = '\0', CharAlignment alignment = CharAlignment.Left)
{
_buffer = new char[size];
_capacity = size;
Alignment = alignment;
FillCharacter = fillCharacter;
}
#endregion
#region Methods
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(int value)
{
const int CHAR_0 = '0';
var isNeg = value < 0;
// value = Math.Abs(value);
var cap = _capacity;
var log = (int)Math.Floor(Math.Log10(value));
var charCnt = log + (isNeg ? 2 : 1);
var blankCnt = cap - charCnt;
var currentCharIndex = 0;
switch (Alignment)
{
case CharAlignment.Left:
{
if (isNeg)
{
_buffer[0] = '-';
currentCharIndex++;
}
var min = Math.Max(charCnt - cap, 0);
for(var i = log; i >= min; i--)
{
var pow = (int)Math.Pow(10, i);
var digit = value / pow % 10;
_buffer[currentCharIndex++] = (char)(digit + CHAR_0);
}
for(var i = 0; i < blankCnt; i++)
{
_buffer[currentCharIndex++] = FillCharacter;
}
}
break;
case CharAlignment.Right:
{
for(var i = 0; i < blankCnt; i++)
{
_buffer[currentCharIndex++] = FillCharacter;
}
if (isNeg)
{
_buffer[currentCharIndex] = '-';
currentCharIndex++;
}
var min = Math.Max(charCnt - cap, 0);
for(var i = log; i >= min; i--)
{
var pow = (int)Math.Pow(10, i);
var digit = value / pow % 10;
_buffer[currentCharIndex++] = (char)(digit + CHAR_0);
}
}
break;
}
}
#endregion
#region Fields
private readonly char[] _buffer;
private int _capacity;
#endregion
#region Properties
// ReSharper disable once MemberCanBePrivate.Global
public CharAlignment Alignment
{
get;
set;
}
public string Value => new string(_buffer);
public char[] Buffer => _buffer;
// ReSharper disable once MemberCanBePrivate.Global
/// <summary>
/// The character to fill the remaining space with. For example, if you create a StaticString(6, "-")
/// It will contain 6 characters. If you set it too 123 with Alignment Left it will return "123---"
/// </summary>
public char FillCharacter { get; set; }
#endregion
}
Another performance tip for stuff like showing score etc in the UI. You donât really have to update it every frame. Maybe just update it 10 or 5 or whatever times a second.