为 .NET 10 GC(DATAS)做准备

原作者:maoni 原文链接:https://maoni0.medium.com/preparing-for-the-net-10-gc-88718b261ef2

在 .NET 9 中,我们默认启用了 DATAS。但 .NET 9 并不是长期支持(LTS)版本,因此很多人会在升级到 .NET 10 时首次获得 DATAS。这是一个很艰难的决定,因为 GC 功能通常是不需要用户干预的 —— 但 DATAS 有些不一样。这也是为什么本文标题是“做准备”,而不是单纯的“新功能介绍”😊。

如果你在使用 Server GC,你可能会注意到相比以往的运行时升级,性能特征差异更为明显。内存使用可能会显著不同(很可能更小)—— 这未必是你想要的。这取决于这种取舍对你来说是否明显,以及它是否符合你的优化目标。我建议你至少快速查看一下应用性能指标,看看是否对这种变化满意。很多人会绝对欢迎此变化 —— 但如果你不是其中之一,不必慌张。我建议继续阅读,看是否可以简单地关闭 DATAS,或者稍微调优让它对你有好处。

我将介绍我们通常如何决定添加哪些性能功能,为什么 DATAS 与典型 GC 功能有很大不同,以及自我上次撰写 DATAS 文章以来引入的调优变化。我还会分享两个我在首方场景中调优 DATAS 的实例。

如果你主要是想知道 DATAS 不适用的场景以帮助判断是否要关闭它,可以直接跳到相关部分。

术语表

在深入内容前,先列出本文中使用的缩写:

  • GC:垃圾回收器,负责管理应用程序的内存分配与释放
  • DATAS:动态适应应用程序大小(Dynamic Adaptation To Application Sizes)
  • TCP:吞吐成本百分比(Throughput Cost Percentage)—— 测量 GC 开销,包括 GC 暂停和分配等待
  • BCD:通过 DATAS 计算的预算(Budget Computed via DATAS)—— 代际 0 分配预算的上限
  • LDS:活动数据大小(Live Data Size),即应用程序在最强 GC 下占用的内存大小
  • UOH:用户旧代堆(User Old Heap),旧代中用户代码分配的部分,包括 LOH 和 POH
  • LOH:大对象堆(Large Object Heap),用于存储 ≥85,000 字节的对象,可通过 GCLOHThreshold 配置修改
  • POH:固定对象堆(Pinned Object Heap),专门用于存储在分配时标记为固定的对象的堆区域

添加 GC 性能功能的一般策略

大多数 GC 性能功能 —— 无论是新的 GC 类型、新的机制,还是优化现有机制 —— 通常在你升级到新的运行时版本时自动启用。我们不要求用户采取操作,因为这些功能旨在改善广泛的场景。这也是我们选择实现它们的原因:我们分析许多场景以找出常见问题,确定解决它们的方法,然后优先设计并实现可带来最大影响的功能。

当然,任何性能变化都有引入回退的风险 —— 对于一个拥有数百万用户的框架,你几乎可以肯定会让某些人退步。这种退步在微基准测试中尤其明显,因为微基准测试的行为高度极端,甚至细微变化都会让结果大幅波动。

一个近期的例子是我们改变了处理 UOH(即 LOH + POH)代的可用区域的方式。我们从基于预算的裁剪策略改为基于“年龄”的策略,因为它整体更稳健(这样我们不会快速释放内存再重新提交,或在长时间后仍保留大量可用区域,因为一直没有消耗几乎所有的 UOH 预算)。但这会完全改变一个原本在一次 /* by 01022.hk - online tools website : 01022.hk/zh/countdown.html */ GC.Collect() 后主内存降到很低值的微基准测试,使其必须调用 3 次 /* by 01022.hk - online tools website : 01022.hk/zh/countdown.html */ GC.Collect()(因为我们需等待 UOH 可用区域在两次 gen2 GC 中“变老”,第三次才会把它放入释放列表)。

但对于 DATAS,我们知道它天生并不适用于广泛场景。正如我在上篇博客文章中所述,DATAS 针对两类特定场景。我在这里重申:

  1. 在受内存限制的环境中运行的突发性负载。DATAS 旨在当应用不需要那么多内存时收缩堆大小,而在需要更多时扩展堆大小。这对运行在有内存限制的容器中的应用尤其重要。
  2. 使用 Server GC 的小型负载 —— 例如有人想试试一个小型 asp.net core 应用在 .NET 中的体验,DATAS 旨在提供与小型应用实际需求更加匹配的堆大小。

