zoukankan      html  css  js  c++  java
  • 【Fundamentals of Windows Performance Analysis】翻译,第五章: 观察资源使用

    第五章 观察资源使用

    之前的章节中,我们已经向您展示了如何识别几种感兴趣区域的重要的类型,进程生命周期,窗口焦点,和UIdelay。我们也通过打开一个大于500MB文件出现未响应的实例向您演示了如何分析这这3种类型。

    基于问题区域的资源占用

    在这个章节中我们将继续通过记事本无响应的实例进行调查,这次聚焦在资源使用方面。特别是通过观察资源如何被使用,我们能够识别更早已经介绍过的典型模式。通过观察这些特征,我们能有机会定位到未响应问题的根因。一旦根因知道了,我们能总结出一组经验方法。

    首先做一个快速的回顾,我们在第一章中介绍了几种在做性能分析中碰到的最常见的典型问题:

    • 饱和 - 某种资源已经占满

    • 争抢 - 多个活动互相争抢一个共享资源,不同的进程彼此打架。

    • 饥饿 - 一个活动已经做好了继续的准备,但是拿不到必要的资源

    • 利用率不足 - 一个活动不能充分利用可用资源

    • 巨大的操作成本(Significant operation cost) -- 活动正在执行的固有成本是巨大的

    让我们看看如何从当前的Notepad例子中识别这些症状。

    转入资源视图

    正如我们在第一章中提到的,许多开发者犯了一个错误,只聚焦于他们自己的代码,而忽视了他们的组件也是一个巨大的生态系统的一部分,他们之间会相互影响。 仅仅聚焦于自己的代码容易忽略一些可能的问题,而这些问题会在之后的生产环境中暴露出来。这些问题通常是因为其他活动的干扰导致的(比如争抢问题),同时你的代码和系统相互影响(比如因为内存压力带来的分页消耗)

    我们因此得到的一个结论就是,我们需要整体去看系统 -- 例如考虑所有的活动组件,观察他们如何消耗各种逻辑的和物理的系统资源。

    注意:比如逻辑资源包括临界区和注册表,物理资源包括存储设备,逻辑核,物理内存,等等。

    就像我们之前罗列的公共资源,就是主要的资源消耗者,这不是一个巧合。组件和关键资源之间相互作用,并且通常瓶颈就在这些资源上。很多组件通过一些物理和逻辑资源发生交互,这是定位这些性能问题的机会。并且我们别忘了,几乎所有的执行都是在一个CPU物理核上执行,CPU恰恰是系统最核心的资源。

    考虑到系统的核心资源,我们要在性能分析中聚焦于观察这些核心资源。在活动区域观察资源的使用情况可以提供至关重要的线索,有助于我们找到可能的根因,就像饱和和竞争问题。自然,这些观察需要一个精确的资源使用情况的统计。

    资源使用典型通过时间花费(比如CPU繁忙时间,磁盘IO服务使用时间),或者资源容量消耗足迹(比如内存足迹,文件大小,网络带宽,等)来衡量。这是性能时间中一次又一次提及的2个概念: 时间和空间。

    回到第4章,我们使用一组公共活动和状态去构造我们感兴趣的区域,这帮助我们更好地聚焦于性能问题。后面的第6章,可以为你自己的场景简单添加一个额外的ETW时间去构造自定义的活动和状态。作为另一种有价值的分析方法,在第6章中我们也可以看到一种公共的练习,通过特殊的代码去构造资源使用,去观察执行时间消耗。这些都统称为以活动为中心的分析

    我们也知道了,观察资源是如何被使用的也是至关重要的。观察资源使用要求操作系统在非常核心的设备栈提供合适的方法。观察系统资源使用视图被成为以资源为中心的分析

    注意:这两种分析方法的另一个不同点在于,以活动为中心的视图提供一个自顶向下的逻辑视图。 这更容易去根据墙上时间去分析当时的场景, 而CPU时间的精确度量让我们更关注执行环境的潜在问题(比如操作系统的行为)

    相反的,以资源为中心的视图提供一个自底向上的视图,让我们关注于系统资源的使用情况。有了资源使用数据,我们能够结合场景进行分析。 我们能看到CPU在任何时间点做的事,然后使用过滤和聚合方法去看到一个给定的代码路径花费了多少CPU时间。

    接下来的5.1实例,我们现在依赖以资源为中心的分析方法去更好地理解在这个事件区域内发生了什么。

    实验5.1: 查看在记事本发生UIdelay时CPU和磁盘资源的使用情况

    在本练习中,我们将利用WPA的资源视图来检查关键系统资源的使用情况。 在这样做的过程中,我们将寻找不同的模式,帮助我们识别与跟踪中的特定活动相对应的感兴趣的区域。

    1. 继续打开4.1实验中的wpr文件
    2. 如图5.1所示
    3. 这个视图在为场景提供上下文信息方面显然是非常有用的——例如,它向我们展示了记事本在时间轴上哪些地方经历了用户可注意到的延迟。 最后,我们真正感兴趣的是为什么记事本没有反应。 要理解这一点,我们需要查看在此时间段内发生了什么,为此,我们首先需要缩放到跟踪中的相应时间间隔。

    重点关注延迟区域

    1. 让我们保留已经添加到这个分析视图中的图,并创建一个新的分析视图,只关注发生Notepad UI延迟的跟踪部分。 为此,在UI delays图形的图例控件中选择notepad.exe process,如图5.1所示。 注意,这将自动选择Notepad没有响应的时间区域

    2. 现在右键单击UI delay图形数据视图中的选择,并在新视图中单击Zoom graph in new view,如图5.2所示。

    注意:注意UI延迟的详细图还包括COM延迟,这是系统上两个组件之间通信的不同类型的延迟。 为了识别影响最终用户的UI延迟,通常可以忽略这些。

    1. 您将看到,如图5.3所示,WPA默认打开analysis(2)窗口。这是一个新创建的分析视图,这个新视图被缩放到Notepad经历UI延迟的时间区域。

    2. 特别地,注意你现在被放大到时间范围[7.919s, 38.766s],这样一些其他的延迟(例如rundll32.exe)不再显示,因为它们发生在这个区域之外。 通过查看时间轴的左边,您可以看到自己在跟踪中的位置,如图5.3中突出显示的那样。 如您所见,时间轴包括开始和结束时间戳以及您所缩放的视图的持续时间。

    3. 尽管如此,在这段时间内确实发生了一些其他的延迟,我们现在真的只想专注于记事本。 没问题-只要右键单击表格中的notepad.exe,然后单击Filter to Selection,如图5.4所示。

    4. 过滤将删除除选定进程外的所有进程。 一旦过滤到notepad.exe,我们就可以查看它的UI延迟,而不会出现其他进程的混乱,如图5.5所示。

    5. 现在我们终于关注了发生UI延迟的时间范围,让我们试着找出在这段时间内系统上发生了什么。 回顾第1章,性能问题的最常见症状包括饱和、争用、缺乏、利用率不足和巨大的操作成本。 让我们试着在我们刚刚确定的感兴趣的区域中找到这些症状的实例。

    根据资源使用情况确定感兴趣的隐式子区域

    1. 在这个场景中使用了三种关键资源:CPU、磁盘和内存。 因为我们打开的是一个500MB的文件,所以记事本必须从磁盘读取500MB的数据到内存。 在整个场景中,它使用CPU, CPU执行代码,协调数据读取,然后将获取的数据呈现给用户。 通过使用WPA,我们可以看到所有这些都在运行,并确定这些资源在哪里被使用。 如果我们在跟踪中发现潜在性能问题的症状(例如,饱和度的情况),那么我们可能就能够确定其背后的原因。

    查看CPU使用率

    1. 让我们从添加CPU图开始。 为此,双击Computation缩略图。 这将为当前组展开一个默认的图,称为CPU Usage (Sampled) - Utilization By Process,如图5.6所示。

    2. 注意,时间轴下面有两个缩略图,表示分析视图中的图表。 您可能还注意到,分析视图中的两个图的大小已被调整为具有相同的垂直间距。 正如我们在附录A(它更详细地讨论了WPA)中所述,这种模式被称为自动布局模式。 现在,我们只需单击分析视图中任何详细图形右上角的restore按钮,切换回手动布局模式,如图5.6所示。 生成的视图将如下所示

    3. 如您现在可能已经猜到的,这个视图显示了场景期间系统上运行的所有进程的CPU使用情况。 显然,除了记事本打开data.txt的活动外,系统上还同时发生了许多其他活动。 如此之多,以至于很难看到他们彼此背后如何影响。 WPA此时可以帮得上忙。 CPU使用情况图的默认图形类型是线形图,其中所有数据序列相互叠加。 在有大量重叠活动的情况下,这种类型的图表并不是非常有效,而且通常会在噪音背后隐藏关键的趋势

    4. 为了解决这个问题,WPA还提供了堆叠线形图,它将它们相互叠加。 让我们切换到这个图形类型,点击Select Chart type图标,然后选择Stacked Lines选项,如图5.8所示。 有关各类WPA图表的详细资料,请参阅附录A。

    5. 该视图显示了0-100%范围内每个进程的CPU利用率。 注意,在(8s~30s)时间范围内, CPU利用率小于25%。 让我们将这个低CPU活动的隐式区域标记为R1。

      注意:正如我们稍后将看到的,WPA实际上会自动将垂直轴最大值设置为数据段中最大的数据点。 这个视图覆盖0%-100%范围的原因是,它只是碰巧在33秒左右有100%的利用率。

    6. 请注意,在随后的时间间隔(30s, ~38s)中,CPU已经达到了50%左右,有一些小的峰值达到了更高的水平。 我们称这个区域为R2

    7. 图5.9展示了我们目前为止定义的两个感兴趣的区域。 正如我们所看到的,大部分的CPU使用来自于记事本——也就是所有被红色覆盖的区域。 请注意,在您的WPA会话中,颜色可能会有所不同,因为在编写本书时,WPA还不支持跨会话的颜色一致性。

    8. 还请注意,虽然CPU在R2期间非常繁忙,但它显然没有饱和(记住,资源饱和意味着资源达到100%利用率)。

    查看磁盘使用率

    1. 正如我们所看到的,无论是R1还是R2, CPU资源都没有得到充分利用。 如果我们更有效地使用CPU, Notepad的UI延迟是否会更短? 也许。 让我们看看系统在这段时间内做了什么。

    2. CPU并不是Notepad用来实现这个用户场景的唯一资源。 让我们看看另一个关键的系统资源—辅助存储设备,即硬盘驱动器。 我们将通过双击存储图来将磁盘使用情况图添加到分析视图中。 这将为该组添加默认的图形,称为Disk Usage - Utilization By Disk,如图5.10所示。

    3. 显然,在整个UI延迟期间,磁盘非常繁忙。 注意,这个默认图显示了当时系统上并发执行的所有活动的总磁盘使用量。

    4. 这个累积磁盘使用情况视图对于这个场景还不是特别有用,因为其中没有表示单个进程。 为了更好地了解到底是谁导致了所有这些活动,让我们切换到按进程显示磁盘使用情况的视图。 要做到这一点,您需要使用presets预定义的数据视图,允许在各种有用的配置中显示相同的底层数据。

    5. 例如,假设我们想要对比按应用程序名称分组的存储I/O数据和按文件名分组的存储I/O数据。 然后我们将对两个不同的视图感兴趣—第一个视图,它允许您查看每个特定应用程序访问的所有文件,第二个视图,它允许您查看访问每个特定文件的所有应用程序。 预设允许你在这些有用的视图之间快速切换,只需几个鼠标点击。

    注意:在附录A中,我们教你如何创建自己的预置以及自定义现有的预置。 现在,让我们使用WPA自带的一些内置预设。

    1. 要在Disk Usgae中使用预置,请单击其活动预置的名称(Utilization by Disk),它将为您呈现该详细图的所有可用预置集。 接下来单击Utilization by Process, Path Name, Stack,,如图5.11所示。

    注:预设值和/或它们的名称可能会在WPA的主要版本之间改变。 如果你找不到这个预设,那就找一个在语义上与之相似的预设标题(例如,一个预设标题表示它将按进程罗列磁盘使用情况)。

    1. 当您选择新的预设时,您将再次看到相当多的重叠活动,即很难区分各个进程的磁盘使用情况。 让我们重复步骤(17)中图5.8所示的步骤,使用堆叠线形图切换到复合磁盘使用视图。 得到的复合磁盘使用视图如图5.12所示。

    2. 注意,复合视图加起来就是我们在图5.10中看到的总磁盘使用情况的线形视图。 但是,与前一个视图不同的是,复合视图可以很容易地看到哪个进程对总的磁盘使用情况做出了多大的贡献。 注意,Notepad的CPU使用用红色表示,这里它的磁盘使用用蓝色表示。 在写这本书的时候,WPA并没有为过程提供交叉图形的颜色一致性。

    3. 现在让我们看看使用这个视图可以识别哪些感兴趣的区域。 第一个突出的区域位于迹线开始的~7 ~15秒之间。 正如你所看到的,在这个区域,除了记事本的I/O之外,还发生了许多其他的I/O。 事实上,记事本似乎只利用了大约60%的可用磁盘带宽。 在这个时间间隔内,Notepad和访问磁盘的其他进程之间出现了明显的I/O争用。 我们称这个区域为R3。 我们将在第10章详细分析I/O争用

    4. 15秒开始,一直到31秒,记事本似乎在很大程度上独占了磁盘。 但是,请注意,即使在这个时间窗口内,它的磁盘利用率也在75%左右,从未使磁盘完全饱和。 我们称这个感兴趣的区域为R4。

    5. 最后,从大约31秒开始,Notepad进程完全没有磁盘利用率。 我们称最后一个区域为R5

    6. 图5.13注释了这3个区域:

    复合观察CPU、磁盘视图

    1. 现在让我们把这两张图片放在一起,做一些侦查工作。 在这个单独的视图中,如图5.14所示,我们可以看到在Notepad的UI延迟期间CPU和存储设备的使用情况,以及我们刚刚确定的感兴趣的区域。

    2. 您很快就会注意到R3和R4的区域,实际上,与R1表示的区域完全对齐。 此外,我们看到R2 = R5。

    3. 这是巧合吗?。 在系统活动的不同视图中,通常会出现相同的感兴趣区域。 考虑到系统资源的并发利用之间的逻辑关系,这并不奇怪。

    4. 为了更好地理解这些隐式关系,我们需要了解在确定感兴趣的区域期间系统上发生了什么活动。 随着您对性能分析越来越熟练,您会注意到分析人员经常使用场景知识来确定在这些感兴趣的区域中执行了什么活动。 回想一下,我们的场景是用记事本打开一个大文件。 这意味着文件需要首先从磁盘读取,然后显示在屏幕上。 我们可以马上观察到,在区域R1(即R3 + R4)中,我们的记事本使用的是低CPU使用率和高磁盘使用率。 这很有意义:当记事本将data.txt文件读入内存时,它并没有花费太多的时间来利用CPU

    5. 类似地,一旦记事本完成了文件的读取(即记事本的磁盘使用消失),它的CPU利用率就会上升。 这映射到重叠区域R2和R5(即这两个区域代表相同的时间间隔)。 为什么这里的CPU使用率会达到峰值? 好吧,如果不能访问代码,这很难确定,但根据良好的经验猜测,它会格式化检索到的数据,以便将其呈现给用户

    现在,我们已经使用CPU和磁盘资源使用视图来确定Notepad的UI延迟中感兴趣的主要子区域,让我们进一步研究其中一些。

    分析识别到的感兴趣区域

    R3区域: IO争抢

    我们要看的第一个区域是区域R3,在这里您可以看到几个进程在争夺磁盘时间(其中大约有4或5个进程获得了足够的磁盘时间,在图中脱颖而出)。

    1. 当这么多进程都在争夺一个共享资源(如磁盘)时,您预计会发生什么? 它们可能访问不同的文件,导致存储设备交错传入的I/O请求。 正如我们将在第10章中看到的,交错访问不同的文件可能会极大地降低I/O吞吐量。 为了确认这一点,让我们放大区域R1,在磁盘使用Disk Usage图形数据视图中选择8秒到30.5秒,然后右击并选择zoom,如图5.15所示。 注意,您也可以使用Ctrl+。 缩放选区的快捷方式。

    注意:在ssd上,在类似的工作负载引起的I/O争用期间,您可能会观察到不同的行为。 详见第10章。

    1. 现在添加Disk Usage的第二个实例,并将其切换到进程吞吐量视图Throughput by Process, IO Type,如图5.16所示。
    2. 如您所见,Notepad读取500MB data.txt文件的吞吐量在区域R3中明显低于在区域R4中。 事实上,在争用区域R3中,吞吐量比在区域R4中低约60%,在R4中,Notepad实际上并没有与磁盘上的其他进程竞争。 如果没有争用,我们的文件读取速度会更快——事实上,我们甚至可以推断记事本加载文件的速度会快多少。 区域R3的平均吞吐量(~ 27mb /s) / (~ 11mb /s) =区域R4的2.46倍。 因此,如果没有I/O争用,区域R3将是~7.4s / ~2.46 = 3秒,而不是7.4s。 换句话说,在记事本中打开文件时,仅仅因为区域R3中的争用,我们就损失了大约4.4秒。

    注意:你可能想知道我们从哪里得到这些数字。 你可以用图表来观察这些,但当然得出的数字并不准确。 获得这些指标的一个更好(精确)的方法是使用这些图表附带的表,我们将在本书后面介绍这些表

    注意:当我们使用术语MB时,我们指的是MiB。 从技术上讲,MB单位对应10的6次方字节,而MiB单位对应 (2的10次方)2 =(1024)2,在Windows系统中,MiB一直使用MB表示。 这包括我们正在使用的工具,因此在本书中,我们将采用KB表示1024字节,MB表示1024 KBs。

    区域R6:硬盘间歇性减速

    1. 吞吐量的下降通常是争用问题的良好指示。 当然,也可能有其他原因,比如间歇性的减速或无效的访问模式。 事实上,看看17秒到18秒之间的区域R6,我们可以看到吞吐量的巨大下降,如图5.17所示。 还要注意(来自图5.16),在此期间,磁盘完全饱和。

    2. 这显然不是一个争用问题。 那么为什么吞吐量会下降呢? 为了找出答案,让我们通过选择该区域,右击并放大16.5秒到18.5秒之间的时间间隔,来进一步研究区域R6,如图5.18所示

    3. 通过观察,您可以看到在17.2秒到17.7秒之间的磁盘I/O吞吐量非常低,仅为~2MB/s——比周围I/O的吞吐量低一个数量级。

    4. 如果我们能看到在这个低吞吐量区域发生的底层I/ o不是很好吗? 多亏了Windows中的磁盘I/O插装,WPA可以分别向我们展示每一个I/O。 切换到混合布局模式,展开详细图显示对应的表格,如图5.19所示。

    5. 新显示的表格显示了WPA用于构造图形数据视图的底层数据。 在图5.19的表格中,可以看到三个数据序列,表示在此时间间隔内启动磁盘I/ o的三个进程。 注意,表格视图首先以进程为中心,其次是I/O类型。 我们可以看到,Notepad .exe发起的所有I/ o都是读取(这是预期的,因为Notepad此时正在读取data.txt)。 在这本书的后面,我们将进一步了解WPA如何处理数据。 现在,让我们利用这个已经存在的枢轴视图。

    6. 要查看从notepad.exe中读取的数据,让我们通过单击折叠的组图标来展开各自的组,如图5.20中的步骤1所示。

    7. 注意,默认情况下,这个视图排序的所有I / o值的Disk Service Time,按照降序排列,顶部的最长的I / Os(你可以告诉这列数据是按如果你找小下箭头正上方列标题的文本)。 立即,最上面的读操作在~457ms或大约半秒(注意IO TimeDisk Service Time列是以微秒显示的)。 为了更好地查看这个缓慢的I/O,让我们再次单击相应的行来选择它。 然后,WPA将交叉选择服务慢读I/O的时间间隔,如图5.20所示

    8. 注意,除了依赖于数据系列的选择,您还可以通过时间选择手动选择感兴趣的区域,方法是在图形中间隔的开始处左键单击,并将选择拖到间隔的末尾,如图5.21所示。

    9. 然后,WPA将您的手动临时选择与下表中基础数据系列的选择同步。 换句话说,如果您手工进行临时选择,那么表中的相应数据也将被选中,这在您的分析中是非常宝贵的。 我们将在整本书中依赖于选择同步,并在附录A中深入讨论它,当我们更仔细地看WPA时。

    10. 让我们仔细看看这个表格告诉了我们什么。 对于初学者,我们可以推断,在这个时间范围内,Notepad发起的每次读取都正好是1MB大小(注意size列中的值,1024的平方 = 1,048,576字节)。

    11. 您可以在这个视图中看到其他有趣的指标。 例如,我们还可以看到所有这些读具有Normal I/O优先级。 此外,QD/I -Queue Depth at Init Time 列的队列深度显示每个I/O请求前面有多少I/O。 我们将在第10章和第11章更深入地讨论这些和其他重要的特定于磁盘的概念。

    12. 正如我们所看到的,读吞吐量下降的根本原因是由于单个缓慢的磁盘I/O花费了近半秒的时间(即巨大的操作成本)而导致的间歇性放缓。 造成这种延迟的原因可能有很多,包括驱动器逻辑欠佳和磁盘固件试图执行某些维护任务或由于故障扇区而导致的数据重新映射。

    13. 在这种特殊情况下,我们可能会处理一个“数据刷新”命令,该命令劫持/抢占了记事本的读I/O,导致它支付了刷新操作的成本。 我们将在第10章详细讨论磁盘刷新及其干扰I/O的能力。 最重要的是,物理设备是不完美的,有时我们最终会遇到像这样的延迟

    注意:像这样的间歇减速当然是非常常见的,只要你把网络加入到这个等式中。 与本地存储不同,使用网络文件共享可能导致大量的I/ o,并且延迟很高。 如果您的场景支持网络文件,那么您需要确保对它进行了测试

    1. 现在我们已经研究了R6区域的间歇减速,让我们缩小到R1区域(即R3 + R4)。 有很多方法可以回到之前的视图。 您可以右键Unzoom上下文菜单选项(也可通过Ctrl+Shift+ -访问),然后重新缩小到R1。 或者,您也可以仅仅依靠Select Time Range…上下文菜单选项,指定区域R1的开始和结束时间,然后缩放到选择(我们将在本练习的后面展示一个示例)。 另一种取消缩放的方法是在鼠标滚轮上使用Ctrl键,这可以让您同时放大和缩小,并将鼠标光标作为焦点。

    2. 回想一下,我们曾经研究过区域R3中的I/O争用。 因为,正如我们刚刚在R6区域看到的间歇性放缓,吞吐量的下降可能并不实际表明I/O争用,另一种检查是否正在处理争用的方法是利用WPA中的另一个非常酷的图。 这个图显示了时间轴上的每个磁盘I/O,以及哪个进程启动了它。 它甚至显示了这些I/ o之间的转换。 让我们通过添加磁盘使用情况图的另一个实例并选择Disk Offset预置来快速查看这个图。 您应该会得到一个如图5.22底部所示的图表。

    注意:正如我们在附录A中提到的,磁盘偏移是状态转换图类型的一个实例。 注意,上面没有“选择图表类型”选项。 您可能已经注意到,它在焦点图中的UI Delays、Processes和Window中也缺失了,这些图都是甘特图类型的实例。

    1. 正如您可以在磁盘偏移图中高亮显示的区域中看到的,有许多紧密聚集的活动正在发生。 相比之下,在I/O争用停止后发生的磁盘活动,磁盘是自由的,可以为来自一个发起者的请求提供服务——例如记事本。 我们将在第10章详细讨论磁盘偏移图

    2. 这里存在一个潜在的改进机会:我们对区域R3中的I/O争用的研究表明,通过解决该区域中的I/O争用,我们可以获得大约4.4秒的时间。

    磁盘利用率不足

    现在我们把注意力集中在区域R4上,它紧跟着我们刚刚看到的区域R3。

    1. 在图5.22中您注意到的第一件事是,尽管R4区域的吞吐量显著提高(大约提高了250%),但磁盘利用率仍然没有达到平均75%以上。 这是怎么回事? 回想一下第1章,解决一个原因并不一定能解决手头的问题,相反,你可能会面临另一个原因。 通常被称为剥洋葱,这种现象也被称为级联链。 这是一种连锁反应吗?

    2. 在R3区域的I/O争用期间,Notepad能够使用高达55%的磁盘。 剩余的磁盘时间用于服务来自其他进程的请求。 有人可能会认为,如果没有I/O争用,记事本的磁盘利用率现在将达到100%。 然而,正如我们刚才看到的,它的利用率仅提高了~75% - ~55% = 20%,留下25%的未使用。

      注意:你可能想知道我们是如何找到这些数字的(即55%和75%)。 一种方法是使用显示前面看到的各个I/O操作的表。 您可以使用它访问底层数据,并轻松地计算许多不同的有用指标,包括这些指标。 您还可以使用一种巧妙的技巧,即水平调整图形数据视图的大小,使其足够小,以便可视化聚合消除数据中的差异,并显示平均值。 通过水平调整WPA窗口的大小并观察线形图的变化,您可以自己尝试一下。 不幸的是,在撰写本书时,WPA还不支持对图形数据视图中用于聚合的时态桶进行定制,因此调整图形大小是影响图形聚合的唯一方法。

    3. 如果Notepad在这里充分利用了磁盘,那么在区域R4期间,它将以25%的速度读取文件,从而加速~15s * 25% = ~3.75秒。 那么是什么阻止了我们充分利用磁盘呢?

    4. 当磁盘繁忙处理未处理的I/ o时,磁盘处于饱和状态。 当它的利用率徘徊在75%左右时,这意味着大约有25%的时间它不繁忙。 这表明记事本的读取请求在时间轴上不是完全相邻的,并且在它们之间有间隙。 对于发生这种情况的原因,这里只有这么多的选择:要么是记事本在CPU上做了一些工作,而在那些时候没有让磁盘繁忙,要么它只是空闲,什么都不做。 第一种情况是过度序列化导致利用率不足(即不必要地序列化本可以并行化以更有效地利用资源的内容),而第二种情况是不必要的等待导致利用率不足。

    5. 如果它实际上正在使用CPU,那么它可能正在处理传入的数据和/或准备下一个读操作,这可能解释了为什么它没有使磁盘繁忙。 让我们放大区域R4,看看在这个时间间隔内CPU和磁盘上都发生了什么。

    6. CPU Usage(Sampled)Disk Usage图放入分析视图,并将视图放大到区域R4。 实际上,让我们进一步放大位于15.2秒到30.5秒之间的区域。 结果视图应该类似于图5.23所示。

    7. 您会注意到,在这两个图中对比记事本活动并不是特别容易,因为这些图还包括来自其他流程的活动。 为了解决这个问题,让我们通过在左侧的legend控件中选择Notepad .exe来过滤这些图形,然后右键单击,然后在上下文菜单中选择Filter To Selection选项。 您将得到如图5.24所示的视图。

    注意:您可能已经注意到CPU Usage图的y轴已经更改为包含更小范围的值(最大20%利用率)。 这是由于WPA的自动缩放,它在图中为垂直轴的顶部范围选择最大值。

    1. 堆叠线视图设计用于显示多个数据系列,但在这里并没有增加多少价值,因为每个图形只有一个系列,所以让我们通过单击图标并选择line选项切换回常规线视图。 结果视图如图5.25所示。 注意,在许多情况下,磁盘使用高峰CPU使用低谷之间存在密切的相关性,这表明由Notepad发起的CPU和磁盘活动是串行化的

    2. 虽然图5.25暗示了CPU和磁盘使用之间的密切关系,但在这个放大级别上,仍然很难看出是否确实如此。 为了看得更清楚,让我们放大看得更清楚。 从图5.23的整体CPU和磁盘使用情况来看,20秒到20.5秒之间的时间区域似乎几乎没有其他进程的干扰,可以作为一个很好的数据样本来查看。 为了选择这个时间范围,让我们使用上下文菜单中的select time range…选项,然后放大它,如图5.26所示。

    3. 结果视图如图5.27所示。 在这里,很容易看到记事本的CPU和磁盘活动的交错。

    4. WPA有一种更好的方法来在时间轴上可视化活动的持续时间,使用专门为此设计的图形类型—甘特图(我们以前已经看到过这种图形类型,当我们查看进程生存期、焦点窗口和UI延迟时,还记得吗?) 对于磁盘,您可以通过按IO类型将磁盘使用率图切换到Activity by IO Type, Process来获得它。 对于CPU,您需要引入一个不同的CPU使用率图—在其标题中使用(Precise)(即精确的)而不是(Sampled)。 接下来,选择Timeline by Process,Thread,线程预设和过滤器只查看notepad.exe进程。 结果视图如图5.28所示。 注意,我们再次过滤了CPU Usage,只看notepad.exe中的活动。

    注意:我们已经在第2章中提到了采样和属性(精确)CPU使用测量之间的基本区别。 在讨论处理器性能分析时,我们将在第7章、第8章和第9章详细讨论这两种CPU使用度量方法。

    1. 你可以清楚地看到,记事本不是只从磁盘读取,它也正忙于使用CPU。 事实上,交错的图案是完美的,没有重叠。 看起来就像Notepad首先读取数据,然后处理它,而不要求磁盘读取更多数据。 正如我们已经说过的,这是一个典型的过度序列化的例子。 事实上,正如我们将在本书后面看到的,这种症状是由于使用了同步读取,导致磁盘资源利用率不足如果Notepad使用了I/O交错,它可能会更好地利用存储设备。 我们将在第11章讨论异步I/O时进一步讨论这个问题以及如何补救。

    2. 这里还有另一个很好的改进机会:我们对磁盘利用率不足的调查表明,通过解决导致R4区域磁盘利用率不足的过度序列化问题,我们可以获得大约3.75秒的时间

    区域R2: CPU利用率不足

    1. 在上一节中,我们查看了R4区域的一个磁盘利用率不足的案例,这是由Notepad启动的CPU和磁盘活动的同步交错造成的。 在本节中,让我们看看另一个关键资源未得到充分利用的情况。 即,我们在图5.14中的R2区域中看到的~50%的CPU利用率(为了方便您,在图5.29中再次显示)。

    2. 让我们从放大时间区域R2开始。 由于此时没有来自记事本的磁盘活动,我们只需要查看它的CPU使用情况。 为此,让我们看看复合整体CPU使用视图,以及一个过滤到只是notepad.exe进程。 注意,您既可以从graph Explorer中添加两个CPU Usage图形实例,也可以在右键菜单中使用duplicate graph选项在视图中复制图形(按下Ctrl按标题拖放图形也可以正常工作)。 我们希望其中一个实例显示附加的CPU使用情况(使用堆叠线形图模式),另一个实例只显示notepad.exe进程的CPU使用情况(使用过滤)。 结果视图应该如图5.30所示。

    3. 你注意到的第一件事是,记事本的CPU使用率稳定地固定在50%的CPU。 因此,有人可能会说,如果Notepad在这里完全使用CPU,它将以两倍的速度完成它的工作。

    4. 你可能会问,为什么是50%呢? 正如我们将在第7章中看到的,这些试图保持CPU繁忙的活动,它们的CPU利用率往往有非常不同的级别:50%、25%、12.5%,等等。 这些数字有什么共同之处? 它们是2的幂的倒数1/2 1/4 1/8等等。这是因为现代系统通常有多个处理器,处理器的数量通常是2的幂(正如你可能知道的,我们将在第7章讨论,这些处理器可以是不同的CPU或CPU上的核心,这就是为什么我们通常把他们称为逻辑处理器)。 如果给定的活动正在利用这些处理器中的一个,您将看到它显示为上述百分比级别中的一个,这取决于系统有多少处理器。

    注意:不是所有的cpu都有两个核的能力。 市场上有些产品的核心计数不是两倍的。

    1. 让我们通过研究WPA中一个非常有用的特性,即通过Trace菜单访问的System Configuration,来看看这是哪种CPU,如图5.31所示

    2. 然后,WPA将打开一个单独的视图,其中包含描述收集当前打开跟踪的系统的关键属性摘要。 它看起来将如图5.32所示

    3. 正如这个跟踪的系统配置视图所示,我们的复制系统有两个逻辑处理器,这解释了为什么记事本的CPU使用率固定在50%。

    4. 这是一个典型的由于Notepad代码缺乏并行性而导致的CPU利用率不足的情况:也就是说,Notepad的代码一次只能在一个逻辑处理器上运行。 为了提高Notepad对CPU资源的使用,它需要利用多线程(我们将在第6章到第9章讨论的另一个概念)。

    5. 注意,虽然记事本的CPU使用率相对稳定,但总CPU使用率有时会从50%飙升至100%。 其中一个突出的例子是32.5s到33s之间,如图5.33所示。 我们称这个区域为R7,在下一节讨论它。

    6. 这是另一个潜在的改进机会:我们的CPU未充分利用的调查表明,并行的CPU活动地区R2我们可以获得尽可能多的[(39 - 30.5)s - (~ 1 s的并发活动不太可能受益于记事本的并行化))/(2处理器)= ~ 3.75秒。

    区域R7:附加CPU饱和

    1. 接下来让我们更深入地了解R7区域,以便更好地理解附加CPU饱和的本质。 这一次,我们将使用三种不同的CPU Usage (Sampled)图表副本,以及三种不同的预设值:
    • 通过Utilization By CPU展示CPU使用率(跨所有处理器)
    • 通过Utilization By Process展示进程情况(跨所有处理器)
    • 最后一个显示的是notepad.exe进程的利用率(即前面的图表只过滤到一个进程)。
    1. 我们将切换前两个图,使用堆叠线形图来帮助传达CPU用户的附加特性。 这个视图,放大到32.5秒到33秒的时间范围(即R7区域),应该如图5.34所示。

    注意:正如我们前面提到的,WPA在其图中并不使用一致的颜色,因此在第一个图中为CPU 0选择的颜色与在第二个图中为表示notepad.exe活动选择的颜色没有逻辑关系。

    1. 回想一下,一个典型的单个存储设备的多个并发用户往往会导致I/O争用,从而降低彼此的执行速度。 这是因为大多数消费者存储设备不擅长同时处理多个请求,而是倾向于将它们排队。 这对于旋转设备(HDD)的影响要大得多,但是SSD也会受到并发使用的影响。

    2. 对于CPU来说就不一定是这样了。 只要可用的处理器数量大于并发活动的数量,每个活动就可以在单独的处理器上运行,它们通常不会相互干扰

      注意:即使有足够的逻辑处理器让每个并发执行的活动并行运行,它们仍然可以通过各种次要效应(例如缓存失效、内存总线争用等)相互影响。 讨论这些超出了本介绍性文本的范围。

    3. 让我们仔细看看R7区域的两个子区域,如图5.34所示,即:

    • R8,表示32.70秒到32.75秒之间的子区域,其中notepad.exe的CPU使用率一路降至零。

    • R9,表示notepad.exe CPU使用率稳定在50%的子区域。

    1. 具体来说,让我们对比R8和R9区域。 在这两个区域中,我们看到TiWorker.exe与notepad.exe同时运行。 事实上,在R9中,svchos .exe也加入了进来,在32.87s左右取代了TiWorker.exe。 有了这些争论,您可能会认为记事本的CPU使用量会下降。 你甚至可能认为这就是为什么Notepad的CPU使用率在R8中下降的原因。 也就是说,您会注意到在R8期间没有其他进程的CPU占用显著增加,因此一定有其他东西偷走了这些CPU周期。

    2. 另外,有趣的是,在R9中,尽管存在更多的并发CPU用户,Notepad的CPU利用率没有错过任何一个心跳,并稳定在50%。

    3. 事实上,否决Notepad在R8中使用CPU的原因是底层系统本身——即操作系统和安装在其上的驱动程序的组合。 正如我们将在第9章中看到的,这些活动被称为DPCs(延迟过程调用)和ISRs(中断服务例程)。 当我们仔细观察操作系统是如何执行其低优先级任务时,我们将向您展示如何可视化这种类型的CPU消耗。

    注意:正如您可以想象的那样,一个行为不端的驱动程序很容易对终端用户系统造成严重破坏。 作为一个开放平台,每个Windows PC都有第三方(即非微软)驱动程序。 甚至在用户开始在他们崭新的机器上安装定制驱动程序之前,这些驱动程序就已经预装在他们的系统上了。 这些设备包括网络摄像头、指纹阅读器和最新的显卡。 其中许多驱动程序甚至不是原始操作系统映像的一部分,而是由OEM在工厂车间添加的。 不幸的是,并不是所有这些驱动程序都没有bug。 虽然微软已经为内置驱动程序创建了一个非常彻底的质量控制过程,但第三方驱动程序的质量控制,包括那些通过WHQL认证的驱动程序(一个强加于任何想要通过Windows Update共享的驱动程序的质量控制过程)可能没有那么严格。

    1. 考虑到区域R7中的每个进程(例如svchos .exe, TiWorker.exe, notepad.exe)没有占用超过50%的CPU时间,我们可以推断出它们的执行可能在同一时间局限于单个逻辑处理器(回想一下这个系统有两个逻辑处理器)。 这是否意味着这些过程没有相互影响 ?

    2. 正如我们所看到的,有三个进程占用CPU时间,而只有两个逻辑处理器可用,因此可能存在一些争用。 同时,notepad.exe似乎已经在整个区域R7中获得了它的CPU时间份额,不受其他两个进程的影响。 如果我们通过使用多个处理器来解决Notepad在该区域的CPU利用率不足问题,那么Notepad .exe上的其他两个进程的CPU争用的影响可能会在这个特定的系统上变得更加明显。

    3. 这里有2个关键点:

      • 多个并发CPU用户并不一定会窃取彼此的CPU周期,而且与大多数磁盘不同,也不一定会导致争用(注意:有些磁盘也可以处理多个并发访问)。
      • 即使您有足够的处理器来满足所有并发运行的进程,系统本身也可能占用您的资源(毕竟,系统代码和驱动程序也在CPU上运行)。

    我们将在第7章和第9章中更详细地讨论附加CPU饱和的相互作用和特权代码执行的影响。

    实验总结

    哇,真是一个相当长的练习! 以资源使用为中心的分析无疑是一个丰富的见解来源。 我们所获得的新发现的理解在寻找如何改进它方面是无价的。 具体来说,解决区域R3中的I/O争用(不同文件IO争抢导致吞吐量变低)、区域R4中的磁盘利用率不足(与CPU串行同步处理导致无法100%利用磁盘资源)以及区域R2中的CPU利用率不足(没有多线程并发处理,只在单线程上跑),可以使场景的速度提高大约4.4s + ~3.75s + ~3.75s = ~11.9s,或者大约12s,这是一个非常可观的改进。 整个UI延迟时间约为30.85s,减少12s将提高约40%!

    注意,其中一些改进比其他的更容易处理。 当面临多种选择时,选择正确的补救策略需要考虑权衡和成本/风险/效益分析。

    当然,这些改进只是对记事本开发人员在加载大文件时决定采用的现有方法进行了微调。 这个故事鼓舞了士气。 虽然我们可以让自己忙于微调现有实现的缺点,但退一步考虑完全不同的方法可能会付出更多的代价。

    通过重新审视整体设计,或许,选择了“滑动窗口”访问模型数据文件(这将只需要加载尽可能多的数据显示当前窗口,或许,更方面),我们可以进一步提高性能大文件加载的数量级。 记住这一点很重要——有时,与其投资数千美元修理你的旧旧车,不如考虑买一辆新车。

    到目前为止,我们已经通过查看其CPU和磁盘使用情况,设法找到了许多加速记事本打开大文件的机会。 在这个跟踪过程中是否隐藏着更多的问题,以及由此产生的改进机会? 在下一个练习中,我们将看看另一个共享和有限的关键系统资源:内存

    对于CPU和磁盘,如果您使用资源,则通常会占用场景所需的时间。 在内存方面,事情会涉及更多。 回想一下,内存容量压力会导致分页,这可能会导致I/O争用。 因此,在可用内存较低的系统上占用大量内存,可能会潜在地影响与辅助存储设备(磁盘)交互的几乎所有场景。

    随着典型用户系统在内存容量方面的最新进展,由于普通用户场景的功能需求而导致的内存压力实际上并不常见(在代码中缺少内存泄漏)。 而且,在设备耗尽内存之前,简单地使用更大的内存分配并不一定导致更慢的执行

    然而,大量的分配和回收可能会影响你的活动的持续时间,并可能导致碎片问题。

    注意:无效的分配模式可能导致用于满足这些分配请求的内存池/堆的碎片。

    此外,尽管单一的分配和重新分配指令并不昂贵,但如果执行得足够多,你就会遭受千刀万剐的痛苦。

    另一个需要考虑的问题是访问内存的方式。 与辅助存储设备一样,一些内存访问模式可能比其他模式更有效(这与处理器执行优化和缓存有关)。

    注意:如何访问分配的内存通常也会影响执行时间。 导致CPU缓存丢失的内存访问模式会导致CPU从内存中获取数据,这可能比从缓存中访问相同的数据慢几个数量级

    在所有这些考虑中,内存容量压力可以说是最容易重现和识别的。 在下一个练习中,我们将向您展示WPA图,您可以使用它来查看记事本应用程序的内存占用情况。

    在不深入研究内存子系统操作的这些更高级的怪癖的情况下,让我们回顾一下记事本的总体内存使用情况,看看它在使用系统内存方面是否有改进的机会。

    注意:虽然CPU和磁盘争用在典型的用户场景中是非常常见的,但我们有时也会看到由于缓存一致性问题导致的内存争用,当多个具有大量活跃使用内存占用的进程在L2/L3缓存中不断地使彼此的缓存线路无效时。 由于对主存的访问要比对CPU缓存的访问慢几个数量级,这可能会对整体性能产生重大影响。 您通常会看到CPU使用量的增加,因为在这些场景中,内存访问指令最终会“花费”更多的周期。 查看内存争用超出了本文的介绍范围。

    实验5.2 通过UIdelay例子聚焦内存使用

    在本练习中,我们将利用WPA的内存使用图来检查记事本的内存占用情况。在这样做时,我们将再次寻找不同的模式,以帮助我们识别与跟踪中的特定活动相对应的感兴趣的区域。

    1. 再次打开etl文件

    2. 打开到5.14步骤,应该类似下图:

    交叉使用CPU/Disk/Memory视图

    1. 让我们添加一个整体内存使用视图,以便在记事本打开文件时对比系统内存使用情况。为此,在Graph Explorer中展开Memory组,并将Virtual Memory Snapshots图添加到分析视图中,如图5.36所示。

    2. 现在让我们仔细看看内存使用图,特别是图5.37中所示的感兴趣的区域。

    3. 观察记事本在8秒到15秒的内存使用量增长速度比15秒到31秒要慢。 然后您可以看到它的内存使用量在大约31秒时显著下降,从那时起,系统内存使用量保持稳定。 这将时间轴划分为四个不同的兴趣区域,如图5.37所示,即:

    • R10 = (8s,15s): 较低的内存分配速率

    • R11 = (15s, 30s): 高内存分配速率

    • R12 = (31s, 38.5s) : 恒定的内存占用

    • R13 = (31s) : 突然的内存下降

    注意:R13是一个有趣的区域,描述了一个表示状态转移(即内存回收)的时间点附近。

    1. 由于我们不再对竞争的活动感兴趣,让我们将虚拟内存快照图形切换到线形图形模式,然后过滤到notepad.exe进程在系统上做什么,如图5.38所示

    2. 哎哟! 看纵轴,我们可以看到记事本分配了1.5GB的内存来读取500MB的文件。 你不觉得这有点低效吗? 好吧,至少在31秒的时候还给了我们500MB。 你能猜到记事本在那里做什么吗?

    3. 另一个有趣的观察是,在Notepad与其他进程争用磁盘时间的时间段内,它的内存分配增长会受到这种争用的显著影响。 更具体地说,在R10区域(与磁盘I/O争用区域R3相同)中,内存分配率约为R11(与磁盘I/O争用区域R4相同)的40%。 实际上,我们可以通过猜测Notepad内存分配率与它的读I/O率成正比来解释从R10到R11的速度提升。 考虑到I/O争用降低了Notepad读取文件的能力,Notepad最终也以较慢的速度分配内存。

    4. 事实上,请注意内存分配增长率大约是我们前面看到的读吞吐量的三倍。 要估计区域R10的内存分配增长,可以放大该区域,观察我们在最初7秒内分配了226MB,相当于~33MB/s。 类似地,放大到R11区域,我们可以看到,在这16秒内,我们已经分配了1,437MB - 228MB = 1,209MB,这大约是75MB/s的分配速率。

    5. 回想一下,R3有11MB/s的读吞吐量,而R4有25MB/s的读吞吐量。 请注意,~33MB/s的分配速率等于3 * ~11MB/s的读吞吐量。 此外,请注意~75MB/s的分配速率等于3 * ~25MB/s的读吞吐量。 在下一章中,我们将看到这个3的比例因子是怎么来的。

    推论

    总结一下,当读取data.txt时,Notepad的内存使用量达到了1.5GB。 在读取该文件后,记事本将500MB的内存归还给系统,并稳定在1GB的内存占用。 在此场景中,内存分配增长率大约是读吞吐量的三倍。 在I/O争用期间,较慢的读吞吐量导致内存分配增长明显较慢。

    现在应该很清楚,Notepad并没有非常有效地使用内存。 有了5GB的系统内存,我们很幸运有足够的可用内存来满足记事本的饥渴的内存习惯。 猜猜如果我们的系统没有足够的可用内存会发生什么? 当Windows系统耗尽可用的物理内存时,它开始严重依赖磁盘上的页文件来尝试和补充物理内存的容量。 当这种情况发生时,由此产生的分页活动可能会使整个系统的用户体验减慢几个数量级。

    注意:你在系统配置中看到的5072mb是(5* 1024 - 48)MB,其中48MB是系统预留的。

    那么,为什么记事本要向我们征收如此高的内存税(总共300%)呢? 正如我们所看到的,在Notepad完成从磁盘读取文件后,它将500MB的文件大小还给系统。 这表明记事本将整个文件的内容读入内存,然后在处理完数据后,将这些内存释放回操作系统。 剩余的1GB内存使用正好是文件大小的两倍。 两个因素是巧合吗 ?

    为了更好地理解Notepad实际上在做什么,我们需要看看在将data.txt读入内存时执行了哪些代码。 您可能认为下一步是记事本代码的内省代码检查。 幸运的是,使用ETW,您不需要返回实际的源代码。 在下一章中,您将看到,我们可以依靠一种称为基于堆栈的CPU采样的技术来了解Notepad在打开它的大文件时执行的代码。 也许通过观察它在做什么,我们就可以推断出对额外内存的需求从何而来。

    关键点

    复习一下我们学过的知识:

    • 由于系统上的所有活动最终都由一个或多个底层系统资源提供服务,因此通过查看资源的使用情况来分析系统瓶颈是非常有效的
    • 查看一段时间内的资源利用情况,我们通常可以使用模式匹配来确定感兴趣的重要区域
    • 资源争用对多个并发用户共享有限资源的影响特别大。 随着试图使用同一资源的消费者数量的增加,他们之间争用的可能性也会增加

    典型的(客户端配置)存储设备不需要很多这样的消费者,特别是旋转驱动器。 即使只有两个并发用户,也常常会导致磁盘争用,从而降低了相应场景的速度。

    对于并发CPU活动,情况要好一些,只要有足够的处理器(或内核)可用,通常可以忽略不计并发的影响

    • 资源利用率不足的症状可能是由过度串行化引起的,过度串行化是指将本来可以并行化执行的任务按顺序串行化执行

    • 在非内存密集型场景中,CPU和/或磁盘的并发使用通常远远超过并发内存使用对性能的影响。 在那些性能更依赖于在快速CPU缓存(例如L2, L3)中缓存内存内容的场景中,内存的并发使用也会导致争用,其中每个内存用户都在争夺缓存空间, 将其他进程的缓存内容从快速缓存中推出,并需要重复访问较慢的RAM进行数据访问

    • 在查看任何给定场景的性能时,必须考虑到系统本身的影响(例如,系统活动,如索引、分页等)

    • WPA提供了CPU、磁盘和内存图,以帮助分析这些共享的关键系统资源的使用情况

    • WPA可以让你查看资源使用的整体以及过滤到相关数据的特定子集(例如,给定进程的磁盘使用情况,如notepad.exe)

    • 堆叠线形图可以是可视化添加性质的伟大工具 ,特别是,在使用WPA的资源使用视图时,堆叠线形图是识别资源使用症状(例如饱和、争用、未充分利用等)的好工具

    • 为了进行更深入的研究,WPA允许您在当前分析视图或新创建的视图中放大特定的时间选择

    • 详细图表中的表格可以让你查看图表中对应的单个数据点(例如,我们在磁盘使用图中看到的单个磁盘I/ o)

    • 对比,并排,两个不同资源的使用图表,过滤到同一个进程,使我们能够直观地识别该进程的跨资源使用模式(例如,由于磁盘和CPU使用交错导致的过度序列化)

    在本章中,我们从资源使用的角度研究了系统和应用程序活动。 虽然这使我们能够找到用户场景的瓶颈在哪里,但我们还不知道如何找出原因。 查看在感兴趣的区域期间在CPU上执行的代码通常可以帮助我们更好地了解导致当前问题的特定活动。

    在下一章中,我们将通过关注在本章中确定的感兴趣区域中发生的逻辑活动,继续在分析记事本无反应的根本原因方面取得进展

  • 相关阅读:
    BZOJ3512 DZY Loves Math IV
    HDU5608 function
    数论
    动态点分治
    tarjan
    插头DP
    斯坦纳树
    css中的display(显示)和visibility(可见性)
    简单的数据整理, 递归算法
    java链接数据库构建sql语句的时候容易记混的地方
  • 原文地址:https://www.cnblogs.com/mooooonlight/p/15484023.html
Copyright © 2011-2022 走看看