02-03-原理篇-大对象堆

大对象堆(LOH)

篇章 :02-原理篇

阅读时间 :约 30 分钟

前置知识:了解分代 GC


一、引言

在 .NET 的分代 GC 体系中,有一个特殊的存在------大对象堆(Large Object Heap,简称 LOH) 。它不遵循常规的分代规则,却对应用性能有着深远影响。许多 .NET 开发者都曾遇到过 LOH 碎片化导致的 OutOfMemoryException,而 Unity 开发者在使用 IL2CPP 后端时也会间接受到 LOH 机制的影响。

本章将深入剖析 LOH 的定义、分配策略、碎片化问题、压缩机制,以及 .NET 5 引入的 POH(Pinned Object Heap),帮助读者理解并优化大对象堆的使用。


二、LOH 的定义与阈值

2.1 85000 字节阈值

在 .NET 中,大小 ≥ 85000 字节(约 83 KB)的对象会被直接分配到 LOH,而不是 Gen0。这个阈值从 .NET Framework 1.0 开始就固定为 85000 字节,至今未变。

复制代码
using System;

class LOHThresholdDemo
{
    static void Main()
    {
        // 84999 字节 → Gen0
        var small = new byte[84999];
        Console.WriteLine($"84999 bytes → Gen{GC.GetGeneration(small)}"); // Gen0
        
        // 85000 字节 → LOH(算作 Gen2)
        var large = new byte[85000];
        Console.WriteLine($"85000 bytes → Gen{GC.GetGeneration(large)}"); // Gen2
        
        // 注意:LOH 对象在 GetGeneration 中报告为 Gen2
        // 但它们实际上在独立的 LOH 区域中
    }
}

2.2 为什么是 85000 字节?

85000 字节这个阈值并非随意选择,而是基于以下考量:

  1. 性能权衡 :Copying 算法复制大对象的代价太高。85000 字节意味着一次 memcpy 操作约 85KB,如果 Gen0 中有多个大对象,Copying 的开销会显著增加 STW 时间。
  2. 内存页对齐:85000 字节接近 2 个 4KB 内存页(8KB × 10),在内存管理上是一个合理的分界点。
  3. 历史原因:这个值在 .NET 1.0 时代确定,当时的设计目标是让大多数常见对象(字符串、小数组、小集合)都在 SOH 中分配。

2.3 哪些对象会进入 LOH

对象类型 大小条件 进入 LOH
byte[] ≥ 85000 bytes ✅
int[] ≥ 21250 个元素(21250 × 4 = 85000) ✅
string ≥ ~42500 个字符(UTF-16,每字符 2 bytes) ✅
double[] ≥ 10625 个元素(10625 × 8 = 85000) ✅
自定义 struct 数组 元素大小 × 数量 ≥ 85000 ✅
普通对象 < 85000 bytes ❌(进 Gen0)
复制代码
// 常见 LOH 分配场景
using System;
using System.Text;

class LOHAllocationExamples
{
    static void Main()
    {
        // 场景1:大字节数组(网络缓冲区)
        byte[] networkBuffer = new byte[65536]; // 64KB → Gen0
        byte[] largeBuffer = new byte[131072];  // 128KB → LOH
        
        // 场景2:大字符串
        var sb = new StringBuilder();
        for (int i = 0; i < 50000; i++) sb.Append('x');
        string largeString = sb.ToString(); // ~100KB → LOH
        
        // 场景3:大整数数组
        int[] largeIntArray = new int[25000]; // 25000 × 4 = 100000 bytes → LOH
        
        // 场景4:大 double 数组
        double[] largeDoubleArray = new double[15000]; // 15000 × 8 = 120000 bytes → LOH
        
        Console.WriteLine($"networkBuffer (64KB): Gen{GC.GetGeneration(networkBuffer)}");
        Console.WriteLine($"largeBuffer (128KB): Gen{GC.GetGeneration(largeBuffer)}");
        Console.WriteLine($"largeString (~100KB): Gen{GC.GetGeneration(largeString)}");
        Console.WriteLine($"largeIntArray (100KB): Gen{GC.GetGeneration(largeIntArray)}");
        Console.WriteLine($"largeDoubleArray (120KB): Gen{GC.GetGeneration(largeDoubleArray)}");
    }
}