关于第 1 点,我需要进一步解释。突发性负载非常常见。一个处理请求的应用在某个时间段的用户数自然可能远超一天中其他时间段。但关键在于随后的动作 —— 如果在非高峰时释放了内存,你会如何利用这些内存?事实证明,有时人们并没有真正的计划 —— 他们只是希望看到内存占用下降,但并不打算使用这些内存。而有的团队也不需要降低内存占用,因为他们已经为应用预留了全部内存。我最近就和一个客户交流,当我问他们“如果 DATAS 为你释放了内存,你会用来做什么?”时,对方回答:“这是个好问题,我们从没想过。”

对于那些希望利用释放内存的人,一个常见的方法是使用编排环境。DATAS 让此场景更稳健,因为堆大小更可预测,从而帮助设定合理的内存限制。例如在 k8s 中,你可以为非高峰和高峰负载分别确定合适的 request 和 limit 值,更好利用 HPA。我还见过有团队安排任务在机器/虚拟机有空闲内存时运行 —— 这通常更复杂(而且这些团队通常配有专门的性能工程师),但控制力更强。

另外,也有很多团队拥有专用机器集群,想在高峰时尽最大可能提高吞吐量。他们不愿容忍任何形式的性能下降。他们显然不是 DATAS 的目标用户,因为 DATAS 几乎总会降低其吞吐量 —— 性能问题很少是“全有或全无”,我会在下文讨论如何决定是否该关闭 DATAS。

所有这些因素让我们很难将 DATAS 设为默认开启,因为我们知道有很多团队不愿牺牲吞吐量,或者不会利用释放的内存。

我将在下文详细讨论,如果你想比较性能差异并判断 DATAS 是否适用(或者看到内存减少后想到利用这些空闲内存),该如何分析。

DATAS 与传统 Server GC 的性能差异

DATAS 是一个我花了比其它任何 GC 功能更多时间向同事解释的功能——由于它是高度显性的用户可感知特性,自然比我添加的几乎任何其它 GC 功能收到更多的提问。而且,围绕它存在许多误解。有些人认为 DATAS 只影响启动阶段;有些人假设它只是“内存减少 x%,吞吐量降低 y%”;还有些人期待它能“神奇地减少内存占用,而不会带来其他性能差异”(好吧,“神奇地”是我加上的 😆);等等。

要正确理解它的不同,我们需要先理解两者的策略差异。首先也是最重要的,Server GC 并不会根据应用大小进行自适应——这从来就不是它的设计目标。Server GC 主要观察每一代的存活率,并根据这一指标来决定何时进行 GC(当然,还有其他因素影响 GC 的触发时机,但存活率是其中最重要的因素之一)。在我上一篇关于 DATAS 的文章中,我谈到了堆的数量会显著影响堆大小,尤其是在分配了大量临时数据的负载场景下。由于 Server GC 会创建与进程可使用核心数相同数量的堆,这意味着同一个应用在不同核心数的机器上运行,或者同一台机器上限制可用核心数量运行,都会表现出非常不同的堆大小。

而 DATAS 的目标,则是适应应用的大小,这意味着即使核心数差异很大,你的堆大小也应该是相近的。因此,没有什么“DATAS 会比 Server GC 减少 X% 的内存占用”的固定结论。

如果我们看 asp.net 基准测试的“最大堆大小”指标,可以明显看到 Server GC 在 28 核机器(28c)和 12 核机器(12c)上的行为差异——

细心的读者会注意到图中的颜色顺序并不一致。比如在 MultipleQueriesPlatform 场景中,最大堆大小在 12c 情况下比 28c 更大。仔细查看数据可以发现,在 12c 情况下最大堆大小其实发生在测试一开始——

(Heap size (before) 指在某次 GC 之前的堆大小,即 GC 还未来得及缩减堆时的大小。因此 “最大堆大小” 是此指标的最大值)

这是因为在刚开始阶段,28c 配置下由于有 28 个堆,在第一次 GC 发生前进行了更多的内存分配。于是第一次 GC 后,观察到的存活率较小,从而使 gen0 的分配预算(budget)比 12c 更小。12c 很快就进入了稳定状态,并且堆大小显著低于 28c。在稳定状态下,这些基准测试在 28c 下的堆大小始终高于 12c。

这说明了两点:

  1. 如果只测量“最大堆大小”,很容易被非稳定状态的行为影响;
  2. 堆大小会因为测试运行的机器不同而有非常大的变化。

需要注意的是,这些效应在小型基准测试里会被放大,但其原理同样适用于真实应用。

使用 DATAS 时,我们看到的情况是——

28c 与 12c 的最大堆大小非常接近,这正是 DATAS 的设计目的——它适应应用的大小。

如果我使用 Workstation GC,需要关注 DATAS 吗?

答案取决于你使用 Workstation GC 的原因。如果你使用 Workstation GC 是因为你的工作负载根本不需要 Server GC,例如应用是单线程的,或分配压力很小,你完全可以接受只有一个线程做垃圾回收,那么 Workstation GC 不仅足够,而且就是正确的选择。

