7

我有一个应用程序,我将日志字符串保存在循环缓冲区中。当日志已满时,对于每个新插入,旧字符串将被释放以进行垃圾收集,然后它们在第 2 代内存中。因此,最终会发生第 2 代 GC,我想避免这种情况。

我试图将字符串编组为结构。令人惊讶的是,我仍然得到了第 2 代 GC:s。似乎结构仍然保留对字符串的一些引用。下面完成控制台应用程序。任何帮助表示赞赏。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {

        [StructLayout(LayoutKind.Sequential)]
        public struct FixedString
        {
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
            private string str;

            public FixedString(string str)
            {
                this.str = str;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct UTF8PackedString
        {
            private int length;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
            private byte[] str;

            public UTF8PackedString(int length)
            {
                this.length = length;
                str = new byte[length];
            }

            public static implicit operator UTF8PackedString(string str)
            {
                var obj = new UTF8PackedString(Encoding.UTF8.GetByteCount(str));
                var bytes = Encoding.UTF8.GetBytes(str);
                Array.Copy(bytes, obj.str, obj.length);
                return obj;
            }
        }

        const int BufferSize = 1000000;
        const int LoopCount = 10000000;

        static void Main(string[] args)
        {
            Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
                "Type".PadRight(20), "Time", "GC(0)", "GC(1)", "GC(2)");
            Console.WriteLine();
            for (int i = 0; i < 5; i++)
            {
                TestPerformance<string>(s => s);
                TestPerformance<FixedString>(s => new FixedString(s));
                TestPerformance<UTF8PackedString>(s => s);
                Console.WriteLine();
            }
            Console.ReadKey();
        }

        private static void TestPerformance<T>(Func<string, T> func)
        {
            var buffer = new T[BufferSize];
            GC.Collect(2);
            Stopwatch stopWatch = new Stopwatch();
            var initialCollectionCounts = new int[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) };
            stopWatch.Reset();
            stopWatch.Start();
            for (int i = 0; i < LoopCount; i++)
                buffer[i % BufferSize] = func(i.ToString());
            stopWatch.Stop();
            Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
                typeof(T).Name.PadRight(20),
                stopWatch.ElapsedMilliseconds,
                (GC.CollectionCount(0) - initialCollectionCounts[0]),
                (GC.CollectionCount(1) - initialCollectionCounts[1]),
                (GC.CollectionCount(2) - initialCollectionCounts[2])
            );
        }
    }
}

编辑:使用 UnsafeFixedString 更新代码,完成所需工作:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        public unsafe struct UnsafeFixedString
        {
            private int length;

            private fixed char str[256];

            public UnsafeFixedString(int length)
            {
                this.length = length;
            }

            public static implicit operator UnsafeFixedString(string str)
            {
                var obj = new UnsafeFixedString(str.Length);
                for (int i = 0; i < str.Length; i++)
                    obj.str[i] = str[i];                
                return obj;
            }
        }

        const int BufferSize = 1000000;
        const int LoopCount = 10000000;

        static void Main(string[] args)
        {
            Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
                "Type".PadRight(20), "Time", "GC(0)", "GC(1)", "GC(2)");
            Console.WriteLine();
            for (int i = 0; i < 5; i++)
            {
                TestPerformance(s => s);
                TestPerformance<UnsafeFixedString>(s => s);
                Console.WriteLine();
            }
            Console.ReadKey();
        }

        private static void TestPerformance<T>(Func<string, T> func)
        {
            var buffer = new T[BufferSize];
            GC.Collect(2);
            Stopwatch stopWatch = new Stopwatch();
            var initialCollectionCounts = new int[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) };
            stopWatch.Reset();
            stopWatch.Start();
            for (int i = 0; i < LoopCount; i++)
                buffer[i % BufferSize] = func(String.Format("{0}", i));
            stopWatch.Stop();
            Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
                typeof(T).Name.PadRight(20),
                stopWatch.ElapsedMilliseconds,
                (GC.CollectionCount(0) - initialCollectionCounts[0]),
                (GC.CollectionCount(1) - initialCollectionCounts[1]),
                (GC.CollectionCount(2) - initialCollectionCounts[2])
            );
        }
    }
}

我电脑上的输出是:

