[c#] Unity 如何禁用 GC

本文介绍了如何在Unity中禁用和重新启用垃圾收集器GC,通过C#调用Unity的内部函数。作者探讨了禁用GC的影响,包括内存使用和可能的性能问题,提醒开发者谨慎使用这项高级技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

英文原文:https://www.jacksondunstan.com/articles/4644

Unity 的 GC 一直是我们的眼中钉。我们不断通过池化对象、限制语言功能的使用和避免 API 来解决这个问题。我们甚至在加载屏幕上调用 GC.Collect,希望 GC 不会在游戏过程中运行。今天的文章更进一步,展示了如何完全禁用 GC,使其运行的可能性为零。当我们再次准备好时,我们还将了解如何重新打开它。

Unity 的 GC 是用 Unity 附带的 C 代码实现的。以下是截至 2017.3 的 Mac 安装位置:

/Applications/Unity/Unity.app/Contents/il2cpp/external/boehmgc/

在 Misc.c 文件中,我们在大约 1800 行下面看到了这些函数:

GC_API void GC_CALL GC_enable(void)
{
    DCL_LOCK_STATE;
 
    LOCK();
    GC_ASSERT(GC_dont_gc != 0); /* ensure no counter underflow */
    GC_dont_gc--;
    UNLOCK();
}
 
GC_API void GC_CALL GC_disable(void)
{
    DCL_LOCK_STATE;
    LOCK();
    GC_dont_gc++;
    UNLOCK();
}

这些都是非常简单的函数。它们只是增加一个整数 (GC_dont_gc),该整数用作是否启用 GC 的全局标志。如果为零,则 GC 已启用。如果它不为零,则 GC 被禁用。所以我们应该能够调用 GC_disable 来禁用 GC,然后调用 GC_enable 来重新启用它。

因此,让我们尝试使用 [DllImport] 属性从 C# 调用这些函数:

using System.Runtime.InteropServices;
 
public static class GcControl
{
	// Unity engine function to disable the GC
	[DllImport("__Internal")]
	public static extern void GC_disable();
 
	// Unity engine function to enable the GC
	[DllImport("__Internal")]
	public static extern void GC_enable();
}

现在我们可以从应用程序中的任何位置调用 GcControl.GC_disable() 或 GcControl.GC_enable() 来禁用和重新启用 GC。重要的是要记住,这些函数以类似于引用计数的方式递增和递减整数。因此,对 GC_disable 的调用应与对 GC_enable 的调用相匹配。

现在让我们尝试使用一个小应用程序来显示 GC 运行的次数以及用于打开和关闭 GC 的按钮。每一帧,我们都会在 byte[] 中分配 100 KB 的托管内存并保留它,这样内存压力就会不断增加并导致 GC 运行。

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;
 
class GCTestScript : MonoBehaviour
{
	// UI
	public Text gcCountText;
	public Text toggleButtonText;
 
	// GC status
	private bool isGcDisabled;
 
	// Holds allocated memory
	// Prevents GC from collecting it
	private List<byte[]> storage;
 
	void Awake()
	{
		storage = new List<byte[]>();
		toggleButtonText.text = "Disable GC";
	}
 
	void Update()
	{
		// Display how many times GC has run
		int gcCount = GC.CollectionCount(0);
		gcCountText.text = gcCount.ToString();
 
		// Add managed memory pressure
		storage.Add(new byte[100*1024]);
	}
 
	// Toggle GC on or off
	public void OnToggle()
	{
		isGcDisabled = !isGcDisabled;
		if (isGcDisabled)
		{
			GcControl.GC_disable();
			toggleButtonText.text = "Enable GC";
		}
		else
		{
			GcControl.GC_enable();
			toggleButtonText.text = "Disable GC";
		}
	}
 
	// Re-enable GC on exit
	void OnApplicationQuit()
	{
		if (isGcDisabled)
		{
			GcControl.GC_enable();
		}
	}
}

下面是它的实际效果:
在这里插入图片描述
单击禁用 GC 的按钮确实会阻止 GC 收集计数上升。单击按钮重新启用 GC 会恢复数字的上升,因为 GC 会开始尝试回收内存,为新分配的 byte[] 对象腾出空间。

这适用于 Unity 编辑器和 Android 设备。无法保证它可以在 Unity 支持的每个平台上运行,但至少这两个平台都可以运行。

既然我们有了这个工具,那么我们应该如何使用它呢?要决定策略,了解该工具的效果非常重要。对于我们大多数人来说,在禁用 GC 的情况下运行游戏是一个新领域。

通常,启用 GC 后,当我们分配托管内存(例如使用 new Class())并且托管堆中没有足够的空间来存储该对象时,会发生两件事。首先,运行 GC 来收集所有垃圾,以便可以重用内存。其次,如果仍然没有足够的空间,则会扩展托管堆以为新对象腾出空间。

禁用 GC 后,第一步将不会发生,并且不可能重用一个或多个垃圾对象的内存。相反,我们将直接进入第二步并扩展托管堆。这意味着随着我们分配越来越多的对象,内存使用量将继续增长。如果我们分配足够的内存,我们可能会耗尽内存并面临诸如由于使用“虚拟内存”(例如在 Mac 上)或让我们的应用程序被操作系统彻底终止(例如在 Android 上)而导致性能下降等后果。禁用GC根本不让我们放弃所有的自我控制并随心所欲地分配。

因为我们真的不希望这些事情发生,所以在禁用 GC 时不要使用太多内存,这一点至关重要。通常可以对此进行安排。例如,如果我们在加载一个游戏关卡后开始使用 100 MB 的内存,并且我们知道在该关卡期间不会分配超过 100 MB 的内存,那么我们在任何可以使用的设备上都没有问题。最多使用 200 MB。作为一种保护措施,我们始终可以定期查询内存使用情况,并在达到对设备而言存在危险的阈值(例如 250 MB)时重新启用 GC。

还值得注意的是,我们调用的这些 C 函数是 Unity 引擎的内部函数,而不是公共 API 的一部分。无法保证函数名称、签名或行为在不同版本中保持相同。但实际上,这些函数是 Unity 使用的现成 GC 的一部分,文件头中的最新日期是 2001 年,因此这种技术很可能会继续有效,直到 Unity 最终取代 GC。目前该项目位于“开发”下的路线图上,描述为“正在进行中,时间表较长或不确定”。

尽管如此,这绝对是一项先进的技术,使用时应非常谨慎。对于游戏的帧速率关键部分禁用 GC 并在之后重新启用它(例如在加载屏幕上)可能是一个很大的好处,对于某些游戏来说是值得的,但应该非常小心地使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值