但如果你使用它只是因为 Server GC 内存占用太大,而改用 Workstation GC 来限制内存占用,那么你可能会觉得 DATAS 很有吸引力,因为它既可以限制内存占用,也可以让更多的 GC 线程参与回收,从而减少 GC 暂停时间。

DATAS 是如何工作的

如果你理解了 DATAS 的工作原理,就会自然地得出下面这些建议,帮助你判断它是否适合你的场景。你也可以跳过这一部分,但我个人更喜欢去理解事物的运作机制,而不是单纯记下一些经验规则。这样我能自己得出结论,而不是机械地照搬配置。在上一篇博客中,我提到过当时(.NET 8)的 DATAS 一些实现细节,并指出它很可能会发生重大改变 —— 事实确实如此,在设计和实现上都有较大改动。我们在 .NET 8 中的实现更多是功能性验证,几乎没有花时间在优化调优上,而主要的调优工作是在 .NET 8 之后进行的。

DATAS 的目标是根据应用规模(即 LDS,Live Data Size,存活数据大小)进行自适应调整。因此,需要有一种方法去适配它。由于 .NET GC 是代际垃圾回收,它并不会频繁回收整个堆。而且大多数完整 GC 都是后台 GC,并且不会进行压缩,因此我们可以近似地通过旧年代对象占用的空间(即总大小减去碎片)来估计 LDS。在做性能分析时,另一个方便的数值是查看一次完整 GC 时的晋升大小(promoted size)。

在上一篇博客中我提到过 conserve memory 配置是 DATAS 实现的一部分 —— 这一点没有变化。但是 conserve memory 只影响何时触发完整 GC。对于分配非常频繁的应用,除非它们主要分配的是临时 UOH(大对象堆)对象,否则大部分触发的都是瞬时代(ephemeral)GC。而瞬时代的大小在小堆场景中可能占据整个堆的一大部分。

在尝试不同方法之后,我最终确定了一个兼顾适应应用大小保持合理性能的策略,包含两个核心部分:

  1. 引入了一个概念——DATAS 计算的预算(BCD, Budget Computed via DATAS),它是基于应用规模计算出来的 gen0 最大预算上限。这个值可以近似 gen0 的代大小(考虑到有对象会被固定,实际 gen0 的大小可能会略有不同)。
  2. 在上述预算上限之内,如果能保持合理性能,我们还会进一步减少内存占用。我们用目标吞吐量成本百分比(TCP, Throughput Cost Percentage)来定义“合理性能”。TCP 考虑了 GC 暂停时间以及分配线程等待时间,不过在稳定状态下,使用 GC 暂停时间百分比近似 TCP 已足够。目标是在可能的情况下将 TCP 控制在这个目标值左右。这意味着当负载减轻时,我们会缩小 gen0 预算,从而使 gen0 在下一次 GC 前的大小更小,最终导致堆大小变小。默认目标 TCP 为 2%,可以通过 GCDTargetTCP 配置进行修改。

我们来看两个例子,看看这如何在不同场景下体现出来。为了简化说明,我忽略了后台 GC,并用 GC 暂停时间百分比近似 TCP。

场景 A —— 一个电商应用将完整商品目录存储在内存中,并在整个进程生命周期保持不变,这就是我们的 LDS。进程开始处理请求,每个请求都会分配一些内存,并在请求完成后释放。

在高峰时段,它同时处理大量请求。这时我们达到了最大的预算 BCD。假设这个预算是 1GB,这意味着每分配 1GB 就会发生一次 GC。如果用 GC 暂停时间百分比近似 TCP,假设每秒分配 1GB,会进行一次 GC,暂停时间为 20ms。那么 GC 时间占比为 2%,正好等于目标 TCP。

在非高峰时段,同时请求减少,假设每秒只分配 ~200MB,如果仍用 1GB 预算,就会每 5 秒一次 GC,此时 GC 时间占比为 (20ms / 5s = 0.4%),远低于 2%。为了达到目标 TCP,我们希望减少预算,更早触发 GC。如果预算减少到 200MB,并假设 GC 暂停时间仍为 20ms(实际上可能会更短,因为存活率少),那么 TCP 再次达到 2%。

在这种情况下,非高峰时段的堆大小减少了约 800MB。根据总堆大小,这会是非常显著的提升。

场景 B —— 基于场景 A,但我们增加了一个缓存,该缓存是 LDS 的一部分,并在轻负载时缩小,因为不需缓存过多。由于 LDS 变小,BCD 也会变小,此时 gen0 预算会进一步减少,再次体现了 DATAS“根据规模自适应”的特性。同时,conserve memory 机制仍然生效,它会相应调节旧年代的预算和大小。