Type                    Time    GC(0)   GC(1)   GC(2)

String                  5746    160     71      19
UnsafeFixedString       5345    418     0       0
4

4 回答 4

8

structstring字段的 a 在这里会有所不同,这不足为奇:string字段始终只是对托管堆上对象的引用——特别是string某处的对象。将string仍然存在并且最终仍然会导致 GC2。

“解决”这个问题的唯一方法是根本不把它作为一个对象;并且做到这一点的唯一方法(不完全超出托管内存)是使用fixed缓冲区:

public unsafe struct FixedString
{
    private fixed char str[100];
}

在这里,每个结构实例FixedString都有 200 个字节为数据保留。str只是一个相对偏移量,char*它标志着这个保留的开始。但是,使用它很棘手 - 并且需要unsafe自始至终的代码。另请注意,FixedString无论您实际要存储 3 个字符还是 170 个字符,每个都保留相同数量的空间。为避免内存问题,您需要使用空终止符,或单独存储有效负载长度。

请注意,在 .NET 4.5 中,该<gcAllowVeryLargeObjects>支持使得拥有此类值(FixedString[]例如 a )的大小合适的数组成为可能 - 但请注意,您不想经常复制数据。为避免这种情况,您希望始终在数组中留出空闲空间(因此您不会复制整个数组只是为了添加一个项目),并通过 处理单个项目ref,即

FixedString[] data = ...
int index = ...
ProcessItem(ref data[index]);

void ProcessItem(ref FixedString item) {
    // ...
}

这里item直接与数组中的元素对话——我们在任何时候都没有复制数据。

现在我们只有一个对象——数组本身。

于 2013-09-23T11:10:36.617 回答
2
    const int BufferSize = 1000000;

您的缓冲区太大了,因此能够将字符串引用存储太久,并允许将它们提升到 gen#1 之后。试验缓冲区大小提供了这个解决方案:

    const int BufferSize = 180000;

没有更多的 GC(2) 集合。

您可以从中推断出 gen#1 堆大小。尽管这个测试程序很难做到,但字符串的大小太可变了。无论如何,在真正的应用程序中都需要手动调整。

于 2013-09-23T12:36:14.877 回答
1

虽然我喜欢 Marc Gravell 和 Hans Passant 的答案(一如既往)......

您可以微调 GC 以同时运行,从而避免冻结时间。在这里阅读

于 2013-09-23T12:46:14.383 回答
0

使用StringBuilders 的缓冲区本质上与该方法完全相同unsafe fixed char[]。但是给你一个超出你最初分配的特定字符串长度的潜在灵活性(当然,是的,这会导致那个字符串,或者更准确地说是底层char[]StringBuilder资格进行垃圾收集,但让我们实际一点)。此外,您不必进行自己的字符串长度管理。

private static void TestPerformance2()
{
    var buffer = new StringBuilder[BufferSize];
    // Initialize each item of the array.  This is no different than what
    // unsafe struct is.
    for (int i = 0; i < BufferSize; i++)
    {
        buffer[i] = new StringBuilder(256);
    }

    GC.Collect(2);
    Stopwatch stopWatch = new Stopwatch();
    var initialCollectionCounts = new int[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) };
    stopWatch.Reset();
    stopWatch.Start();
    for (int i = 0; i < LoopCount; i++)
    {
        buffer[i % BufferSize].Clear(); // Or use .Length = 0;, which is what the Clear() method does internally.

        buffer[i % BufferSize].AppendFormat("{0}", i);
    }
    stopWatch.Stop();
    Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
        typeof(StringBuilder).Name.PadRight(20),
        stopWatch.ElapsedMilliseconds,
        (GC.CollectionCount(0) - initialCollectionCounts[0]),
        (GC.CollectionCount(1) - initialCollectionCounts[1]),
        (GC.CollectionCount(2) - initialCollectionCounts[2])
    );
}

结果是速度的两倍(您甚至可以将秒表向上移动以包含数组初始化,它仍然比 快UnsafeFixedString)。

Type                    Time    GC(0)   GC(1)   GC(2)

String                  4647    131     108     23
StringBuilder           2600    94      0       0
UnsafeFixedString       5135    161     0       0
于 2017-06-16T01:48:22.580 回答