C# - 传递参数以避免 GC 卡顿的正确方法?[关闭]

C# - Correct way to pass parameters to avoid GC stutters? [closed]

提问人:Priyansh Yadav 提问时间:11/7/2023 更新时间:11/7/2023 访问量:89

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章用事实和引文来回答。

12天前关闭。

(我发现了一些相关的问题,但它们并不完全相同,也不是十年前的。

因此,C# 中的参数是作为值还是引用传递。

说我愿意,

static void main()
{
    string name = "hello world";
    Console.WriteLine(name);
    testfunc(name);
    Console.WriteLine(name);
}

void testfunc(string name)
{
    name = "stackoverflow";
    Console.WriteLine(name);
}

#Output
=> hello world
=> stackoverflow
=> hello world

因此,在此参数中作为值传递,不再与原始变量(在内存中)相关联。我想这意味着变量被复制到内存中的新位置,然后传递(这涉及分配、复制,然后传递给函数)。

然后我们这样做:

static void main()
{
    string name = "hello world";
    Console.WriteLine(name);
    testfunc(ref name);
    Console.WriteLine(name);
}

void testfunc(ref string name)
{
    name = "stackoverflow";
    Console.WriteLine(name);
}

#Output
=> hello world
=> stackoverflow
=> stackoverflow

因此,在这种情况下,我猜原始变量引用/指针已传递到函数中,并且没有分配或复制新的内存/数据。

所以,我在想第一种方法是否有一些开销(对于数据 >= 10MB,它肯定有开销)。但是第二种方法是不安全的,因为它可能会损坏原始数据。

那么,我们应该做什么,或者我们可以采取什么样的优化来克服第一种方法(如果有的话)的开销?对于从函数返回的值也同样有效。

**我的用例:** 我正在用 Unity 开发一款游戏。我必须创造一个世界。为此,我创建了 3D 噪声图(非常大,湿度、文明、地形等也是多个)。它们总共占用大约 70-80 MB 的内存。现在,我把它们全部传递给一个函数,在那里它们被组合在一起,最终的世界就生成了。因此,如果在内存中复制 100 MB 的数据(如上面的第一种方法),那么我认为对于具有 <= 4GB 内存的机器来说,情况不会很好。 至于从函数返回的数据,假设我有一个 25 MB 的保存文件(一些游戏(如 RimWorld 等游戏有这么大的保存文件)。我调用该函数(它读取文件,将其转换为变量并将其存储在变量中,然后返回它)。此变量的大小应约为 25-28 MB。因此,如果返回的数据也是重复的,那么对于如此大的数据来说是不利的。loadSaveFile()SaveClasssaveDatasaveData

我知道重复的数据最终会被 GC 销毁,但当 GC 销毁它时,这会使该特定帧的帧速率停滞不前。这可以通过使用增量 GC 来解决,但仍然会造成内存膨胀(因为创建和销毁重复数据的那几秒钟会影响机器的整体性能)。

所以,最后一个问题是:将大数据作为参数传递给函数并接收大数据作为返回值的正确、最佳和最实用的方法是什么?

C# unity-游戏引擎 垃圾回收 参数传递 游戏开发

评论


答:

-1赞 Saurabh 11/7/2023 #1

在 C# 中,参数默认按值传递。这意味着,当您将参数传递给方法时,值(而不是引用)的副本将传递给该方法,并且对方法中的参数所做的任何更改都不会影响传递的原始变量。这是您在第一个代码示例中观察到的行为。

在第一个代码示例中,您有:

static void Main()
{
    string name = "hello world";
    Console.WriteLine(name);
    testfunc(name);
    Console.WriteLine(name);
}

void testfunc(string name)
{
    name = "stackoverflow";
    Console.WriteLine(name);
}

输出显示,对 testfunc 方法中的 name 变量所做的更改不会影响 Main 方法中的原始 name 变量。这是因为 testfunc 中的参数名称是原始值的单独副本。

在第二个代码示例中,使用 ref 关键字通过引用传递参数:

    static void Main()
    {
        string name = "hello world";
        Console.WriteLine(name);
        testfunc(ref name);
        Console.WriteLine(name);
    }

void testfunc(ref string name)
{
    name = "stackoverflow";
    Console.WriteLine(name);
}

使用 ref 时,将传递对原始变量的引用,并且对 testfunc 方法中的参数名称所做的任何更改都将影响 Main 方法中的原始名称变量。这就是您在输出中看到修改后的值的原因。

关于您对 GC(垃圾回收)卡顿的担忧,您传递参数的方式(按值或按引用)通常不会对垃圾回收产生重大影响。垃圾回收主要涉及回收不再使用的内存,它与方法参数的传递方式无关。

如果您担心最大程度地减少应用程序中的 GC 暂停,则应关注其他因素,例如管理对象生存期、避免不必要的对象分配以及使用适当的数据结构和模式来降低内存压力。按值传递与按引用传递不是管理 GC 行为的主要因素。

1赞 JonasH 11/7/2023 #2

对于 GC 来说,您作为参数传递的内容并不重要。参数以值或引用的形式传递到堆栈上,并自动清理。

要避免的是分配大型短期对象,通常是大型数组。

第一步应该是分析您的应用程序。你有GC口吃吗?如果没有,请不要担心。如果遇到问题,请使用分析器和/或基准测试来检查原因。您创建的少数对象可能影响非常小。但有一些经验法则:

  1. 对小对象使用值类型,即结构。使它们成为只读的,如果大于 8 个字节,请考虑使用前缀传递它们。这将通过引用传递值,但会阻止任何更改。in
  2. 使用集合时,请尽可能使用。这在保持性能的同时提供了极大的灵活性。Span<T>
  3. 对象应该永远理想地存在,即直到有 Gen 2 GC 的好机会。或者在 gen 0/1 中有效地收集一小段时间,但如果是这种情况,请考虑改用值类型。
  4. 避免装箱,即用于引用值类型。object
  5. 你也可以对更大和可变的对象使用结构,但你真的需要注意确切的语言规则,以避免复制和性能不佳。这可能会导致代码更难阅读。
  6. 优化通常会使代码更难阅读,因此请将它们保留在实际需要优化的位置。

void testfunc(ref 字符串名称)

请不要这样做,它没有任何优势,并且暗示您可能会用其他东西替换该值,这应该是相当罕见的事情。 “纯”方法往往更容易阅读和理解。

0赞 Javad J 11/7/2023 #3

在 C# 方法调用中,当引用类型传递给它们时,它们应与其引用一起传递。

但是字符串有点棘手。

字符串也是一种引用类型,但此行为是由其不可变的来源引起的。

StringBuilder 是一个可变的字符串类,它具有纯引用类型行为,当它通过方法调用时应该是预期的。

static void Main()
{
    StringBuilder name = new StringBuilder("hello world");
    Console.WriteLine(name);
    testfunc(name);
    Console.WriteLine(name);
}

static void testfunc(StringBuilder name)
{
    name.Replace("world","stackoverflow");
    Console.WriteLine(name);
}

#Output
=> hello world
=> hello stackoverflow
=> hello stackoverflow