注意到在以上例子中,我们完全没有提到堆的数量!这是 DATAS 自己处理的事情,因此你无需手动指定。以前有客户会通过 GCHeapCount 配置来指定 Server GC 堆的数量。而 DATAS 更加稳健,可以在需要时利用更多堆(通常意味着更短的单次暂停时间),并在 LDS 下降时减少堆大小,而无需你自己设置。

DATAS 有专门的事件来表示实际的 TCP 和 LDS,但获取这些数据需要通过 TraceEvent 库编程获取。对于几乎所有性能分析,使用上述近似值已经足够。

在哪些情况下不适合使用 DATAS

如果你读过前面的内容,下面这些判断应该很容易理解。

  1. 如果你不需要释放的内存 这很明显 —— 如果释放的内存对你毫无用途,就没必要改动任何东西。你可以通过 GCDynamicAdaptationMode 配置关闭 DATAS。 我遇到过一些内部团队,他们的进程运行在专用机器上,不会运行其他程序,因此无需额外空闲内存。他们会关闭 DATAS。但如果他们将来希望在非高峰期利用这部分内存,可以再启用。
  2. 如果启动性能至关重要 DATAS 启动时总是从 1 个堆开始,因为我们无法预测你的负载压力,且 DATAS 优化的是大小,所以初始堆数最小。如果你的应用启动性能非常重要,DATAS 会导致在扩展到多堆的过程中有性能回退。
  3. 如果你不能接受任何吞吐量回退 包括启动吞吐量。对于不关心启动的场景,可根据情况选择是否用 DATAS。例如,如果 Server GC 的 GC 暂停时间占比为 1%,你可以设置 GCDTargetTCP 为 1。如果之前限制堆数,DATAS 可能会带来性能提升,因为暂停时间会更短。如果适应应用大小对你有帮助,DATAS 可能是更好的选择。但如第 1 点所述,如果你完全用不到释放出来的内存,就没必要花时间。
  4. 如果你的场景主要发生 gen2 GC 如果你的场景几乎总是 gen2 GC(这几乎总是因为大量分配临时大对象),DATAS 并未在这种情况上进行深入调优。如果你试用 DATAS 后不满意,可以关闭它。如果有足够理由花时间调优,也可参考后面的调优部分进行尝试。

如果需要,如何调优 DATAS

我在一些内部重要负载上试过 DATAS,总体效果很好。但在少数场景下,默认参数效果一般,稍微调整一两个配置就能让它工作得很好。

客户案例 1

这是一个运行在专用机器上的服务器应用。但他们正计划将其容器化,因此使用 DATAS 的确有一定价值。启用 DATAS 后,他们观察到吞吐量下降了 6.8%,同时工作集减少了 10%。目前他们已经禁用了 DATAS —— 我会解释我是如何调试的,并确定在他们未来想重新启用 DATAS 时应使用的配置。

由于 DATAS 会根据 LDS 限制最大的 gen0 预算,我们需要查看是否触及了这个上限。最简单的方法是分别在启用 DATAS 和未启用 DATAS 的情况下捕获 GC 跟踪。如果你发现触发的 GC 次数更多,那么很可能就是达到了这个限制。

你可以用 “% Pause Time” 列来近似计算 TCP,用 “Gen0 Alloc MB” 列来近似计算 gen0 预算。你需要找到 % 暂停时间最高的阶段,并查看此时是否触发了更多的 GC。

对于这个特定客户,下面是他们的一些 GC 摘录(我已裁剪了 GCStats 视图的列)——

未启用 DATAS

GC indexTrigger reasonGen% pause timeGen0 Alloc (MB)Promoted (MB)
7017AllocSmall0N0.74,243.75382.855
7018AllocSmall1N1.24,157.851,074.82
7019AllocSmall0N0.84,218.46484.276
7020AllocSmall1N2.04,249.861,072.56
7021AllocSmall0N1.54,258.12453.534
7022AllocSmall1N1.84,244.211,026.41
7023AllocSmall0N1.04,254.77461.702
7024AllocSmall1N1.44,239.38992.243
7025AllocSmall0N1.04,252.54465.904
7026AllocSmall1N2.54,252.471,153.60
7027AllocSmall0N1.74,216.14442.233
7028AllocSmall2B0.3015,039.20
7029AllocSmall0N0.64,166.23411.238
7030AllocSmall1N1.04,104.28681.430
7031AllocSmall0N1.44,229.11582.256
7032AllocSmall1N1.14,222.06963.817
7033AllocSmall0N1.54,248.45463.555
7034AllocSmall1N1.14,230.40889.286
7035AllocSmall0N0.84,255.81467.854
7036AllocSmall1N1.44,254.73926.103
7037AllocSmall0N2.34,220.31448.918
7038AllocSmall1N1.24,249.19963.297