三、LOH 的分配策略

3.1 空闲链表分配

与 Gen0 的 Bump Allocation(指针碰撞分配)不同,LOH 使用 空闲链表(Free List) 进行分配:

复制代码
LOH 内存布局:

[已分配 128KB] [空闲 64KB] [已分配 256KB] [空闲 128KB] [已分配 85KB] [空闲 2MB]
                                ↑
                           空闲链表节点

分配请求 100KB:
  → 遍历空闲链表
  → 找到 128KB 空闲块(Best-Fit)
  → 分割为 [已分配 100KB] [空闲 28KB]
  → 28KB 碎片留在链表中

3.2 分配算法

.NET 的 LOH 分配使用近似 Best-Fit 策略:

  1. 遍历空闲链表,寻找第一个 ≥ 请求大小的空闲块

  2. 如果找到的块远大于请求大小,继续搜索更接近的块

  3. 如果没有合适的空闲块,向操作系统申请新内存

  4. 分配后,剩余空间作为新碎片加入空闲链表

    // LOH 分配碎片化演示
    using System;
    using System.Collections.Generic;

    class LOHAllocationDemo
    {
    static List<byte[]> keepAlive = new List<byte[]>();

    复制代码
     static void Main()
     {
         Console.WriteLine($"初始堆大小: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
         
         // 分配多个大对象
         for (int i = 0; i < 10; i++)
         {
             keepAlive.Add(new byte[100_000]); // ~100KB each → LOH
         }
         Console.WriteLine($"分配 10 个 100KB 对象后: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
         
         // 释放偶数索引的对象,制造碎片
         for (int i = 0; i < 10; i += 2)
         {
             keepAlive[i] = null;
         }
         
         GC.Collect();
         Console.WriteLine($"释放一半后: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
         
         // 尝试分配一个 500KB 的大对象
         // 可能无法利用碎片空间(碎片不连续)
         keepAlive.Add(new byte[500_000]);
         Console.WriteLine($"分配 500KB 后: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
         
         // 堆大小可能增长,因为碎片无法满足 500KB 的连续请求
     }

    }

3.3 LOH 不分代

LOH 中的对象虽然被报告为 Gen2,但 LOH 本身不分代:

  • 新分配的 LOH 对象直接算作 Gen2
  • LOH 的 GC 只在 Full GC 时发生
  • LOH 默认不压缩(碎片不消除)

这意味着 LOH 对象的回收代价等同于 Gen2 回收------频率低但 STW 时间长。


四、LOH 的碎片化问题

4.1 碎片化成因

LOH 使用 Mark-Sweep 算法(默认不压缩),因此会产生内存碎片:

复制代码
LOH 碎片化过程:

初始状态:
[AAAA] [BBBB] [CCCC] [DDDD] [EEEE] [FFFF] [GGGG] [HHHH]

释放 B、D、F、H 后:
[AAAA] [空闲] [CCCC] [空闲] [EEEE] [空闲] [GGGG] [空闲]

碎片化结果:
总空闲 = 4 块 × 100KB = 400KB
但最大连续空闲 = 100KB
→ 无法分配 200KB 的对象!
→ LOH 向 OS 申请新内存 → 堆不断增长

4.2 碎片化的危害

复制代码
// LOH 碎片化导致 OOM 的典型场景
using System;
using System.Collections.Generic;

class LOHFragmentationDemo
{
    static void Main()
    {
        var buffers = new List<byte[]>();
        
        // 模拟网络服务器场景:分配和释放不同大小的缓冲区
        for (int cycle = 0; cycle < 100; cycle++)
        {
            // 分配一批缓冲区
            for (int i = 0; i < 20; i++)
            {
                int size = 85000 + i * 1000; // 85KB ~ 104KB
                buffers.Add(new byte[size]);
            }
            
            // 释放一半(制造碎片)
            for (int i = buffers.Count - 1; i >= 0; i -= 2)
            {
                buffers.RemoveAt(i);
            }
            
            GC.Collect();
            
            if (cycle % 20 == 0)
            {
                Console.WriteLine($"Cycle {cycle}: 堆大小 = {GC.GetTotalMemory(false) / 1024 / 1024} MB");
            }
        }
        
        // 堆大小持续增长,因为碎片无法被复用
        Console.WriteLine($"最终堆大小: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
    }
}

4.3 检测 LOH 碎片化

复制代码
// 使用 GC API 检测 LOH 碎片化
using System;
using System.Diagnostics;

class LOHFragmentationCheck
{
    static void Main()
    {
        // 分配和释放制造碎片
        var keep = new System.Collections.Generic.List<byte[]>();
        for (int i = 0; i < 50; i++)
        {
            keep.Add(new byte[100_000]);
        }
        for (int i = 0; i < 50; i += 2)
        {
            keep[i] = null;
        }
        GC.Collect();
        
        // 获取各代堆信息
        for (int gen = 0; gen <= 2; gen++)
        {
            long heapSize = GC.GetGCMemoryInfo().HeapSizeBytes;
            long fragmented = GC.GetGCMemoryInfo().FragmentedBytes;
            long committed = GC.GetGCMemoryInfo().TotalCommittedBytes;
            
            Console.WriteLine($"堆总大小: {heapSize / 1024 / 1024} MB");
            Console.WriteLine($"已提交: {committed / 1024 / 1024} MB");
            Console.WriteLine($"碎片: {fragmented / 1024 / 1024} MB");
            Console.WriteLine($"碎片率: {(double)fragmented / committed * 100:F1}%");
        }
    }
}

五、LOH 压缩

5.1 默认不压缩

.NET 的 LOH 默认不执行压缩,原因有二:

  1. 移动大对象的代价高 :复制 85KB+ 的对象需要大量 memcpy 操作
  2. 引用更新开销大:大对象可能被大量其他对象引用,更新所有引用代价高

5.2 手动触发压缩

从 .NET 4.5.1 开始,可以手动请求 LOH 压缩:

复制代码
using System;
using System.Runtime;

class LOHCompactionDemo
{
    static void Main()
    {
        // 制造 LOH 碎片
        var keep = new System.Collections.Generic.List<byte[]>();
        for (int i = 0; i < 100; i++)
        {
            keep.Add(new byte[100_000]);
        }
        for (int i = 0; i < 100; i += 2)
        {
            keep[i] = null;
        }
        
        GC.Collect();
        var info = GC.GetGCMemoryInfo();
        Console.WriteLine($"压缩前碎片: {info.FragmentedBytes / 1024 / 1024} MB");
        
        // 启用 LOH 压缩(仅一次)
        GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
        GC.Collect();
        
        info = GC.GetGCMemoryInfo();
        Console.WriteLine($"压缩后碎片: {info.FragmentedBytes / 1024 / 1024} MB");
        
        // 重置为默认模式
        GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.Default;
    }
}

5.3 压缩的代价

LOH 压缩虽然能消除碎片,但代价很高:

指标 不压缩 压缩
STW 时间 短 长(与 LOH 大小成正比)
CPU 开销 低 高(大量 memcpy)
碎片消除 否 是
适用频率 每次 Full GC 偶尔(如内存紧张时)

最佳实践:不要在性能敏感的路径上启用 LOH 压缩。建议在场景切换、关卡加载等非关键路径时手动触发。


六、LOH 在不同 GC 中的实现

6.1 .NET Framework / .NET Core / .NET 5+

版本 LOH 特性
.NET Framework 1.0-4.0 LOH 不压缩,碎片化严重
.NET Framework 4.5.1+ 支持手动 LOH 压缩
.NET Core 1.0+ 继承 4.5.1 的 LOH 压缩支持
.NET 5+ 引入 POH(Pinned Object Heap)
.NET 6+ LOH 性能优化,减少分配锁竞争

6.2 Unity 中的大对象处理

Unity 的 Boehm GC 没有 LOH 概念,所有对象都在同一个堆中:

复制代码
// Unity 中的大对象分配行为
#if UNITY_EDITOR
using UnityEngine;
using System;

public class UnityLargeObjectTest : MonoBehaviour
{
    void Start()
    {
        // Boehm GC 不区分 LOH
        // 大对象和小对象在同一个堆中
        var large = new byte[1_000_000]; // 1MB
        var small = new byte[100];       // 100 bytes
        
        // 两者都在同一个堆中,没有 LOH 概念
        // Boehm GC 使用 Mark-Sweep,不移动任何对象
        Debug.Log($"堆大小: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
        
        // Unity 的 Boehm GC 也会碎片化
        // 但无法像 .NET 那样手动压缩
    }
}
#endif

6.3 对比表

特性 .NET LOH Unity Boehm
阈值 85000 bytes 无(所有对象同堆)
算法 Mark-Sweep + 可选 Compact Mark-Sweep(不压缩)
分代 算作 Gen2 不分代
压缩 支持手动触发 不支持
碎片化 可控(可压缩) 严重(不可压缩)
POH .NET 5+ 支持 不支持

七、POH(Pinned Object Heap)

7.1 固定对象的问题

在 .NET 5 之前,固定对象(Pinned Object)使用 GCHandleType.Pinned 或 fixed 语句来防止 GC 移动对象。固定对象会导致两个问题:

  1. 碎片化:如果固定对象在 Gen0 中,Copying 算法无法移动它,导致 Gen0 碎片化

  2. 效率降低:Gen0 的 Copying 算法需要跳过固定对象,增加复杂度

    // 传统固定对象的问题
    using System;
    using System.Runtime.InteropServices;

    class PinnedObjectProblem
    {
    static void Main()
    {
    // 传统方式:在 SOH 中固定对象
    var buffer = new byte[1024];
    GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

    复制代码
         try
         {
             // 获取固定指针,传递给非托管代码
             IntPtr ptr = handle.AddrOfPinnedObject();
             Console.WriteLine($"固定对象地址: {ptr}");
             
             // 问题:这个对象在 Gen0 中无法被 Copying 算法移动
             // 如果 Gen0 GC 发生,这个对象会"卡"在原位
             // 导致 Gen0 碎片化
         }
         finally
         {
             handle.Free(); // 释放固定
         }
     }

    }

7.2 POH 的设计

.NET 5 引入了 POH(Pinned Object Heap),将固定对象分配到独立的堆区域:

复制代码
// .NET 5+ 使用 POH 分配固定对象
using System;
using System.Runtime.InteropServices;

class POHDemo
{
    static void Main()
    {
        // .NET 5+ 支持 POH 分配
        // 使用 GC.AllocateArray 的 pinned 参数
        byte[] pinnedBuffer = GC.AllocateArray<byte>(1024, pinned: true);
        
        // 这个数组直接分配在 POH 中
        // 不会被 GC 移动,也不会影响 Gen0 的 Copying 算法
        
        // 获取固定指针(不需要 GCHandle)
        unsafe
        {
            fixed (byte* ptr = pinnedBuffer)
            {
                Console.WriteLine($"POH 对象地址: {(IntPtr)ptr}");
                // 可以安全地传递给非托管代码
            }
        }
        
        Console.WriteLine($"POH 对象所在代: {GC.GetGeneration(pinnedBuffer)}");
        // POH 对象算作 Gen2,不会被移动
    }
}

7.3 POH 的优势

优势 说明
不影响 Gen0 固定对象不在 Gen0,不影响 Copying 算法
无需 GCHandle 分配即为固定,无需额外 GCHandle 开销
减少碎片化 Gen0 不再有固定对象导致的碎片
简化 P/Invoke 直接传递 POH 对象给非托管代码

7.4 POH 适用场景

复制代码
// POH 典型场景:与非托管代码交互
using System;
using System.Runtime.InteropServices;

class POHUseCases
{
    // 场景1:文件 I/O 缓冲区
    static void FileIOExample()
    {
        // 分配固定缓冲区用于文件 I/O
        byte[] ioBuffer = GC.AllocateArray<byte>(64 * 1024, pinned: true);
        
        unsafe
        {
            fixed (byte* ptr = ioBuffer)
            {
                // 传递给 Windows API ReadFile/WriteFile
                // 不需要 GCHandle,不会影响 Gen0
            }
        }
    }
    
    // 场景2:网络缓冲区
    static void NetworkExample()
    {
        // 固定网络缓冲区,避免 GC 移动导致指针失效
        byte[] netBuffer = GC.AllocateArray<byte>(128 * 1024, pinned: true);
        
        unsafe
        {
            fixed (byte* ptr = netBuffer)
            {
                // 传递给 socket API
            }
        }
    }
    
    // 场景3:Unity 中的原生插件交互
    // 注意:Unity 可能不支持 POH(取决于 .NET 版本)
    static void UnityNativePluginExample()
    {
        // 如果 Unity 使用 .NET 5+,可以使用 POH
        // 否则需要回退到 GCHandle.Alloc(pinned)
        try
        {
            byte[] buffer = GC.AllocateArray<byte>(4096, pinned: true);
            Console.WriteLine("POH 分配成功");
        }
        catch (MissingMethodException)
        {
            // Unity 不支持 POH,回退到传统方式
            byte[] buffer = new byte[4096];
            GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            Console.WriteLine("回退到 GCHandle 固定");
            handle.Free();
        }
    }
}

八、LOH 优化最佳实践

8.1 池化大对象

最有效的 LOH 优化策略是 对象池化(Object Pooling),避免频繁分配和释放大对象:

复制代码
using System;
using System.Collections.Concurrent;

// 大数组对象池
class LargeArrayPool
{
    private readonly ConcurrentDictionary<int, ConcurrentBag<byte[]>> _pools = new();
    
    public byte[] Rent(int minimumSize)
    {
        // 找到合适的池
        int bucketSize = GetBucketSize(minimumSize);
        var pool = _pools.GetOrAdd(bucketSize, _ => new ConcurrentBag<byte[]>());
        
        if (pool.TryTake(out byte[]? array))
        {
            return array; // 复用已有数组
        }
        
        return new byte[bucketSize]; // 池为空时才分配
    }
    
    public void Return(byte[] array)
    {
        int bucketSize = GetBucketSize(array.Length);
        var pool = _pools.GetOrAdd(bucketSize, _ => new ConcurrentBag<byte[]>());
        pool.Add(array); // 归还到池中
    }
    
    private int GetBucketSize(int minSize)
    {
        // 按 2 的幂对齐
        int size = 81920; // 从 80KB 开始(接近 LOH 阈值)
        while (size < minSize) size *= 2;
        return size;
    }
}

// 使用示例
class PoolUsageDemo
{
    private static LargeArrayPool _pool = new();
    
    static void ProcessData()
    {
        // 从池中租借,而不是 new
        byte[] buffer = _pool.Rent(100_000);
        try
        {
            // 使用 buffer 处理数据
            // ...
        }
        finally
        {
            _pool.Return(buffer); // 归还到池中
        }
    }
}

8.2 使用 ArrayPool

.NET 内置了 ArrayPool<T>,可以避免 LOH 分配:

复制代码
using System;
using System.Buffers;

class ArrayPoolDemo
{
    static void Main()
    {
        // 使用 ArrayPool 而不是 new byte[]
        byte[] buffer = ArrayPool<byte>.Shared.Rent(100_000);
        try
        {
            // 使用 buffer
            // 注意:实际大小可能 ≥ 请求大小
            Console.WriteLine($"租借大小: {buffer.Length} bytes");
            
            // ArrayPool 内部会复用数组,避免 LOH 分配
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
        
        // ArrayPool 也可以使用 MemoryPool
        using IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.Rent(100_000);
        Span<byte> span = memory.Memory.Span;
        // span 可以安全使用,超出作用域自动归还
    }
}

8.3 避免不必要的 LOH 分配

复制代码
// 常见的 LOH 陷阱和解决方案
using System;
using System.Text;

class LOHPitfalls
{
    // 陷阱1:字符串拼接产生大字符串
    static void StringConcatenation()
    {
        // ❌ 错误:可能产生 LOH 级别的大字符串
        string result = "";
        for (int i = 0; i < 50000; i++)
        {
            result += "x"; // 每次拼接都创建新字符串
        }
        // result 可能 > 85000 bytes → LOH
        
        // ✅ 正确:使用 StringBuilder
        var sb = new StringBuilder(50000); // 预分配容量
        for (int i = 0; i < 50000; i++)
        {
            sb.Append('x');
        }
        string result2 = sb.ToString();
        // StringBuilder 内部使用 char[],可能进 LOH
        // 但只分配一次,不是反复分配
    }
    
    // 陷阱2:List<T> 内部数组扩容
    static void ListExpansion()
    {
        // ❌ 错误:List 内部数组可能扩容到 LOH
        var list = new System.Collections.Generic.List<byte>();
        for (int i = 0; i < 100000; i++)
        {
            list.Add((byte)i); // 内部数组多次扩容,最终可能进 LOH
        }
        
        // ✅ 正确:预分配容量
        var list2 = new System.Collections.Generic.List<byte>(100000);
        for (int i = 0; i < 100000; i++)
        {
            list2.Add((byte)i);
        }
    }
    
    // 陷阱3:LINQ ToArray 产生大数组
    static void LinqToArray()
    {
        var data = new System.Collections.Generic.List<int>();
        for (int i = 0; i < 25000; i++) data.Add(i);
        
        // ❌ 可能产生 LOH 分配
        int[] array = data.ToArray(); // 25000 × 4 = 100000 bytes → LOH
        
        // ✅ 如果需要频繁使用,考虑池化
        int[] pooled = ArrayPool<int>.Shared.Rent(25000);
        try
        {
            data.CopyTo(pooled);
            // 使用 pooled[0..data.Count]
        }
        finally
        {
            ArrayPool<int>.Shared.Return(pooled);
        }
    }
}

8.4 LOH 优化检查清单

检查项 说明
是否使用 ArrayPool 大数组应使用 ArrayPool 而非 new
是否预分配集合容量 List、Dictionary 等应预分配容量
是否避免字符串拼接 大字符串使用 StringBuilder
是否定期检查 LOH 碎片 使用 GC API 监控碎片率
是否在非关键路径压缩 LOH 场景切换时手动压缩
是否使用 POH(.NET 5+) 固定对象使用 POH 而非 GCHandle
是否避免 LOH 中的短生命周期对象 LOH 对象应长期复用

九、总结

大对象堆(LOH)是 .NET GC 体系中一个特殊而重要的设计:

  1. 85000 字节阈值:≥ 85000 字节的对象直接分配到 LOH,避免 Copying 算法复制大对象的开销。

  2. Mark-Sweep 算法:LOH 默认使用 Mark-Sweep,不压缩,因此会产生碎片化。

  3. 碎片化是核心问题 :LOH 碎片化会导致堆不断增长,最终可能引发 OOM。可以通过 GCSettings.LargeObjectHeapCompactionMode 手动压缩。

  4. POH(.NET 5+):固定对象堆将固定对象从 Gen0 中分离,避免固定对象影响 Copying 算法效率。

  5. 池化是最佳实践 :使用 ArrayPool<T> 或自定义对象池避免频繁的 LOH 分配和释放。

Unity vs .NET 的关键差异:

  • .NET 有专门的 LOH 处理大对象,支持手动压缩和 POH
  • Unity 的 Boehm GC 没有 LOH,所有对象在同一个堆中,碎片化更严重且不可压缩
  • .NET 的 ArrayPool<T> 是避免 LOH 分配的有效工具,Unity 中需要自行实现对象池

理解 LOH 的工作原理和优化策略,是编写高性能 .NET 和 Unity 应用的关键技能。在下一章中,我们将深入探讨 GC Roots 和对象图遍历------GC 如何确定哪些对象是"存活"的。

相关推荐
海宇大数据2 小时前
零信任架构实战:基于海宇身份证OCR构建自动化跨境清关核验网关
人工智能·架构·c#·自动化
LateFrames5 小时前
WPF 升级选型:用 .NET 8 还是 .NET 10
.net·wpf·.net8·.net10
samble16 小时前
从零搭建灌装监控系统(十):生产追溯,条码触发自动记录
c#·wpf·mvvm·modbus·工业控制
WarPigs18 小时前
Unity在Create菜单添加创建自定义脚本模板选项
unity
czhc114007566320 小时前
退出防呆:用“总闸“收口所有退出入口,以及三个并发语法坑
开发语言·c#
郝学胜-神的一滴21 小时前
C++ Templates 01:拨开迷雾,读懂C++模板经典著作
开发语言·c++·程序人生·游戏·游戏引擎·软件构建
淡海水1 天前
02-01-原理篇-Mark-Sweep与变种算法
算法·unity·c#·游戏引擎·.net·gc
babywang01 天前
Unity笔记--塔防游戏中的ECS架构:从原理到实战的全面解析
unity·ecs架构
UIU1141 天前
同一个变量在两个 .c 文件里类型不一致,程序会怎样?
c++·学习·c#