Skip to main content

C# class that provides a cached reusable instance of a StringBuilder per thread. It is an optimization that reduces the number of instances constructed and collected.

// ------------------------------------------------------------
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//  Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

// For usage, see: https://github.com/Azure/diagnostics-eventflow/blob/9497940981846432ff179683fcb5ff9699873574/src/Microsoft.Diagnostics.EventFlow.EtwUtilities/ActivityPathDecoder.cs

using System;
using System.Text;

namespace Microsoft.Diagnostics.EventFlow.Utilities.Etw
{
    /// <summary>
    /// Provides a cached reusable instance of a StringBuilder per thread. It is an optimization that reduces the number of instances constructed and collected.
    /// </summary>
    internal static class StringBuilderCache
    {
        // The value 360 was chosen in discussion with performance experts as a compromise between using
        // as litle memory (per thread) as possible and still covering a large part of short-lived
        // StringBuilder creations.
        private const int MaxBuilderSize = 360;

        [ThreadStatic]
        private static StringBuilder cachedInstance;

        /// <summary>
        /// Gets a string builder to use of a particular size.
        /// </summary>
        /// <param name="capacity">Initial capacity of the requested StringBuilder.</param>
        /// <returns>An instance of a StringBuilder.</returns>
        /// <remarks>
        /// It can be called any number of times. If a StringBuilder is in the cache then it will be returned and the cache emptied.
        /// A StringBuilder instance is cached in Thread Local Storage and so there is one per thread.
        /// Subsequent calls will return a new StringBuilder.
        /// </remarks>
        public static StringBuilder Acquire(int capacity = 16 /*StringBuilder.DefaultCapacity*/)
        {
            if (capacity <= MaxBuilderSize)
            {
                StringBuilder sb = StringBuilderCache.cachedInstance;
                if (sb != null)
                {
                    // Avoid stringbuilder block fragmentation by getting a new StringBuilder
                    // when the requested size is larger than the current capacity
                    if (capacity <= sb.Capacity)
                    {
                        StringBuilderCache.cachedInstance = null;
                        sb.Clear();
                        return sb;
                    }
                }
            }
            return new StringBuilder(capacity);
        }

        /// <summary>
        /// Place the specified builder in the cache if it is not too big.
        /// </summary>
        /// <param name="sb">StringBuilder that is no longer used.</param>
        /// <remarks>
        /// The StringBuilder should not be used after it has been released. Unbalanced Releases are perfectly acceptable.
        /// It will merely cause the runtime to create a new StringBuilder next time Acquire is called.
        /// </remarks>
        public static void Release(StringBuilder sb)
        {
            if (sb.Capacity <= MaxBuilderSize)
            {
                StringBuilderCache.cachedInstance = sb;
            }
        }

        /// <summary>
        /// Gets the resulting string and releases a StringBuilder instance.
        /// </summary>
        /// <param name="sb">StringBuilder to be released.</param>
        /// <returns>The output of the <paramref name="sb"/> StringBuilder.</returns>
        public static string GetStringAndRelease(StringBuilder sb)
        {
            string result = sb.ToString();
            Release(sb);
            return result;
        }
    }
}

// ============================================================================
// Example 2 from
// https://github.com/opserver/Opserver/blob/master/Opserver.Core/StringBuilderCache.cs
// ============================================================================

using System;
using System.Text;
using System.Threading;

namespace StackExchange.Opserver
{
    /// <summary>
    /// Provides optimized access to StringBuilder instances
    /// Credit: Marc Gravell (@marcgravell), Stack Exchange Inc.
    /// </summary>
    public static class StringBuilderCache
    {
        // one per thread
        [ThreadStatic]
        private static StringBuilder _perThread;
        // and one secondary that is shared between threads
        private static StringBuilder _shared;

        private const int DefaultCapacity = 0x10;

        /// <summary>
        /// Obtain a StringBuilder instance; this could be a recycled instance, or could be new
        /// </summary>
        /// <param name="capacity">The capaity to start the fetched <see cref="StringBuilder"/> at.</param>
        public static StringBuilder Get(int capacity = DefaultCapacity)
        {
            var tmp = _perThread;
            if (tmp != null)
            {
                _perThread = null;
                tmp.Length = 0;
                return tmp;
            }

            tmp = Interlocked.Exchange(ref _shared, null);
            if (tmp == null) return new StringBuilder(capacity);
            tmp.Length = 0;
            return tmp;
        }

        /// <summary>
        /// Get the string contents of a StringBuilder and recyle the instance at the same time
        /// </summary>
        /// <param name="builder">The <see cref="StringBuilder"/> to recycle.</param>
        public static string ToStringRecycle(this StringBuilder builder)
        {
            var s = builder.ToString();
            Recycle(builder);
            return s;
        }

        /// <summary>
        /// Get the string contents of a StringBuilder and recycle the instance at the same time
        /// </summary>
        /// <param name="builder">The <see cref="StringBuilder"/> to recycle.</param>
        /// <param name="startIndex">The index to start at.</param>
        /// <param name="length">The amount of characters to get.</param>
        public static string ToStringRecycle(this StringBuilder builder, int startIndex, int length)
        {
            var s = builder.ToString(startIndex, length);
            Recycle(builder);
            return s;
        }

        /// <summary>
        /// Recycles a StringBuilder instance if possible
        /// </summary>
        /// <param name="builder">The <see cref="StringBuilder"/> to recycle.</param>
        public static void Recycle(StringBuilder builder)
        {
            if (builder == null) return;
            if (_perThread == null)
            {
                _perThread = builder;
            }
            else
            {
                Interlocked.CompareExchange(ref _shared, builder, null);
            }
        }
    }
}