启用 DATAS

GC indexTrigger reasonGen% pause timeGen0 Alloc (MB)Promoted (MB)
17632AllocSmall0N2.61,645.46236.155
17633AllocSmall1N1.91,637.37430.244
17634AllocSmall0N1.41,648.58228.611
17635AllocSmall1N1.81,633.46461.741
17636AllocSmall0N3.81,644.98257.461
17637AllocSmall1N2.61,646.77492.176
17638AllocSmall0N1.51,650.46217.604
17639AllocSmall1N2.21,652.98446.634
17640AllocSmall0N2.01,647.49176.047
17641AllocSmall1N2.21,638.71495.137
17642AllocSmall0N1.31,643.52194.353
17643AllocSmall1N4.11,589.32451.100
17644AllocSmall0N2.81,645.70220.343
17645AllocSmall1N2.41,644.41479.159
17646AllocSmall0N1.11,642.08229.877
17647AllocSmall1N1.21,638.72436.051
17648AllocSmall0N1.21,653.15158.115
17649AllocSmall1N1.51,648.69487.923
17650AllocSmall0N1.61,649.91211.391
17651AllocSmall1N5.21,624.07412.570
17652AllocSmall0N1.91,644.00213.895
17653AllocSmall2B0.3014,936.54

比较他们在 GC 中的 gen0 预算和 % 暂停时间 —

指标无 DATAS有 DATAS无 DATAS / 有 DATAS
gen0 预算 (GB)4.221.642.6
% 暂停时间1.22.10.6

因此,无 DATAS 时的 gen0 预算是有 DATAS 时的 2.6 倍。另一个有用的观察是 % 暂停时间几乎正好等于目标 TCP——2%。这表明从 DATAS 的角度来看它完全按设计工作。但无 DATAS 时我们有 2.6 倍的预算,自然触发 GC 的频率降低,% 暂停时间从 2.1 降到了 1.2。

如果我们想启用 DATAS 且在这一阶段不降低吞吐量,就需要让 DATAS 使用更大的 gen0 预算。要做到这一点,我们必须理解 DATAS 是如何确定 BCD 的。既然是适配内存大小,我们希望将大小乘以一个系数。但这个乘数不能是常量,因为当内存很小时,乘数应该非常大——如果 LDS 只有 2MB(对于小型应用这是完全可能的),我们不希望每分配 0.2MB 就触发 GC——这样开销太高。假设我们希望在触发 GC 前允许分配 20MB,这意味着乘数是 10。但如果 LDS 是 20GB,我们也不希望分配 200GB 才 GC,这意味着乘数要小得多。这就意味着需要一个幂函数,同时在最小值和最大值之间进行限制 ——

m = constant / sqrt(LDS);   
// max_m 默认值是 10  
m = min (max_m, m);   
// min_m 默认值是 0.1  
m = max (min_m, m);

幂函数的实际公式是 —

m = (20 - conserve_memory) / sqrt (LDS / 1000 / 1000);

可以简化为 —

m = (20 - conserve_memory) * 1000 / sqrt (LDS);   
m = (20 - 5) * 1000 / sqrt (LDS);   
m = 15000 / sqrt (LDS);

因此常量是 15000,或者如果以 MB 为单位,可以说常量是 15。以下是不同 LDS 值的一些示例 —

LDS (MB)mm 限制后BCD (MB)
115.0010.0010
56.716.7134
104.744.7447
502.122.12106
1001.501.50150
5000.670.67335
1,0000.470.47474
5,0000.210.211,061
10,0000.150.151,500
15,0000.120.121,837
30,0000.090.103,000
50,0000.070.105,000

https://gist.github.com/Maoni0/15064a505db2d06189a875d4b7e9e211/raw/2153a4f57c1d5c30401dd796573e553bb8f4cb36/bcd.csv

这个常量、max_m 和 min_m 都可以通过配置调整,请查阅配置页面的详细说明。

现在很明显 DATAS 是如何得出 gen0 预算以及我们如何调整它的。如果我们想让预算接近无 DATAS 时的数值,应使用 GCDGen0GrowthPercent 配置将常量增加到 2.6 倍,并使用 GCDGen0GrowthMinFactor 配置提高 min_m,使其不被限制为 0.1——不需要非常精确,只要确保它不是限制因素即可。在此案例中,如果用 15GB 近似 LDS(gen2 GC 的 “Promoted (mb)” 列都显示约 15GB),而无 DATAS 时 gen0 预算是 4.22GB,那么 min_m 应该设为 (4.22 / 15 = 0.28)。我们可以将 min_m 设置为 300,这就相当于 LDS 的 0.3。

客户案例 2

这是客户在预备服务器上的一个 asp.net 应用,代表了他们的一个关键场景。我用压测工具生成了可变的工作负载。

团队已经在使用一些 GC 配置:

  • GCHeapCount 设置为 2,使用 2 个堆。
  • 通过 GCNoAffinitize 配置关闭了线程亲和性。

如果指定了 GCHeapCount,DATAS 会被禁用,因为它告诉 GC 不要修改堆数量。而修改堆数量是 DATAS 调整性能的关键机制之一,所以这是关闭 DATAS 的信号。

由于该进程与其他许多进程共存于同一台机器,在没有 DATAS 之前,他们选择使用 2 个堆来限制内存使用,同时保持合理的吞吐量。但这种方式不够灵活——当负载升高时,2 个堆的吞吐量会下降,并且 GC 暂停时间会明显增加,因为只有 2 个 GC 线程运行收集。另外,他们可以手动调整堆数量,但这样工作量很大,且服务器 GC 对于减少内存使用并不积极,当负载较轻时可能会出现堆过大的情况。

我将演示使用 DATAS 如何让这个过程更稳健。当我提高负载时,可以看到 GC 中的 % 暂停时间很高——这在只有 2 个堆时并不意外。所以我通过删除 GCHeapCount 配置启用了 DATAS(我保留了 GCNoAffinitize 配置,因为我仍然希望 GC 线程不进行亲和绑定)。我发现即使有 BCD,GC 中的 % 暂停时间仍然很高,因为我们仍频繁触发 GC。于是我决定用 GCDGen0GrowthPercent 配置将 BCD 增加到默认值的 2 倍(我不需要用 GCDGen0GrowthMinFactor,因为 2 倍仍在 max_m/min_m 的限制范围内)。这样该进程就表现得更理想,具有以下特点:

  • GC 的 % 暂停时间显著降低。使用默认 DATAS 时 % 暂停时间也相当低,且堆大小明显更小。根据优化目标,这可能正是你想要的效果。DATAS 可以通过较小的预算和更多的 GC 线程来完成收集工作。但对于该客户来说,这样的 % 暂停时间会影响吞吐量,他们不希望它这么高。我也可以让 DATAS 使用更小的目标 TCP,但在此情况下默认 TCP 已足够。
  • 单次 GC 暂停时间显著缩短,因为有更多的 GC 线程在运行收集任务。
  • 当负载变轻(并发客户端线程数从 200 降到 100)时,堆也会变小。同时我们依然保持较低的 GC % 暂停时间和单次 GC 暂停时间。

希望这些对你的 DATAS 调优有所帮助,如果你有需要的话。

DATAS 事件

我预计大多数用户都不需要查看这些事件,所以我会简要说明。前面提到的那些近似值应该已经足够。对于少数希望出于某种原因进行详细分析的人,DATAS 会触发一个事件,该事件准确表示了我们讨论过的指标。需要注意的是,我们仅在程序中使用这些事件,因此它们不会显示在 PerfView 的 Events 视图中(在那里面你只能看到 GC/DynamicTraceEvent 的事件名,而不是该事件的各个字段)。请参考这篇博客文章,了解如何在程序中从跟踪中获取 GC 信息列表(以 TraceGC 对象的形式)。

LDS 和 TCP 会在 SizeAdaptationTuning 事件中体现,假设你有一个类型为 TraceGC 的 gc 对象 —

// LDS  
gc.DynamicEvents().SizeAdaptationTuning?.TotalSOHStableSize  
// TCP  
gc.DynamicEvents().SizeAdaptationTuning?.TcpToConsider

该事件不会在每次 GC 都触发,因为我们只是每隔几个 GC 才检查是否需要调整 DATAS 的调优参数。

译者:文章总结

1. DATAS 核心思想

  • DATAS = Dynamic Adaptation To Application Sizes(动态适配应用大小)
  • 目标:让 GC 内存预算(特别是 Server GC)随着应用实时 Live Data Size(LDS)变化而调整
  • 好处:
    • 内存 峰值需求下降后 可以收缩堆大小
    • 处理容器 / 内存限制环境更稳健
    • 适配不同机器核心数的情况下,堆大小更一致
  • 机制:
    1. 计算 BCD(Gen0 Budget 上限)
      公式:
      m = (20 - conserveMemory) * 1000 / sqrt(LDS)  
      m = clamp(min_m, max_m, m)
      BCD = LDS × m
      BCD *= (GCDGen0GrowthPercent / 100)
      
    2. 保持 TCP(GC 暂停时间占比) 接近目标值(默认 2%)

2. DATAS 适用场景

  1. 容器/内存限制环境中的突发型业务
    • 高峰分配大,低峰分配少,需要收缩内存
  2. 小型 Server GC 应用
    • 例如测试 ASP.NET Core 小应用,由于 Server GC 默认堆太大,DATAS 可以自动缩减

3. 不适用的场景(可考虑禁用)

  • 无法利用释放的内存(如专用机器只跑单进程)(GCDynamicAdaptationMode=0
  • 吞吐量完全优先,不能容忍回退
  • 启动性能极其关键(DATAS 启动只有 1 heap → 多 heap 需要时间)
  • 主要是 Gen2 GC 场景(常见于大量临时大对象分配)
  • 业务自己固定堆数(GCHeapCount 会直接禁用 DATAS)

4. 关键可调参数表(DATAS 相关)

配置项默认值影响范围 / 对应指标公式关系适用场景调优方向
GCDGen0GrowthPercent100%(常数=15)改变 BCD 常数部分,直接增加/减少 gen0 最大分配预算BCD × (GCDGen0GrowthPercent / 100)% Pause Time 高于目标 TCP 且预算太小导致频繁 GC提高 → 减少 GC 次数、提高吞吐;降低 → 更频繁 GC,减少内存占用
GCDGen0GrowthMinFactor0.1控制 min_m(预算乘数下限),防止 LDS 较小时预算过低clamp m 下限(m ≥ min_m),最终 BCD = LDS × mLDS 较小但不希望 GC 过频提高 → 给小 LDS 场景更多预算;降低 → 更积极收缩内存
GCDGen0GrowthMaxFactor10控制 max_m(预算乘数上限)clamp m 上限(m ≤ max_m),最终 BCD = LDS × mLDS 极小时预算过大浪费内存降低 → 限制预算上限
GCDTargetTCP0.02(2%)目标 TCP(GC 暂停时间占比)调整 GC 频率以接近目标 TCP对暂停时间敏感的低延迟业务降低 → 减少暂停;提高 → 容忍更多暂停换取更小内存
conserveMemory5影响 BCD 的基数 (20 - conserveMemory)m = (20 - conserveMemory) × 1000 / sqrt(LDS)对内存敏感(容器环境)提高 → 缩小预算更积极收缩内存;降低 → 增加预算减少 GC 次数
GCDynamicAdaptationMode1是否启用 DATAS1=启用,0=禁用不希望内存缩放或要固定吞吐量设为 0 → 回退为传统 Server GC
GCHeapCount默认=核心数固定堆数,禁用 DATAS 堆自适应机制如果设置则 DATAS 禁用需要固定堆数测试或控制内存不设置 → 让 DATAS 自动调整堆数
GCNoAffinitizefalse控制 GC 线程是否绑定固定 CPU 核心与 DATAS 共存,不影响内存大小但影响线程调度多进程共用 CPU 场景true → 让 GC 线程自由调度

5. 调优步骤建议

  1. 收集 GC 性能数据
    • % Pause Time(近似 TCP)
    • Gen0 Alloc (MB)(近似 BCD)
    • Promoted (MB)(近似 LDS)
  2. 判断瓶颈原因
    • 如果 GC 触发太频繁 → 提高 GCDGen0GrowthPercentGCDGen0GrowthMinFactor
    • 如果内存占用过高 → 提高 conserveMemory 或降低 GCDGen0GrowthPercent
  3. 调整 TCP
    • 吞吐优先 → 调低 GCDTargetTCP(减少 GC 次数)
    • 内存优先 → 调高 GCDTargetTCP(更频繁 GC 收缩内存)
  4. 验证回归
    • 高峰 & 低峰数据对比,确保在不同负载下曲线符合预期

C# .NET 交流群

相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群,但是由于各种原因一直都没创建,现在很高兴的在这里宣布,我创建了一个专门交流.NET 性能优化经验的群组,主题包括但不限于:

  • 如何找到.NET 性能瓶颈,如使用 APM、dotnet tools 等工具
  • .NET 框架底层原理的实现,如垃圾回收器、JIT 等等
  • 如何编写高性能的.NET 代码,哪些地方存在性能陷阱

希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET 问题和宝贵的分析优化经验。目前一群已满,现在开放二群。可以加我 vx,我拉你进群: ls1075 另外也创建了 QQ Group: 687779078,欢迎大家加入。

内容概要:本文介绍了ENVI Deep Learning V1.0的操作教程,重点讲解了如何利用ENVI软件进行深度学习模型的训练与应用,以实现遥感图像中特定目标(如集装箱)的自动提取。教程涵盖了从数据准备、标签图像创建、模型初始化与训练,到执行分类及结果优化的完整流程,并介绍了精度评价与通过ENVI Modeler实现一键化建模的方法。系统基于TensorFlow框架,采用ENVINet5(U-Net变体)架构,支持通过点、线、面ROI或分类图生成标签数据,适用于多/高光谱影像的单一类别特征提取。; 适合人群:具备遥感图像处理基础,熟悉ENVI软件操作,从事地理信息、测绘、环境监测等相关领域的技术人员或研究人员,尤其是希望将深度学习技术应用于遥感目标识别的初学者与实践者。; 使用场景及目标:①在遥感影像中自动识别和提取特定地物目标(如车辆、建筑、道路、集装箱等);②掌握ENVI环境下深度学习模型的训练流程与关键参数设置(如Patch Size、Epochs、Class Weight等);③通过模型调优与结果反馈提升分类精度,实现高效自动化信息提取。; 阅读建议:建议结合实际遥感项目边学边练,重点关注标签数据制作、模型参数配置与结果后处理环节,充分利用ENVI Modeler进行自动化建模与参数优化,同时注意软硬件环境(特别是NVIDIA GPU)的配置要求以保障训练效率。
内容概要:本文系统阐述了企业新闻发稿在生成式引擎优化(GEO)时代下的全渠道策略与效果评估体系,涵盖当前企业传播面临的预算、资源、内容与效果评估四大挑战,并深入分析2025年新闻发稿行业五大趋势,包括AI驱动的智能化转型、精准化传播、首发内容价值提升、内容资产化及数据可视化。文章重点解析央媒、地方官媒、综合门户和自媒体四类媒体资源的特性、传播优势与发稿策略,提出基于内容适配性、时间节奏、话题设计的策略制定方法,并构建涵盖品牌价值、销售转化与GEO优化的多维评估框架。此外,结合“传声港”工具实操指南,提供AI智能投放、效果监测、自媒体管理与舆情应对的全流程解决方案,并针对科技、消费、B2B、区域品牌四大行业推出定制化发稿方案。; 适合人群:企业市场/公关负责人、品牌传播管理者、数字营销从业者及中小企业决策者,具备一定媒体传播经验并希望提升发稿效率与ROI的专业人士。; 使用场景及目标:①制定科学的新闻发稿策略,实现从“流量思维”向“价值思维”转型;②构建央媒定调、门户扩散、自媒体互动的立体化传播矩阵;③利用AI工具实现精准投放与GEO优化,提升品牌在AI搜索中的权威性与可见性;④通过数据驱动评估体系量化品牌影响力与销售转化效果。; 阅读建议:建议结合文中提供的实操清单、案例分析与工具指南进行系统学习,重点关注媒体适配性策略与GEO评估指标,在实际发稿中分阶段试点“AI+全渠道”组合策略,并定期复盘优化,以实现品牌传播的长期复利效应。
### CIFAR-10 数据集的使用方法与下载 对于希望利用 CIFAR-10 进行研究或开发工作的用户来说,了解该数据集的具体结构以及掌握其获取途径至关重要。 #### 下载 CIFAR-10 数据集 官方提供了两种主要形式的数据文件供下载: - Python 版本:`cifar-10-python.tar.gz` - MATLAB 版本:`cifar-10-matlab.tar.gz` 为了方便大多数开发者,通常推荐下载 Python 版本。可以通过访问官方网站[^4] 或者直接通过命令行工具 wget 来完成下载操作。如果选择后者,则可以在终端输入如下指令来自动下载并解压数据包至指定路径 `./data` 中: ```bash mkdir -p ./data && cd $_ wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz tar zxvf cifar-10-python.tar.gz ``` 上述脚本会创建名为 `data` 的新目录(如果不存在的话),进入此目录后再从互联网上拉取压缩后的数据档案,并将其展开以便后续处理。 #### 使用 CIFAR-10 数据集 一旦成功下载并准备好了本地副本之后,就可以着手加载这些图像用于训练机器学习模型了。考虑到兼容性和易用性的因素,在 Python 生态系统内有许多库能够帮助简化这一过程;其中最常用的就是 NumPy 和 TensorFlow/Keras 组合。 下面给出一段简单的代码片段展示如何读入 CIFAR-10 并构建一个基础卷积神经网络来进行分类任务: ```python import tensorflow as tf from tensorflow.keras import datasets, layers, models (train_images, train_labels), (test_images, test_labels) = datasets.cifar10.load_data() # Normalize pixel values to be between 0 and 1 train_images, test_images = train_images / 255.0, test_images / 255.0 model = models.Sequential() model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(32, 32, 3))) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.Flatten()) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(10)) model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=['accuracy']) history = model.fit(train_images, train_labels, epochs=10, validation_data=(test_images, test_labels)) ``` 这段程序首先调用了 Keras 提供的方法轻松地完成了数据导入工作,接着定义了一个小型 CNN 架构并通过 Adam 优化器进行了编译配置,最后指定了迭代次数为 10 轮次开始正式训练流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值