斯坦福-CS143-编译原理中文笔记-四-

74 阅读1小时+

斯坦福 CS143 编译原理中文笔记(四)

P78:p78 15-03-_Analysis_of_Loop - 加加zero - BV1Mb42177J7

本视频中,我们将继续讨论控制流图分析,重点关注无疑是最有趣的部分。

循环分析。

这是一个带循环的控制流图示例,分析中需要特殊元素bottom的需求,与循环分析紧密相关,让我们思考如何用此特定控制流图,进行常量传播示例分析,关于x我们知道什么,好的,最初我们一无所知。

在进入控制流图之前,其值为top,在赋值为3后,我们将知道x的值为3,这里的条件分支,谓词不会影响x的值,因此两条分支上都是3,对y的赋值不会影响它,因此这里也是3,现在我们来这里,好的。

让我们关注这个语句,分析x在y等于0时的规则是,好的,所以这里的x值,在赋值给y之前是一个函数,所有前驱节点的值,好的,我们还没有这里的值,所以问题是,你知道,这条边上x的值是什么,为了弄清楚这一点。

我们需要看它的前驱节点,好的,那么它的前驱节点是什么,这里有一个点在谓词之后,这里有一个点在两个语句之间,这里有一个点在执行y之后,我们正在沿着边向后走,嗯,看着,你知道,我们需要为x知道的信息。

我们需要在这里知道它,我们需要在这里知道它,我们需要在这里知道它,然后因为这个边意味着,我们再次需要在y等于0的两个前驱节点上知道它,现在我们处于循环中,这并不太令人惊讶,我的意思是,如果你有。

如果关于x的信息依赖于语句的前驱,并且你确实遵循递归,那么最终你会陷入这样的循环,并且没有好办法,至少不是立即明显的方法来解决这个问题,我们如何获取关于前驱的信息,当y的前驱依赖于自身时。

y的前驱等于零。

所以更精确地说,嗯,再次查看那个特定语句,为了计算x在点,就在语句y等于零之前是否为常数,我们需要知道x在两个前驱处是否为常数,该信息依赖于其前驱,其中包括y等于0,好的,这就是难题。

我们如何解决这个递归问题。

有一个标准解决方案,实际上在许多数学领域都使用,不仅仅是循环分析,当你有这些类型的递归关系或递归方程时,标准解决方案是打破循环,从一些初始猜测开始,所以你有一些初始近似值,可能甚至不期望是最终结果。

但允许你开始,因此,由于循环,所有点,所有程序点在所有时间都必须有值,因此我们将分配一个初始值,这就是bottom的作用,bottom意味着到目前为止我们知道控制从未到达这一点,记住这一点。

我们说过这个,几段视频之前,这将使我们能够取得进展并看到。

让我们继续分析这个,嗯,控制流图现在,我们假设在所有点,最初x有一个底部值,除了入口点,所以入口点是特殊的,我们假设我们不知道关于x的任何信息,因为我们知道控制达到了初始点,但最初我们将只是说好吧。

其他地方x都是底部,所以底部那里,底部那里,好的,我将填写所有值,我在这里到处写,在合并这两个路径后确实还有一个,所以表示一下好吧,所以现在我们有初始设置,现在记住程序是什么,我们去看信息不一致的地方。

然后更新它,信息不一致的地方在哪里,显然这里不正确,因为知道如果if控制到达x等于三之前,那么赋值后x将等于三,嗯再次,谓词不会改变x的值,所以我们必须更新两个分支的结果,在谓词之后。

以及在这个不影响x的赋值之后,使信息一致,我们现在回到有趣的情况,我们知道x等于三,进入y等于零的这条分支,就我们所知控制从未到达另一个前驱,所以我们将开始假设这部分,那条路径从未被采取。

如果那条路径从未被采取,那么它不会贡献任何东西,所以在程序的这个点,我们将知道x等于三,假设所有这些信息都是正确的,我们将能够得出结论x等于三,在这个点并注意我们如何打破循环并开始。

所以我们就假设你知道,这个循环中的最后一条边从不执行,如果不是这样,我们稍后会发现的,这个下面的值将不再是底部,然后我们会再次更新赋值,好的,所以让我们继续,所以我们有,呃,在y被赋值为零之前x等于三。

对y的赋值不会影响x的值,所以使之后的信息一致,我们不得不使x等于三,现在有两个路径的合并,好的,所以在执行这个赋值之前,我们也知道x等于3,赋值a不会影响x,我们会更新那一点,谓词不会影响x的值。

所以我们会知道x在这条回边上等于3,现在信息已经改变,现在我们知道控制可以到达这条边,因为我们遵循了一条控制路径,一路到这里我们有了关于x的新信息,所以现在我们必须再次检查一切是否仍然正常。

所以这里我们有x等于3在这条边上,x等于3在这条边上,我们之前的结论是x在进入时等于3,到语句,Y等于零,好吧,那仍然是一致的,控制流图中没有不一致的地方,所以所有信息都与所有规则一致,所以我们完成了。

这是最终分析,我们能够得出结论,所有这些点这里,我说每个点,除了入口点,x是,实际上是常数。

三。

P79:p79 15-04-_Orderings - 加加zero - BV1Mb42177J7

在最近几段视频中,我们一直在讨论一种抽象计算,使用像底部这样的元素,常数,以及顶部,在这段视频中我们将开始稍微概括这些想法。

我们首先要谈论的是。

朝着概括迈出的第一步是讨论这些值的顺序,首先我想介绍一个技术术语,我们在程序分析中计算的值,常量和顶部,这些称为抽象值,以区分具体值,因此,具体值是程序实际计算的运行时值,如实际对象和数字,等。

程序分析使用的抽象值通常,更抽象,特定抽象值可代表一组可能的具体值,在用于常量传播的特定抽象值集中,实际上只有一个,非常抽象的值,那是顶级,代表任何可能运行时值,代表所有运行时值,无论如何。

结果有一种方法可以简化,我们一直在讨论的分析展示,我们将说底部小于所有常数,所有常数小于顶部,所以如果我们画一张图,较低的值画在底部,顶部绘制高值,值间关系,得到此图,底部在下,低于所有其他值。

底部小于所有常数,好的,知道所有常数在中层,好的,也知道常数间不可比,好的,零不比一大,例如,零和一不可比,其他常量对亦如此,因此你有,底部在底部,所有常数在中间,它们不可比,然后大于其他一切的是顶部。

现在定义了顺序,我们可以定义集合元素的操作,即最小上界或最大下界,这意味着取最小的元素,大于集合中所有元素的最小上界,例如,如果最小上界为底部和1,则等于1,好的,如果最小上界为顶部和底部,则等于顶部。

也许更有趣的是,1和2的最小上界,这里有两个不可比较的常数,记住最小上界的含义,它是排序中大于所有元素的元素,大于我们取最小上界的所有元素,所以我们的最小上界只有两个元素,但1和2的最小上界。

大于它们两者中最小的元素是顶部,应该说大于它们两者的是顶部,好的,所以最小上界,如果你考虑一下,如果我们再次画出我们的图片,我们有底部和顶部,如果你在这里挑选一些点,比如我们想取底部和2的最小上界。

你只是在挑选大于两者的最小元素,那将是顶部本身,类似地,对于顶部和底部,你将得到顶部,如果你有任何不可比较的元素,那么你必须挑选一个大于两者的元素,在这种情况下,那总是最终成为顶部。

有了最小上界这个概念,结果规则1到4,它们只是在计算最小上界,所以语句的in就等于,所有前驱的out的最小上界,这就是规则1到4所说的,如果你记得我们那里有什么,我们有和,我们有一堆前驱。

然后有一些类型的语句,S,我们只是在做所有前驱的信息,我们只是在取这些前驱的最小上界,好的,这就是进入S的信息。

抽象值的排序也有助于澄清我们分析算法的一个重要方面,即为什么它会终止,所以记住算法的终止条件是重复,重复应用规则,直到什么都不改变,直到控制流图中没有更多的不一致性,没有更多的信息需要更新。

仅仅因为我们说我们将重复直到什么都不改变,这不保证最终一切不变,那可能永远持续下去,每次更新都会引入新不一致,我们从未真正达到所有信息一致的点,所以排序实际解释了为何不能发生,算法保证终止,所以记住。

除了入口点外,值开始时为底部,所以它们从排序的最低位置开始,然后仔细看规则,规则只能使值在程序点增加,底部可提升。

在给定程序点上可改变至某个常数,然后另一更新可提升该常数至顶部,但当然一旦我们达到顶部,没有更大的元素,若规则只能使元素增加,最终我们必将耗尽可增加的元素,好的,这意味着我们为每条语句计算的信息块。

对于每个变量,对于输入或输出,最多改变两次,好的,所以可以从底部到常数,从常数到顶部,但之后将不再更新,并且,这意味着我们描述的常量传播算法,实际上是程序大小的线性,所以,步骤数将被常数值的数量限制。

我们尝试计算乘2,因为每个可变2次,由于入口和出口值各1,算法可能执行的总步数是程序语句数乘4。

P8:p08 03-02-Lexical_Analysis - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将继续词汇分析的讲座,结合过去编程语言中出现的一些有趣的词法问题。

我们已经稍微谈过一些关于Fortran的内容,Fortran中一个有趣的词法规则是,空格不重要,所以空格无关紧要,类似uv a r one这样的,呃,变量名bar one完全等同于v a空格r one。

因此,这两个程序片段必须具有完全相同的意思,Fortran的想法是,你可以拿你的程序,并删除所有空格,这应该不会改变程序的含义,让我们看看Fortran的空格规则如何影响词法分析。

这里有几个Fortran代码片段,我应该说,这个例子来自《龙书》,实际上,后来的几个例子也来自《龙书》的旧版,呃,不过,我们有什么,这实际上是,呃,Fortran循环的头部,你知道它是一个循环。

因为它有关键字,Do,类似于现代C或C++中的for,所以这是一个,这是一个循环关键字,然后我们有迭代变量i和i将变化的范围,所以在这种情况下,i将从1变化到25,然后这里的数字5,这有点奇怪。

在现代语言中看不到的东西,在旧版的Fortran中,你的do语句在循环的顶部,然后循环的大小或包含在循环中的所有语句都由一个标签命名,它们紧跟在do语句之后,所以循环将从头部,do语句一直到标签5。

所以任何被标记为5的语句,所有在中间的这些语句都是循环的一部分,所以循环会执行这些语句,然后我们回到头部,它会继续执行那些,直到,对于迭代变量i的所有值都执行了,在这种情况下,1到25。

现在这里是另一个代码片段,正如你所见,这个几乎和上面的完全一样,唯一的区别是让我换颜色,此处特定片段在此位置有逗号,此片段有一个句号,结果是这个差异至关重要,这两段代码含义完全不同,所以此片段。

第一个实际上是一个do循环,如我之前所说,所以它具有,你知道,关键字,执行标签五,变量i和范围1到25,现在此片段下方,这实际上是一个变量名,do五i所以如果我写时不加空格,记住空格不重要。

这将是一个do五,i然后这是一个赋值,等于1。25,好的,所以你可以看到,呃,这些符号,这个序列,第一个符号序列被完全以不同方式解释,取决于是否有句号或逗号在后面,所以让我们更精确一点。

我们如何知道do是什么,所以让我们只关注这里的关键字do,当我们处于这一点时,当我们关注在这里,在o的后面,并记住i,这个实现方式,是通过从左到右扫描,所以我们将沿着这个方向走过,输入。

成功查看每个字符,当我们的焦点到达这一点时,我们可以做出决定,这是一个这是一个关键字,因为我们已经看到了整个关键字do,问题是我们的信息不够做出这个决定,我们不知道这是do,还是最终将成为。

变量名的一部分,如do五i,唯一知道的方法是向前看输入,到这个位置,看看是否有逗号或句号,这是一个需要向前看的词法分析示例,以理解do在从左到右行进中的作用,预览输入以查看后续符号。

在那时无法区分do的作用,因为到此为止符号序列完全相同,唯一区分它们的是更远的东西,可以想象,大量预览使词法分析实现复杂,因此,词法系统设计的一个目标是,最小化预览量或限制所需预览量。

你可能想知道为什么Fortran有这个关于空格的奇怪规则,结果是在穿孔卡机上很容易意外添加空格,因此,他们向语言中添加了这条规则,这样穿孔卡操作员就不必总是重做工作。

幸运的是今天我们不再用穿孔卡输入程序,而是,这个例子帮助我们更好地理解词法分析的目的。

如我所说,目标是分割字符串,我们试图将字符串分成语言逻辑单元,这是通过从左到右读取实现的,我们正在从左到右扫描输入,一次识别一个标记。

由于这一点,可能需要预览来决定一个标记的结束,以及下一个标记的开始,再次,我想强调预览总是需要的,但我们希望最小化预览量,事实上我们希望将其限制为某个常数。

因为这将大大简化词法分析器的实现,现在只是为了说明预览是我们始终需要担心的问题,让我们考虑之前看过的这个例子,只是注意当我们从左到右阅读时,让我们看看这个关键字,这里的else,当我们读到e时必须决定。

那是一个变量名还是单独的符号,或者我们想将其与后续符号一起考虑,因此,在扫描e后有一个预览问题,我们必须决定,它是单独存在,还是属于更大的词法单元,你知道在这个例子中有单个字符的变量名,如i,J,和z。

因此e也可能是其中之一,另一个例子是双等号,当我们读到一个单等号时,我们如何决定它是单个等号,像这些其他赋值一样,还是它实际上是一个双等号?为了做到这一点,需要预览,若焦点在此,需前瞻,见另等号。

我们知晓,或将知晓,嗯,将两合为一符,而非单看等号,另例,古老语言,阿佩尔一语言有趣,由IBM设计,IBM设计,代表编程语言,一个没问题,旨在成为编程语言,至少在IBM内供所有人使用。

并应包含任何程序员可能需要的所有功能,因此它应该非常,非常通用,限制很少,PL/1的一个特点是关键字未保留,因此P和P1,你可以使用关键字,既作关键字也作变量,所以可以使用关键字和其他非关键字角色。

这意味着你可以编写,有趣的句子或程序,让我大声读出来,因为听起来很有趣,如果否则然后等于否则,否则否则等于然后,这里的正确组织,当然是这个是一个关键字,这是一个关键词,这也是一个关键词,其他东西会变色。

这里是所有变量,这些都是变量名,你可以想象,这使词法分析有点难,因为当我们只是从左到右扫描,就像我们在这里通过,当我们说我们到了这一点,你知道,如何判断这些是变量名或关键字,不考虑表达式其他部分。

因此词法分析和P1相当具有挑战性。

这是P1的另一个例子,我们有一个程序片段,我们有声明一词,然后一个开和一个闭,包含一堆参数,我指出这里的平衡括号,结果取决于整个表达式的上下文,这可能是关键字,也可能是数组引用,我指的是,当我说。

声明这里可能是关键字,也可能是数组的名称,这些可能是数组的索引,碰巧的是,仅凭这些无法决定此片段是否有效,是一个有效的声明,也是一个有效的数组引用,这取决于接下来发生了什么,可能取决于,例如。

是否有等号在这里,在这种情况下,这将解释为赋值,并且declare将是数组的名称,这个例子的有趣之处在于,由于这里的参数数量不限,可能有n个参数,对于任何n,这需要无限向前看,好的,因此。

要正确实现这一点,嗯,当您从左到右扫描以决定declare再次,是关键字还是数组引用时,我们需要扫描超过整个参数列表以查看接下来发生了什么。

Fortran和peel one分别于1950年代和1960年代设计,这些经验教会了我们很多关于编程语言词法设计不应该做的事情,所以今天的情况好多了,但问题并没有完全消失。

我将使用C++的例子来说明这一点,这是一个您可能熟悉的C++模板语法示例,或者您可能已经在Java中看到了类似的语法,C++还有一个称为流输入的操作符,因此。

此操作符从这里读取输入流并将结果存储在变量中,问题是这里,存在嵌套模板的冲突,例如,如果我有一个模板,操作看起来像这样,并且,好的,注意这里发生了什么,我的意图是有一个模板的嵌套应用。

但我最终得到了两个大于号在一起,这看起来就像流操作符,问题是,词法分析器应该做什么,应该将其解释为模板的两个闭括号,还是应该将其解释为两个大于号粘在一起作为流操作符,结果很长一段时间以来。

C++编译器都对此感到困惑,多数C++编译器已修复,此情况C++编译器视为流操作符,将报语法错误,你认为解决方案是什么,实际上唯一修复方法是使这,词法分析,正确方法是插入空格,因此必须这样写。

必须记住插入空格,这样两个大于号就不会连在一起,现在必须插入空格修复有点丑,呃,程序的电气分析。

总结,词法分析的目标是将输入流分割为词素,好的,我们将划下分割线在字符串中,决定词素边界,并识别每个词素的标记,正因为从左到右扫描,有时需要前瞻,有时需要预览输入流以确定当前字符串。

我们正在查看的当前子串,在语言中扮演什么角色。

P80:p80 15-05-_Liveness_Analysi - 加加zero - BV1Mb42177J7

本视频中,将探讨另一种全局分析:活跃度分析。

过去几视频中,已探讨了一种在控制流图中全局传播常量的过程,这是,我们一直在看的其中一个控制流图,回顾一下我们讨论过的算法,足以证明,可替换此处对x的使用为常数3,一旦完成,x可能不再有用。

可能不会被任何地方使用,因此可能从程序中删除此语句,这是一个真正的优化,重要的优化,但只有当x在程序其他地方不被使用时。

让我们更小心地定义,说x未被使用的意思,这里是x的使用,语句中对x的引用,显然这个对x的特定引用,获取由这个右x定义的值,我们说右边的x是活跃的,这个活跃,好的,这意味着值可能在未来被使用。

活跃=可能被使用,在未来,好的,在此行代码中写入x的值可能被后续指令使用,这里不仅仅是可能被使用,实际上是保证会被使用,因为只有一条路径,那条路径在另一个x赋值之前有一个对x的引用。

因此这个特定x的值在此处写入是保证会被使用的,但通常我们不需要这样,我们只是意味着必须有它将被使用的可能性,相反,让我们看看这个其他语句,在这个例子中,将x赋值为3,但这个赋值x,这个x的值从未被使用。

这个已死,因为3的值被4覆盖,在变量x有任何使用之前,好的,这个特定的对x的写入将永远不会看到光明,永远不会被程序的任何部分使用。

我们说它是死的,总结来说,变量x在语句s处活跃,如果存在使用x的语句,好的,而不是其他语句,S'使用x,且从s到s'有路径,路径上无x的赋值,x可以,因此需要对x进行赋值,作为某个语句。

S存在通过程序到达x读取的语句,S',路径上无对x的读取,好的,如果出现这种情况,那么第一个语句中写的值。

S现在活着,如果值不是活的,那么它是死的,对x的赋值语句将是死的,如果x在赋值后死亡,代码,所以如果我们知道在赋值会议后,立即对这个x的赋值后,未来没有可能使用x的值,那么,这个赋值是无用的。

整个语句可以删除,好吧,所以语句可以从程序中删除,但请注意,为了做到这一点,我们需要有活跃信息,我们需要知道在这个点x是否死亡。

所以再一次,我们想要做的是关于控制流图的全局信息,在这种情况下,属性是x将来是否会使用,我们希望将该信息本地化到程序的特定点,以便我们可以做出局部优化决策,好吧,就像对于常量传播一样。

我们将定义并执行活跃性分析的算法,它将遵循相同的框架,我们将用相邻语句之间传递的信息来表达活跃性,就像我们为复制或常量传播所做的那样,结果将是,活跃性实际上比常量传播简单得多或稍微简单。

因为它只是一个布尔属性,你知道它是真还是假,好吧。

所以让我们看看一些规则,嗯,对于活跃性,所以这里我们定义了x在此处活跃的含义,所以p之后x是活跃的,它将保持活跃,记得直觉是什么,直觉是变量x在p之后是活跃的,若x值用于某路径,从p开始的某路径上。

为知是否为活跃,将取输入点的活跃信息,即这里这里,这里和这里,p后的每个后续语句,将问x是否在这些点活跃,即x的活跃性是所有后续点的大或,p的输出处的x的活跃性,接下来,考虑单个语句对x活跃性的影响。

第一条规则是若语句读取x值,这里有一个赋值语句,右侧引用x,读取x,则x在该语句前活跃,显然,x即将在该语句末使用,因此x在该点活跃,若语句读取x值,则该语句末x的活跃性为真,第二种情况是语句写入x值。

这里有一个赋值x,右侧不引用x,不读取x值,e中没有x,因此x在该语句前不活跃,或说x在该语句前已死,因为我们覆盖了x的值,因此x在此语句前的值将不会被读取,因为e中,赋值右侧不引用x。

语句前的当前x值,将永远不会在未来被使用,因此x在该点已死,最后一种情况是若语句不引用x,既不读取也不写入x,即x在该语句前不活跃,因为e中不引用x,因此x在该点已死,最后一种情况是若语句不引用x。

既不读取也不写入x,即x在该语句前不活跃,因为e中不引用x,因此x在该点已死,最后一种情况是若语句不引用x,既不读取也不写入x,即x在该语句前不活跃,因为e中不引用x,那么x在语句后的活跃度。

与语句前相同,所以如果x在这里活跃,那么x将在这里活跃,同样地,如果x在语句后死亡,那么x在语句前必须死亡,这是因为x,如果x在语句s后的未来不被使用,那么在语句s前的未来也不会被使用。

陈述既不读也不写x。

所以只有这4条规则,现在我们可以给出算法,所以最初我们让x的活跃信息在所有程序点都为假,然后重复以下步骤,直到所有语句满足1至4条规则,这和我们用于常量传播的算法相同,我们选择一些信息不一致语句。

然后用适当的角色更新该语句的信息。

让我们做一个简单例子,嗯,带有循环的东西,那么让我们开始说吧,将x初始化为零,然后循环体应该做什么呢,我们可以检查x是否等于十,如果是,我们将退出循环,假设x在退出时为死,因此x在循环外不被引用。

否则如果x不是十,然后我们将增加x,然后回到循环顶部,这是一个非常愚蠢的小程序,它只计数到十并退出,但让我们做活跃性分析以查看x何时活跃,好的,因此,由于x在退出时已死,显然。

它在该分支上的条件出口也将是死的,好的,因此,我应该说x不活跃,我们正在使用布尔值,所以x的活跃性为假,并假设x也不活跃,其他地方最初,好的,所以那里有一个程序点,也是x的活跃性为假的地方,好的。

所以现在,嗯,让我们传播信息,所以这里有一个对x的读取,让我换个颜色,这里有一个x的读操作,实际上,这里的信息不一致,因为在声明之前,因为我们有一个x的读操作,X必须存活,实际上,x在此处存活。

现在注意,这个声明既读又写x,好的,但说x在读之前,当我们做读操作时优先,因为读操作发生在写之前,所以我们将读取x的旧值,在我们写入x的新值之前,好的,所以x的旧值确实被使用,这就是为什么x在此处存活。

并且立即之前,所以这里是另一个,嗯,x的读操作,好的,所以在,所以在这个点之前,我漏掉了一个程序点,嗯,x也是存活,好的,然后沿着后向边,嗯,这意味着x将在循环的背边上存活,它也将存活,进入初始化块。

我们回到这里,我们看到我们完成了,因为x已经在循环体内已知存活,现在x也在这里存活,然后问题是,你知道,关于这个控制流图入口点的这一点,嗯,有一个x的写操作,但没有右边的读操作,实际上。

x在进入这个控制流图时不存活,实际上,x在此处死亡,无论x有什么值,当我们进入控制流图时,它永远不会在未来被使用,所以这是每个程序点的正确活跃信息,在这个例子中。

现在你可以从我们的小例子中看到,值从false变为true,但不是相反的方式,所以每个值从false开始,并且最多只能改变一次,以说明该值实际上存活,该属性变为真,然后它永远不会变回假,回到排序。

我们在这个分析中只有两个值,假和真,顺序是假小于真,好的,我们知道,因此一切从排序中最低可能元素开始,它们只能向上移动,因此它们可以被提升为真,但反之则不行,因此由于每个值只能改变一次,终止是保证的。

嗯,最终我们保证控制流图中有一致的信息,分析将终止。

总结我们关于控制流图全局分析的讨论,在过去的几个视频中,我们讨论了两种类型的分析,常量传播被称为四字分析,因为信息从输入推送到输出,所以如果你考虑一个控制流图。

控制流分析中发生的事情是信息朝这个方向流动,它沿着计算的方向流动,如果我有一个常数在上面,X赋值为常数在这里,并且x稍后使用,那么该常数将向前流到使用的地方,好的,因此信息沿着计算的方向流动,活跃度。

另一方面是一个向后分析,信息从输出推回到输入,所以在这个例子中,让我改变颜色这里,我们看到x在这条语句之前是活跃的,这种活跃度以相反的方向传播,它逆着控制,逆着执行流程向程序的开始传播。

文献中有许多其他类型的全局流分析,常量传播分析和活跃度分析是最重要的两种,还有许多其他非常重要的,许多人,许多人已经调查过,几乎所有这些分析都可以归类为向前或向后。

有一些分析和一些重要的分析既不是向前也不是向后,那么信息基本上在两个方向上都被推动,另一件事是,文献中几乎所有进行全局流分析的,也遵循这种方法论,即局部规则关联相邻程序点之间的信息,因此。

真正重要的是局部规则部分,那么,我们将分析整个控制流图的复杂问题分解为一系列规则,它们仅传播非常。

P81:p81 16-01-_Register_Allocat - 加加zero - BV1Mb42177J7

本视频中,将讨论寄存器分配,这是编译器最复杂的事之一,以优化性能,并涉及全局流分析讨论的概念。

回忆中间代码可使用无限临时变量,这简化了许多事,特别是优化,因为,无需担心,保持代码中寄存器的数量,但它会使最终汇编代码复杂化,因为我们可能使用了太多临时变量,这在实践中确实是个问题。

所以中间代码使用比目标机器更多的临时变量并不罕见,问题在于。

重写中间代码,以使用不超过机器寄存器的临时变量,我们打算这样做的方法是,使用算法优化,将为每个寄存器分配多个临时变量,因此将有一对多的映射,从临时变量到寄存器的一对多映射,所以好吧,显然这里有点问题。

如果我们确实使用了很多临时变量,将无法将它们全部放入一个寄存器中,因此需要某种技巧,我们将在几分钟内说明这种技巧,实际上会有这种情况失败,我们需要某种备用方案,但默认计划是尽可能多地临时放入同一台机器。

注册并完成所有操作而不改变程序的行为。

我们如何做到这个神奇的事情,我们实际上如何,嗯,制作一个单寄存器,保持多个值,好吧,寄存器有多个值是可以的,只要它一次只含一个值,让我们考虑这个程序,我将在这里切换颜色,好的,这是一个简单的三语句程序。

注意a在头两句中用,写在第一句,读在第二句,E写在第二句,读在第三句,F仅在第三句写,实际上这3个值,A e 和 f,它们从不同时存在,当我们读完一个,我们实际上已经完成,我们已用尽a的所有用途。

我们将在这个小代码片段中,假设a和f在其他地方未使用,结果a e和f实际上可以,都存在于同一个寄存器中,那看起来会怎样呢,把它们都分配到特定寄存器,寄存器R1,把cd和b分配到各自独立寄存器。

那么代码看起来像这样,寄存器R1是R2加R3,然后寄存器R1是R1加R4,然后寄存器R1是R1减1,好的,现在注意,这仅仅是代码的逐字翻译到寄存器,但存在多对一映射,嗯,左边的名字到右边的寄存器名。

寄存器分配是个老问题,实际上,最早在1950年代的原始FORTRAN项目中识别,但最初的重新分配使用了相当粗糙的算法,并且很快或非常快地注意到,这实际上是一个代码生成质量的瓶颈。

实际上是寄存器分配能力限制了其做好工作的能力,对整体平等影响重大。

编译器可产生代码的整体质量,约30年后,1980年,突破出现,人们发现,IBM一组研究人员发现基于图着色的风险分配方案,该方案优点是简单,易于解释,全局性,利用整个控制流图的信息,同时。

实践中效果良好。

现代寄存器分配算法的基本原则,如果有两个临时变量,T1和T2,我想知道它们何时可共享寄存器,它们允许共享寄存器,它们允许在同一寄存器中,如果它们不在同一时间活跃,好的,因此,在程序的任何一点。

T1或T2中最多只有一个活跃,更简洁的说法,我已部分说过,即如果t两,T1和T2同时直播,好的,如果两个都在运行,那么他们就不能共用一个寄存器,这就是陈述的否定形式,它只是告诉你,如果需要同时两个值。

让我们看看控制流程图,现在我们知道,为了解决重定位问题,需要进行寄存器分配,至少在这方面,我们需要活跃信息,计算程序各点的活跃变量,这就是,我快速过一遍,假设循环退出时只有b是活跃的。

b是这段代码的输出,它在其他地方被使用,但其他变量都不活跃,那么如果我们倒推,记住,活跃度是向后分析,这里看到b已写,在此语句前非活跃,但f和c被读,因此c和f在此基本块前都活跃,好,类似地。

如果我们再上一层,这里看到e现在活跃,f已死,因为f在此处被写,e被读,且在此路径上还有另一个出口,b活跃,现在在此点,此基础块后,活跃变量集为b,C和f,因为b在一路径上活跃。

而c和f在另一路径上活跃,记住,某物为活跃,仅需在未来某些可能,执行演化上活跃,因此,从这个节点,若变量活跃,从这里节点退出时,它是实时的,从这里逆推,因为e是红的,所以b和f在这里是实时的。

在这个声明中,C和f没有被引用,因此它们只是向上传播,B从实时集中删除,因为它被写入,但d在这个集合中被添加,对于此图中的其他边缘也类似,如果你去检查所有其他边缘,你会看到现场是正确的。

这仅遵循上一视频中给出的简单规则。

现在如何使用活跃信息进行寄存器分配,嗯,我们将构建一个无向图,在此图中,将为每个临时变量创建一个节点,因此,每个变量将在图中有一个节点,如果两个临时变量在某些程序点同时活跃,将在它们之间有一条边。

好的,回顾这个小例子,我们可以看到,例如,程序此时,C和e都存活,都在活跃集中,此基本块执行后,因此c和e不能在同一寄存器。

好的,继续,这称为数据结构,此图称为寄存器干扰图或简写为RIG,再说一次,基本思想是,两个临时变量可分配在同一寄存器,如果寄存器干扰图中它们之间没有边连接。

所以这是寄存器干扰图,针对我们的例子,这是从之前几页给出的代码和,对齐分析构建的图,很容易从图中读出约束条件,因此,例如,B和C不能在同一寄存器,因为B和C由边连接,好的,说它们在某部分同时存在。

在程序的某个点,因此它们必须在不同的寄存器,另一方面,注意B和D之间没有边,好的,所以这条边缺失,因此b和d可能分配在同一寄存器,它们的生命周期或存活时间不重叠。

寄存器干扰图的好处是,它提取了描述合法寄存器分配所需的确切信息,因此它给我们所有可能合法寄存器分配的表示,我还没有说我们实际上如何从寄存器干扰图中得到一个寄存器分配,但第一步是以某种精确的方式描述问题。

不能在同一寄存器中存在的图约束,为我们做到了这一点,它的另一个优点是它提供了寄存器需求的全球视图,意味着遍历整个控制流图,考虑控制流图各部分信息,帮助我们做关于寄存器重要性的全局决策,最后注意重建后。

寄存器分配算法与架构无关,未展示算法,暂时相信,最终会发现不依赖机器属性,除了寄存器数量,仅需知道机器这一属性,为使用该算法进行寄存器分配所需,仅需知道机器的寄存器数量,使用该算法进行寄存器分配。

P82:p82 16-02-_Graph_Coloring - 加加zero - BV1Mb42177J7

本视频中,我们将继续讨论寄存器干扰图,并讨论如何使用图来为过程分配寄存器,我们将研究一种流行的技术,称为图着色。

首先,几个定义,图着色是对节点的颜色分配,使得由边连接的节点具有不同的颜色,所以,如果我有一个图,假设有3个节点,并且完全连接,每个节点与其他节点相连,然后图的着色就是颜色分配。

使得每对相连节点颜色不同,例如,我可以将这个节点涂成蓝色,我可以将这个节点涂成绿色,我可以将这个节点涂成黑色,好的,然后这就是图的有效着色,因为每对邻居颜色不同,然后图是k可着色的,若用k色或更少染色。

在我们的问题中,颜色对应寄存器,我们想要做的是为图节点分配颜色或寄存器,我们让k为,允许使用的最大颜色数,为机器寄存器的数量,因此,实际上的寄存器数量是我们在为生成代码的架构,然后如果,如果刚体。

若寄存器干扰图k可染色,则存在一种寄存器分配,使用不超过k个寄存器。

让我们看一个示例RIG,对于这个特定图,没有着色,结果他使用了少于4种颜色,但至少有一种对这个图的着色,这就是,我用彩色标签,但也用寄存器名称,这样你可以看到我们可以将哪些寄存器分配给每个节点,注意。

尽管有很多,超过4个临时变量或节点,我们只用4种颜色着色,一些节点颜色相同,例如,D和b与e和a颜色相同。

提醒我们寄存器冲突图来源,这是原始控制流图,一旦我们有图的颜色,现在可做寄存器分配,可用寄存器名替换临时变量,然后得到这个控制流图,这里仅将程序中的每个变量,重命名为分配给它的寄存器,现在非常接近了。

如您所见,嗯,即将生成可在目标架构上执行代码,我们讨论了寄存器干扰图是什么,定义了图着色的概念,但尚未讨论如何计算图着色,这是下一个要解决的问题,不幸的是,这并不容易,图着色是一个极其困难的问题。

如果您上过计算机科学理论课,那么这会有所帮助,我说它是NP难问题,计算图着色,如果您之前没有听说过NP难,没关系,重要的是没有人知道该问题的有效算法,因此没有已知的快速程序,我们将讨论的解决方案。

每个编译器使用的技巧,基本上是近似技术,不能完全解决问题,但还有第二个问题,即给定数量的寄存器可能不存在着色,我们可能只有8个寄存器,图着色可能无法使用少于9或10种颜色,因此我们不得不处理这个问题。

稍后我们会讨论。

现在不会多说该问题的解决方案,现在我将介绍最流行的着色寄存器干扰图技巧,基本思想非常简单,我们将选择节点t,它在寄存器干扰图中,少于k个邻居,好的,这实际上是关键,只需在图中找到任何少于k个邻居的节点。

然后从寄存器干扰图中消除t及其边缘,只需删除该节点及其相邻的所有边缘,如果结果子图是可k着色的,那么原始图也是可k着色的,这里的想法是做一种分而治之的方法,我们选择一个节点,从图中删除它,着色剩余的图。

好的,这是一个节点少一个的较小问题,当我们完成那个之后,我声称我们可以为原始图找到着色。

那为何如此呢,这里画个图,假设有个节点少于k个邻居,姑且说它有2个邻居,这是其余的图,这个大圆是其余的图,这是要删除的节点t,假设它只有2个邻居,好的,现在我们要做的是,概念上删除t,然后给子图着色。

假设成功着色子图,现在这个大球用红色着色,现在要为这个大球,加上节点t着色,因为说少于k色可着色子图,2当然小于k,t必定有剩余颜色,看t相邻节点的颜色,少于k个,不能用尽k色,为t选剩余颜色。

实践中很有效的方法,先选少于k邻居的节点t,将t入栈并从冲突图删除,递归,重复直到图空,不断选少于k邻居的节点,入栈并从图中删除,直到图完全清空,这是第一阶段,这是第一部分,然后第二部分着色。

为栈上的节点构建着色,按逆序处理节点,它们被添加,所以最后加入栈的节点先处理,每一步我们做的是,选不同于已着色邻居的颜色,想法是取栈顶节点,弹出栈,现在加回图中,连同它在原图中的边,然后着色,然后看。

看它的邻居,删除时少于k邻居,加回时也少于k,会有可用颜色,我们着色它,然后从栈中选另一个节点重复,直到处理完所有图节点。

让我们举个例子,这是我们的寄存器干扰图,我们将用k等于四处理这个图,所以最初我们有整个寄存器干扰图,栈是空的,好的,第一步是选一个邻居少于四个的节点,让我们选a,因为它只有两个邻居,我们做什么。

我们从图中删除a并把它推入栈中,所以那一步后我们的图,这是移除a后的图,这是我们的栈,现在我们要选另一个邻居少于k的节点,抱歉,少于四个邻居,如果我们看这张图,我们可以看到我们有选择几个不同节点的选项。

我们可以选择d或b,实际上只有两个不同的节点我们可以选择,因为c,E和f都有四个邻居,所以让我们移除d,这里是任意选择,我们选择哪一个并不重要,现在栈将会有dna,我们的图将减少到这四个节点,现在。

这里有一个有趣的现象值得注意,此时所有节点邻居都少于四个,因此,由于图中每个节点的邻居数都少于我们允许使用的颜色数,在这一点上,图着色是保证成功的,因为每次我们移除一个节点。

我们只能减少图中其他每个节点的邻居数,同样有趣的是,即使在前一步中一些节点有四个邻居,因此可能不可着色,那时我们不能选择它们,因为可能邻居的着色会使用完所有颜色,注意现在它们都少于四个邻居,这是这个图。

着色启发式的一个有趣属性,即使一个节点有超过k的邻居,最终我们可能从图中移除足够的节点,使它的邻居数低于可用颜色数,然后我们将能够着色它,无论如何,现在选哪个节点不重要,因为我们可以按任何顺序处理它们。

现在从图中删除c,现在选择另一个节点,我们可以删除b,现在只剩下一个2节点图,让我们选择e并删除它,现在只剩下一个1节点图,我们从图中移除f,现在图是空的,我们有了栈,注意栈,实际上,在这个过程中。

我的意思是,这个程序第一阶段的目的是,给图中的节点排序,这是我们应该为图中的节点分配颜色的顺序,对,所以现在往回工作,嗯,之后,你知道,完成第一部分后,现在我们必须做第二部分,我们实际上要分配颜色。

我们将从栈的顶部开始,所以我们将节点f放回图中,我们将给它分配一个颜色,让我们假设我们将选择,未被其任何邻居使用的编号最低的寄存器,因为f在图中是单独的,我们将给它分配寄存器r1现在我们将e放回图中。

它必须与f有不同的寄存器,因为寄存器因为f正在使用寄存器r1,我们将给e分配寄存器r2,现在我们将b放回图中,它必须与f和d有不同的颜色或寄存器,所以我们将给它分配寄存器r3,我们将c放回图中。

现在注意c有所有的f,e和b作为邻居,正在使用前三个寄存器,所以c将被分配寄存器r4现在没有寄存器了,我的意思是这些四个节点正在使用所有的寄存器,但因为我们在第一阶段删除了正确的东西,我们知道。

那么当我们添加其他节点时,栈上的其余节点,它们不会拥有所有这些节点作为邻居,所以将有一些寄存器可用于分配,查看寄存器d,对不起,查看节点d,这里有了,它与其邻居共享,F e 和 c,因此。

唯一可分配的寄存器是我们的三个,与b相同的寄存器,这是唯一未被邻居使用的寄存器,好的,所以d被分配寄存器r三,然后分配a抱歉,我们在图中添加一个回退,我们查看其邻居,它们正在使用寄存器r一和r四,因此。

a可以被分配寄存器r二或r三,由于我们的规则只是使用,未被邻居使用的编号最低的寄存器,我们将分配它寄存器r二。

P83:p83 16-03-_Spilling - 加加zero - BV1Mb42177J7

本视频继续讨论寄存器分配,这次,将讨论图无法成功着色时的情况。

即需进行称为溢出的操作。

上期视频中讨论的图着色启发式,并不总能成功着色任意图,可能陷入困境,无法找到着色,因此在这种情况下,我们只能得出无法在寄存器中,我们有更多的临时值,超出了寄存器的容量,这些临时值得存哪呢。

它们将不得不存入内存,那是我们唯一的其他存储,我们将挑选一些值溢出到内存,脑海中的画面应是一个桶,可容纳固定量的物品,它们是寄存器,当它太满时,有些东西溢出,最终在其他地方。

何时图着色启发式会卡住,唯一无法进展的情况是,如果所有节点都有k或更多邻居。

让我们看看我们最喜欢的寄存器冲突图,我们在示例中一直使用的,现在假设我们想要使用的机器只有三个寄存器,因此我们不是寻找这个图的四色着色,我们需要找到三色着色。

所以让我们思考如何找到这个图的三色着色,若应用启发式,将a从图中移除,但我们会陷入困境,因一旦移除a及其边,图中剩余节点均有3个以上邻居,至少有3个邻居,因此图中无节点可删,以确保能为其找到着色。

按上视频讨论的启发式。

在此情况下,我们将要做的,将选节点作为溢出候选,我们或临时节点可能,或认为需分配内存而非寄存器,假设选f为例,稍后讨论如何选溢出节点,选择特定溢出节点有多种方法,为示例说明,如何选不重要。

只需从图中移除一个节点,好的,我们将移除它。

我们将溢出f,然后我们会这样做,像之前一样从图中删除它,然后继续简化,这现在将成功,因为一旦我们移除f,我们可以看到所有节点,实际上一些节点少于三个邻居,所以b c和d,抱歉,只有b和d有两个邻居。

一旦它们被删除,E和c将只有一个邻居,因此着色现在将成功,这是一个成功顺序的例子。

在我们决定溢出f并成功着色子图后,现在我们必须尝试为f分配颜色,它可能是,我们可能很幸运地发现,即使f有超过三个邻居或三个或更多邻居,当我们从图中移除它时,当我们尝试为子图构建着色时。

那些邻居实际上并没有使用所有的寄存器,最终可能是所有这些邻居,例如,被分配到相同的寄存器,因此有足够的寄存器留给f,这称为乐观着色,我们选择一个溢出的候选者,我们尝试着色子图,一旦我们有子图的着色。

然后我们看看我们是否只是幸运地能够为f分配一个寄存器,在这种情况下,我们可以继续着色其余的图,好像什么都没发生,所以在这种情况下让我们看看,会发生什么,我们将f加回到图中,和和看它的邻居。

我们看到有一个邻居在使用我们的1,有一个邻居在使用r二,有一个邻居在使用我们的三,在这种情况下乐观着色将不起作用,实际上,F有超过k的邻居,在我们着色子图后,结果那些邻居正在使用所有k。

在这种情况下三个,所有三个寄存器名称,因此f没有剩余的寄存器。

我们将实际溢出并存储在内存中,因此如果我要使着色失败,就像这个例子一样,然后我们溢出f,我们将分配一个内存位置给f,通常这意味着将在当前堆栈帧中分配一个位置,让我们称这个地址为a,是f的地址。

然后我们要修改控制流图,我们将更改正在编译的代码,因此在读取f的每个操作之前,我们将插入一个加载,从该地址加载,f的当前值到一个临时名称,好的,这很有意义,因为如果值在内存中。

那么如果我们有一个需要实际使用的操作,呃,这个值我们得先从内存中加载到寄存器中,类似地,在每次对f的写入之后,我们将插入一个存储,因此我们将f的当前值保存到其在内存中的位置。

这是我们从构建寄存器干扰图中得到的原始代码,注意这里有一些对f的引用,我们只是突出显示它们,对吧,所以我们有一些读取和一些写入。

那么现在我们要做什么,所以这里我们有一个这里,我们使用了f,这个语句中的f的读取,现在我们在其前面插入了一个加载,并且注意我在这里给了个新名字,我称之为f1,因为在控制流图中f的不同使用。

不必有相同的临时名称,实际上将它们是分开的好主意,因此f的每个不同使用将获得自己的名称,所以我们加载了f的值,然后它在该语句中被使用,我们对f有一个写入,因此我们存储了f的当前值。

注意这里我给了它不同的名称,f2,临时值在这里计算,它将被存储,它被称为f2,最后f的第三个使用,这里有一个对f的另一个加载,就在这里,然后它被用于这个计算b这里,好的。

这是一种系统化的修改代码以使用存储中f的方法,现在我们必须重新计算f的活跃度,那么会发生什么呢,好吧,这是计算寄存器干扰图的原实时信息,好的,现在注意f不见了,程序中不再使用f。

可以删除所有提到f存活的地方,现在我们有3个新名称,F1、F2和F3,需要添加它们的存活信息,在这里创建了一些新的程序点,插入了语句,当然,当我们加载当前f值时,该值在下一语句使用前是活的,这里我们有。

当前f值的右侧,在存储前是活的,然后这里是当前f值的另一个加载,直到存储都是活的,抱歉,直到下一语句使用,好的,现在注意这里f曾经在很多很多,很多地方在代码中存活。

现在不仅f或不同版本的f存活的地方更少,而且我们已区分它们,实际上我们已分离了f的不同使用,它们将有自己的节点和图中的干扰集,不会与其他f的使用共享,实际上这也将减少图中的边数。

总结一下上一页的例子,一旦我们决定实际上要溢出临时变量f,这意味着我们要改变程序,我们将会有加载和存储到程序中,现在我们将有一个不同的程序,这将改变我们的寄存器分配问题,因此我们不得不重新计算存活信息。

我们将不得不重建寄存器干扰图,然后我们将不得不再次尝试着色该图,事实证明,这个新的存活信息几乎与之前相同,除了f之外的所有临时名称几乎不受新添加的语句的影响,由新语句添加的只有几个新的程序点。

它们可能存活,但之前它们存活的所有地方仍然存活,而f本身已经发生了相当大的变化,它的存活信息发生了相当大的变化,当然,旧名称f不再使用,因此它的存活信息消失了,然后我们还将f分成三个,在这种情况下。

三个不同的临时变量,控制流图中每个不同f使用的各一个,现在注意每个这些新的f使用,或这些新的f版本只在非常,非常小的区域存活,因此,加载指令的加载是我们要加载的,临时变量,我们正在加载。

Fi只在加载和下一个使用它的指令之间存活,Fi只在加载和下一个使用它的指令之间存活,同样适用于商店,临时fi的商店仅在商店本身和前一条指令之间存活,创建fi的那个。

这样做的效果是大大减少了溢出变量的生命周期,因此,无论我们决定溢出哪个名称,通过在那些值被使用的位置附近添加加载和存储,我们极大地减少了生命周期,并且,此外,如我之前在幻灯片上提到的。

通过将名称f分成多个不同的名称,我们也,你知道,避免不同版本f的共享不同生命周期。

因此,f的生命周期通过溢出减少,在新程序中,它的干扰比旧程序少,特别是这意味着,在重建的寄存器干扰图中,F将有更少的邻居,它以前的一些邻居已经消失了,因为它只在更少的地方存活。

所以如果我们看新的寄存器干扰图,我们看到在所有版本中,记住f已拆分为3个临时变量,我们看到它们现在只与and c冲突,而之前f图中有其他邻居,现在,实际上,这个新图可3色化。

当然可能无法只拆一个名字,我们可能需拆多个临时变量,难点是决定拆哪个,这是注册分配中必须做出的艰难决定,任何选择都是正确的,只是性能问题,你知道一些溢出选择将产生比其它更好的代码。

但任何溢出选择都将导致正确的程序,人们使用一些启发式方法选择溢出的临时变量,这里是一些,或我认为最流行的三个,一个是溢出具有最多冲突的临时变量,原因是这是临时变量,你能存入记忆的一件事。

将最影响图中干扰数,所以想法是可能溢出这一个变量,我们将移除足够的边从图中,使其可用注册数染色,另一种可能是溢出定义和使用的临时变量,这里的意思是通过溢出这些,因为它们使用不多。

加载和存储的数量将相对较小,所以如果一个变量没有在太多地方使用,在额外指令执行的代价上相对较小,还有另一个,这实际上是编译器,我认为都会实现的,以避免溢出和内循环,所以如果你在溢出。

程序内最内层循环中使用的变量,和另一个在其他地方使用的变量之间有选择,你可能更倾向于切换到溢出,那个不在最内层循环中使用的,因为这样会导致更少的加载和存储,你真正想要避免的是向内循环添加额外的指令。

总结本视频,寄存器分配是编译器最重要的工作之一,如今,任何合理生产编译器必备,需要它的原因是中间代码通常使用太多临时变量,我们可以对中间代码稍显随意,正是因为我们有好的寄存器分配算法。

另一个原因是寄存器是非常重要的资源,充分利用寄存器,有程序高效利用寄存器可大大改善最终代码,更高效的代码,现在描述的寄存器分配算法针对风险机,因此对于风险机,精简指令集计算机,这类机器。

可以几乎采用我描述的寄存器分配算法,对许多这些机器,它可直接使用,CISC机器,意为复杂指令集计算机,通常对寄存器的使用有限制,某些操作只能与特定寄存器配合使用,你可能注册了不同尺寸,只能存储特定值。

因此为这些机器进行寄存器分配变得更加复杂,人们所做的就是适应我描述的图着色过程,因此,基本思想完全相同,您会认出这些算法主要是,我们讨论的图着色算法,这些算法中只有附加步骤。

以及必须观察特定寄存器可以使用的地方。

P84:p84 16-04-_Managing_Caches - 加加zero - BV1Mb42177J7

前几视频讨论了管理寄存器,本视频,将花时间讨论另一重要资源,现金,编译器能做什么不能做什么。

现代计算机系统有复杂内存层次,若从处理器最近层开始,会发现芯片上有若干寄存器,这些访问极快,通常单周期内可访问,与时钟频率相同,问题是建造高性能内存非常昂贵,因此我们无法拥有很多。

通常你知道你可能有256,比如说到8千字节寄存器总共可用,现代处理器上的很大一部分芯片面积将用于缓存,缓存也非常高性能,但不如寄存器那么高性能,平均可能需要3个周期从缓存中获取,但你可以得到更多。

现代处理器最多有一兆缓存,远离处理器的是主内存,动态随机存取存储器,分配更多时间访问更昂贵,典型值是20到100个周期,我认为你知道更多在100,如今大多数处理器接近100和20,但你能得到很多。

你得到32兆字节,这将是一台相当小的机器,最多4GB,最大配置处理器,最远是典型硬盘,这需要非常,长时间达数十万或百万周期,但可拥有大量存储,GB至TB存储。

现在,如我所说,寄存器和缓存大小速度有限,这些受限于功率,如今和其他因素一样,因此,人们希望有尽可能多的寄存器和现金,但如何做大做快有实际限制,相对于处理器的速度,现在,不幸的是,缓存未命中代价很高。

如前页所示,若能从缓存中几周期内获取,若不在缓存中,那可能需要几个数量级的时间从主内存中取出,因此,你认识的人,尝试构建,缓存,在处理器和主内存之间,以隐藏主内存的延迟,因此大部分数据在缓存中,如今。

通常需要多级缓存才能很好地匹配快速处理器,与非常大的主内存速度相匹配,所以现在处理器中通常有2级缓存,一些处理器甚至有3级缓存,那么,关键是管理这些资源非常重要,嗯,对于高性能来说。

正确管理这些资源至关重要,特别是要管理寄存器和缓存。

若要程序性能好,编译器已变得非常擅长管理寄存器,实际上,我认为今天大多数人会同意,对于几乎所有程序,编译器比程序员更擅长管理寄存器,因此,将分配寄存器的任务留给编译器是非常值得的,或分配寄存器给编译器。

然而,编译器不擅长管理缓存,虽然编译器能做一点,这就是我们将在本视频余下部分讨论的内容,大部分情况下,如果程序员想要获得良好的缓存性能,他们需要了解机器上缓存的运行行为。

他们需要了解他们的程序正在做什么,需要了解编译器能做什么,然后他们仍需编写程序,以利于缓存友好,这仍是一个开放问题,编译器能多大程度提高缓存性能,尽管我们发现编译器能做几件事。

要看到编译器实际能做到的,让我们看看这个示例循环,这里有什么,我们有外层循环j,内部循环i,每次内部循环读取b_i向量,B_i,你知道,你知道,计算该值,将结果存入a_i元素,该程序缓存性能极差。

表现会很差,所以让我们想象缓存是内存块,那么这里会发生什么,我的意思是,第一轮迭代会是什么,我们将,你知道,加载b1并存储其函数到a1,那么什么会被加载到缓存中,是a1和b1,对吧。

假设它们只是进入不同的元素,并且让我们说只是为了争论,假设它们落在缓存中的前两个元素,然后我们要进行这个的第二轮迭代,我们将会,呃,我们将加载b2并写入a2,所以,嗯,a2和b2将被加载到缓存中,对吧。

然后继续,这将会一遍又一遍地重复,加载a的一个元素和b的一个元素,重要的是要注意所有这些对a和b的引用都是缺失的,好的,每一个都是缓存缺失,因为在循环的每次迭代中,我们引用新的元素,好的。

所以我们在上一次迭代中并没有引用相同的元素,所以现在让我们暂时忽略,同一个缓存行中可能存在多个元素的事实,好的,所以如果你们中的一些人可能已经知道,当我们从内存中获取数据时,我们不会只获取一个单词。

好的,所以通常,当我们引用b1时,例如,你知道,如果b1存储在这里,我们将获取整个缓存行,这将是一块内存,它可能包含,你知道,b的其他元素,所以我们也可能同时将b的另外一些元素加载到缓存中。

但这里重要的是在循环的每次迭代中,我们引用新鲜的数据,好的,并且如果这些数据值足够大,如果它们占用整个缓存行,那么循环的每次迭代都将是对两个元素的缓存,缺失,我们不会从缓存中获得任何好处。

此循环将以主内存的速率运行,以主内存的速率,而不是缓存的速率,这里另一个重要的事情是此循环边界非常大,我特意选它很大,暗示它比缓存的大小大得多,当我们接近循环的末尾时,会发生什么,我们将填满整个缓存。

整个缓存将充满来自a和b的值,然后它将开始覆盖已经在缓存中的值,如果这个循环,你知道如果这些向量的尺寸是缓存尺寸的两倍,嗯,当我们绕回来并完成内循环的整个执行时,缓存中的内容是a和b数组的第二半。

不是第一半,然后当我们回到并执行外循环的另一个迭代时,现在缓存中的内容,也将不是我们引用的数据,所以当我们绕回来并开始内循环的执行时,第二次。

当我们引用a_sub_1和b_sub_1以及a_sub_2和b_sub_2时,缓存中的内容,是来自a和b向量高编号元素的值,不是低编号元素,所以所有这些引用都是缺失的,所以此循环的基本问题。

如果循环像这样结构化,几乎每个内存引用,并且如果数据值足够大,嗯再次,以至于它们填满整个缓存行,嗯,那么每一个内存引用都是缓存缺失。

现在让我们考虑相同程序的另一种结构,我在这里将i循环放在外层作为外循环,将j循环放在内层作为内循环,我们在这里做的是加载b_sub_i并写入a_sub_i,然后我们在相同的数值上重复该计算十次。

所以这里我们将获得出色的缓存性能,第一次引用将是缺失的,但随后的九次引用,数据将已在缓存中,或者将完全耗尽我们的计算,并且这些特定的a和b值,然后我们将继续下一个,嗯,a和b值。

我们将完成内循环并继续外层,嗯然后做外循环的另一个迭代,这种结构的优点是它将数据带入缓存,然后尽可能地使用该数据,然后再继续下一个数据,而非每项数据都做一点,然后返回,你知道,一次遍历。

然后返回并遍历所有项目,项目再次,再做一点,好吧,这种特定结构,我们交换了外层循环的顺序,抱歉,交换了内层和外层循环的顺序,它计算完全相同的东西,但具有更好的缓存缓存行为,它可能会运行超过10倍快。

现在编译器可以进行这种简单的循环交换优化,这种特定类型的优化称为循环交换,因为你只是在交换循环的顺序,在这种特定情况下,很容易看出这是否合法,编译器实际上可以弄清楚,不是很多编译器实际上实现了这种优化。

因为通常很难决定是否可以反转循环的顺序,因此通常,程序员需要弄清楚他们想要这样做,以提高程序的性能。

P85:p85 17-01-_Automatic_Memory - 加加zero - BV1Mb42177J7

本视频将讨论垃圾回收,需要几段视频讲解,本段为问题概述,后续视频将讨论具体技术。

为铺垫,先讨论要解决的问题,若手动管理内存,意味着所有分配和释放需手动完成,编程困难,易导致难以消除的程序错误,如今主要见于C和C++程序,这些是主要使用,手动内存管理的语言,因手动管理内存。

可能出现的存储错误,如忘记释放未用内存,导致内存泄漏,引用,悬垂指针,无意覆盖数据结构部分,实际上还有更多问题,尽管这些可能是最常见的,这些错误很难发现,强调这些错误通常是,复杂系统中最后发现的。

它们经常存在于生产中,有时甚至在代码投入生产后很长时间,使用和原因,存储错误通常影响远离源的时间空间,如何发生?考虑内存中的某个对象,假设它有一些字段,假设有几个字段,我保留了一些指针指向它。

程序中某个地方有对该对象的引用,现在我释放它,我手动管理内存,释放该对象,但我忘了有这个指针,现在发生了什么,该存储已被释放,不再是有效内存,但指针仍指向它,然后当我分配其他东西时。

可能会分配同一块内存,这可能是另一种对象,好的,这里可能不同类型,甚至这块内存可能用于完全不同的事情,现在我有一个指针说它认为它是一个红色对象,它指向一个蓝色物体,当我进来写入这个对象时。

当然我写的都是废话,所以这无论哪段代码持有这个指针,都认为它还是旧类型的对象,它会在这里写入一些位,当我进入程序的其他部分,可能在很远的地方并读取出来,这是一个蓝色物体,我将得到一些随机垃圾。

这可能会导致程序崩溃。

这是一个非常古老的问题,自20世纪50年代以来一直在研究,我是在Lisp中首次认真考虑的,有一些众所周知的完全自动内存管理技术,因此,您不必自己管理内存,这只是在20世纪90年代才成为主流,实际上。

在Java流行之前,当时没有主流语言使用自动内存管理,因此,在那之前,没有主流语言使用自动内存管理,所以现在仅剩最后一步,近二十年来,垃圾收集和自动管理已成为主流编程技术。

自动内存管理的基策略相当简单,当一个对象被创建时,当我们分配一个新对象时,系统,运行时系统将找到未使用的空间分配给该对象,它将直接分配它,因此,当你使用'new'和某个类名时,系统将自动分配一些内存。

系统自动分配未用内存,重复多次同样操作,不久将用完空间,最终无剩余未用空间,最终需采取措施,需回收部分空间分配更多对象,垃圾回收系统依赖观察,部分使用空间可能被不再使用的对象占用,它们。

这些对象不会被程序再次引用,如果我们能找出,那些是,哪些不再使用的物体,然后我们可以定位它们并重新利用空间。

所以大问题来了,我们如何知道一个物体将不再被使用,目前大多数垃圾收集技术都基于以下观察,程序只能使用它能找到的物体,我们所说的'它'是什么意思,我将切换颜色,嗯,让我们看看这段代码,那么会发生什么呢。

当我们执行这个时,首先,我们将分配一个a对象,并将其分配给x,因此,x将指向该对象,然后在let的主体中会发生什么呢,我们将把x,分配给y指向的值,所以y是另一个变量,它指向内存中的其他对象,好的。

接下来会发生什么,现在x将指向这个对象,现在注意,这个对象a不可达,意味着它没有引用,不再有任何指针指向它,我怎么知道这一点呢?因为它在这里是全新的,当它被创建时,我只创建了一个指针指向它x。

然后我立即将x赋给了其他东西,所以我丢失了唯一的指针,程序中没有对a的任何引用,因此程序将永远无法找到它,如果程序,如果程序中没有变量或数据结构指向a,那么a将永远无法被程序引用,在未来,在未来。

后续程序执行无指针指向,因此将不再使用a,空间可回收用于其他对象。

实际上需要更广义对象可达性定义,而非示例所示,让我们看看,对象x可达,当且仅当以下之一为真,要么寄存器含x指针,要么x可立即从某寄存器访问,记住寄存器包含局部变量等,它们只是,你知道。

程序可立即访问的值,或另一个可访问对象y包含指向x的指针,这说明了什么,嗯,这意味着你将从寄存器开始,所以程序可能使用几个寄存器,然后你会查看那些寄存器指向的所有东西,它们指向的所有对象。

你会查看这些对象中的指针和它们能指向的所有内容,好的,其中一些可能会重叠,我的意思是,其中可能有一些可以通过多个路径到达的东西,从寄存器开始,你可以到达的完整集合,从寄存器开始,跟随所有可能的指针。

这些都是可达对象,然后该集合的补集,不可达对象,即不可触及之物,其余所有对象,那些你无法通过递归,从寄存器开始并跟随指针,所能触及的,那些对象永远无法使用,显然实现只能通过寄存器访问,然后仅能通过。

你知道的,从寄存器可触及的对象中加载指针,凡不能通过步骤触及,将不再使用,是垃圾。

让我们看另一个例子,展示可达性和自动内存管理的有趣方面,例子首先做什么?它在堆上分配一个a对象,并将其赋给变量x,所以x是指向该对象的指针,然后它分配一个b对象,y将指向该对象,然后将y的值赋给x。

我们有这个配置,现在,嗯,让我们,嗯,在这里画条线,好的,我们稍后会回来,记住这个时间点,这个时间点事物看起来如何,然后我们会离开,我们将执行这个条件,知道条件会做什么,它总是为真,好吧。

因此谓词始终为真,因此它永远不会走假分支,它将永远只走真分支,那它将做什么,它将立即覆盖x,因此x最终将指向,另一个新对象,无论它是什么,现在让我们说在这一点,在这里我们尝试进行垃圾回收,所以你知道。

出于某种原因,程序在此停止,尝试回收未用内存,它能收集什么,就像以前一样,因为到这一点的例子本质上相同,嗯,我们可以看到此对象不可达,好的,因此,第一个a对象在那时变得不可达,现在可以收集。

第二个对象呢,嗯,它是可达的,显然可达,可通过x到达,好的,在这一点上,它也可以通过y到达,所以它不是垃圾,不会被收集,但请注意,x的值总是会被覆盖,好的,因此,程序,编译器不知道这个分支总是为真。

所以它没有意识到x在此处的值,将永远不会再次被使用,但该值每次我们采取这个条件时都会被立即覆盖,此外,如果y在程序的其他地方不被使用,如果y在此处已死,假设y在这里死了。

那么对b的这两个引用将永远不会被触及,实际上,b的值将永远不会再次被使用,尽管它是可达的,这告诉你可达性是一个近似值,我的意思是,它是一个永远不会再次被使用的对象的近似值,我们真正感兴趣的是。

当我们做垃圾收集时,是收集未来执行程序时将不会被使用的对象,因为显然,这个空间被浪费了,可以用于其他可能更好的用途,可达性近似于此,如果一个对象不可达,它肯定不会再被使用,然而。

仅仅因为一个对象是可达的,并不意味着它将被再次使用。

所以现在让我们谈谈如何在酷C中做垃圾收集,酷C有一个相当简单的结构,它使用一个累加器和,它指向一个对象,当然,指向一个对象,该对象可能指向其他对象等等,因此,我们必须跟踪从累加器可达的所有对象。

但我们也要担心堆指针,栈上也有可访问的东西,每个栈帧,当然,可能包含像这样的指针,和,例如,存储在栈上的方法参数,每个栈帧也可能包含一些非指针,好吧,所以如果我想激活记录的布局。

现在将有一些指针和非指针的混合,比如返回地址,所以我们必须知道帧的布局,但如果我们知道布局,当然编译器决定布局,所以它自然地知道布局,它可以找到帧中的所有指针。

所以本质上编译器必须为每种激活记录保持记录,它为每个方法构建,所以如果你有一个方法Foo的激活记录,假设该激活记录有四个插槽,那么编译器需要跟踪哪些是对象指针。

也许帧的第二和第四个元素总是指向对象的指针,其他两个总是非指针,所以编译器必须在某个地方跟踪这些信息,以便垃圾收集器在运行时知道,当它查看Foo的激活记录时,需要跟随的指针在哪里。

所以在Cool See中,我们从累加器和栈开始跟踪,这些被称为根,好的,在垃圾收集术语中,根是从您开始跟踪所有可达对象的所有寄存器,如果我们这样做,所以你可以看到,我们这里有我们的对象。

这里我们有累加器,对不起,还有我们的栈指针,所以我们可以简单地浏览这个内存的小图表并找到所有可达的对象,累加器指向对象a,所以我们将它标记为可达,A指向C,所以我们将它标记为可达,C指向E。

所以我们将它标记为可达,栈指针有几个帧,第一个帧没有指针,第二个帧指向E,我们已触及那个,它已被标记,因此可再次标记,但没关系,只要有人标记,现在未标记的皆不可达,我们遍历可达对象时未触及哪些对象。

它们是对象b和d,因此是不可达对象,它们可被回收,存储可再利用,注意,一个对象有指针指向,并不意味着它可达,注意这里对象d指向它,好的,但对象d不可达,为什么?因为指向它的指针,都来自其他不可达对象。

所以重要的是要知道,并非所有不可达对象都没有指针,将会有一些不可达对象,或可能有一些不可达对象实际上有指针指向它们。

但它们只会来自其他不可达对象,垃圾收集方案如下步骤,按需为新对象分配空间。

只要还有空间,就继续分配新空间,或当我们需要时,当空间用尽时,在需要计算时,哪些对象可能再次被使用,通常通过跟踪从一组根寄存器可达的对象来完成,然后释放该集合的补集,有些策略会在空间耗尽前进行垃圾回收。

我们将在下期视频中查看其中之一。

P86:p86 17-02-_Mark_and_Sweep - 加加zero - BV1Mb42177J7

本视频将讨论,三种垃圾收集技术中的第一种,我们将详细查看,第一种是标记清除。

标记清除分两个阶段,不出所料地称为,标记清除,标记阶段将跟踪所有可达对象,当内存用尽并停止垃圾收集时,我们首先要做的是找出所有可达对象,然后清除阶段将收集所有垃圾对象,为了支持这一点。

每个对象将有一个额外的位,其中某个地方称为标记位,这是为内存管理保留的,除了垃圾收集器外,它不会被任何东西使用,在开始垃圾收集之前,每个对象的标记位始终为零,然后在标记阶段将标记为可达对象的位设置为1。

因此,当我们标记一个对象时,我们用1标记它,这表示该对象可达。

这是标记阶段,它将是一个基于工作列表的算法,因此,我们最初的工作列表包含所有根,即所有初始指针保存在寄存器中,然后,只要工作列表,待办事项列表不为空,我们将执行以下操作。

我们从待办事项列表中取出某个元素v,我们从待办事项列表中删除它,好的,这是算法的核心,如果对象v尚未标记,那么我们将标记它,好的,我们将它的标记位设置为1,然后,我们找到它内部的所有指针。

然后将它们添加到我们的工作列表中,好的,现在,v指向的所有内容都将添加到工作列表中,如果v已经标记,那么我们已经处理过它,并且我们已经将所有它指向的内容添加到工作列表中,那么然后我们什么也不做。

这里没有else分支,我们只是丢弃它,嗯,从待办事项列表中。

因此,一旦我们完成了标记阶段,每个可达对象都被标记,然后甜阶段将扫描堆,寻找标记位为零的对象,甜阶段将遍历所有内存,它将从堆的底部开始,遍历堆中的每个对象并检查其标记位。

所以它找到的所有标记位为零的对象,它们在标记阶段未被访问,显然不可达,所以所有这些对象将被添加到空闲列表,当我们遍历内存时,还有一个重要的细节,任何标记位被设置的对象,它的标记位将被重置为零。

以便为下一次垃圾收集做好准备。

这是清扫阶段的伪代码,这个小函数size_of_p是块的大小,指针p开始的对象的大小,正如您将看到的,这是我们在Cool对象中编码对象大小的原因,记住,在Cool对象的标题中,有一个大小字段。

这是为了垃圾收集器,当它在内存中行走时,可以弄清楚对象的大小,无论如何,我们从堆的底部开始,只要我们还没有到达堆的顶部,我们这样做,我们看看我们指向的地方,然后我们将始终指向一个对象的开始。

所以我们检查该对象的标记位是否为1,如果是,那么它是一个可到达的对象,所以我们只是将其标记位重置为零,否则,如果其标记位为零,那么我们将该块内存,即对象的大小添加到空闲列表,好吧,并将其添加到空闲列表。

最后,在任何情况下,好吧,我们将p递增为其指向的对象的大小,所以我们指向下一个对象,我们将重复该循环一遍又一遍,嗯,重置被触及的东西的标记位,并将未被触及的东西添加到空闲列表。

直到我们触摸了堆中的每个对象。

这里有一个小例子,所以我们现在从这里开始有一个堆,我们将假设只有一根根简单起见,这里是所有对象,初始标记位为0,我们有空闲列表,这里是初始空闲列表,注意,你知道有一些内存在空闲列表上,好的。

标记阶段后发生了什么?我们遍历了所有可达对象,我们从A开始,当然,我们将其标记位设为1,然后跟随从A可达的指针设置标记,但跟随从C可达的指针,设置那里的标记位,因此我们最终得到一个,C和E被标记。

其他什么都没标记,好的,现在甜蜜阶段将遍历,内存将重置所有标记位,呃,为零,并在发现不可达对象时,例如,B和D,将其添加到空闲列表,因此最终空闲列表将是什么?将是一个内存块链表,可供未来分配。

现在,这个算法非常简单,概念上我认为它是,它非常清楚如何工作,但有一些棘手的细节,这是自动内存管理算法非常典型的,并且标记阶段实际上有一个严重问题,这也是垃圾收集算法非常典型的,现在请注意。

我们仅在用完空间时运行此算法,好的,整个目的是我们正在垃圾收集,因为已经没有系统内存可用于分配新对象,并且我们有一个待办事项列表,好的,请注意待办事项列表的大小没有限制,没有关于我们将有多少元素的保证。

在待办事项列表上,我认为很容易看出该数据结构实际上可能相当大,正确,因此我们不能为待办事项列表分配固定数量的内存,或保留一些恒定的空间,但我们需要处理的事实是,当我们开始进行垃圾收集时。

实际上没有任何空间,现在有一种技巧可以在标记阶段维护待办事项列表,而无需使用任何额外存储,那就是执行所谓的指针反转,因此,当指针被跟随时,它将反转以指向其父。

这将使我们实际上能够跟踪堆中哪些元素或哪些对象,仍然需要处理而无需使用任何额外空间。

如果你不明白这一点,马上举一个例子,还想提第二个问题,你知道自由列表存储在哪,这个更容易看出如何工作,自由列表由内存块组成,用这些块的空间维护自由列表,块内存的第一个词或类似的东西包含块的大小。

第二个词指向列表中的下一个块,类似这样,我们可以使用块本身的空间来维护自由列表,现在回到反转的想法,假设我们有一些对象,我们要跟踪可达性,好的,不能在单独的数据结构中维护待办事项列表,好吧,嗯。

那么我们将如何做到这一点呢,好的,这就是想法,让我改变颜色,我们将进入这里,我们将标记第一个对象,假设这个对象从根部可达,现在这是根,第一个对象,现在我们将跟随这个对象中的指针,假设这是一个。

这是对象中的第一个指针,我们将跟随它,然后我们将反转它,我们将让它指向父节点,所以现在我们将标记这个对象,然后我们将跟随这个对象中的指针,好的,当我们向下走时,这个指针将指向回,然后我们将标记这个对象。

现在对象中没有指针了,所以我们需要回到并处理任何未被,在我们已经看到的对象中覆盖的指针,好的,我们如何找到回去的路呢,这就是指针反转的目的,所以我们沿着蓝色箭头回到这里,当我们回来时。

我们将恢复原始指针,我们将删除反转的指针,这个对象中也没有更多的指针了,所以我们将回到上一个对象,当然,这个指针也会消失,我们将恢复原始指针,现在我们在这个对象中,我们看到还有一个未跟进的指针,好的。

然后我们会跟随它并反转它,我们将跟随这个指针并反转它,当我们到达这个对象时,我们将标记这两个对象,我们发现没有其他指针,我们可以使用这些蓝色箭头回溯,当我们向上遍历对象时,我们将恢复红色箭头,本质上。

指针反转的作用是,它帮助我们为图的深度优先搜索维护栈,所以如果你正在对图进行深度优先搜索,并且你想确保覆盖所有可到达的节点,那么你必须能够回溯,反向指针允许我们这样做,关于反向指针还有一个小问题。

注意这里有一点问题,当我谈论反向指针时,让我画两个新对象,只是为了说明这一点,假设我从这个对象指向那个对象,当我跨过指向的对象时,反转这个指针意味着什么,嗯,你知道指针的空间实际上在这个对象中。

目标对象中可能根本没有指针的空间,我即将到达的,实际上会发生的是,假设这是一系列对象的一部分,好的,这个问题很容易解决,问题只是一个偏移量为一的问题,我在对象中有指针的空间,我可以更改那个指针。

我不知道这个对象中是否有任何指针,但假设这是一系列对象的一部分,好的,我沿着这个链走到了这个特定对象,当我跨过到第三个对象时,但我要反转的指针是这个,我会让它指向前一个对象,好的。

然后我会记住这个特定对象,我会将这个特定对象的指针保存在寄存器中,我会将最后遍历的指针保存在寄存器中,我会将来自上一个对象的指针保存在寄存器中,然后当我继续到另一个对象时。

我将使用当前对象中正在遍历的指针,指向前一个对象的父对象,好的,所以只是一个稍微偏移量为一的问题,需要一个寄存器保留上次访问的对象,然后可以反向指针回到父辈和祖辈。

总结标记和清除讨论,从空闲列表分配新对象的空间,那里有个打字错误,我们总是选择一块,总是从空闲列表中选择一块,足够大以容纳要分配的对象,从该块中分配所需大小的区域,剩余部分放回空闲列表。

假设空闲列表有一个块,说它有100字节,然后我们需要一个50字节的物体,所以会发生的是这个块将被分割,我们使用第一个一半,第一个50个用于对象,然后这部分剩余的放回空闲列表,那种策略的结果是。

我们必须找到足够大的块,但可能不会使用整个块,标记和清除会碎片化内存,我们可能会留下很多小块剩余内存,可能没有足够大的块来实际持有对象,这些块,这些小块可能散布在各个地方,因此实际上对于标记和清除来说。

也,嗯,合并块是可能的,因此,我们需要在可能的情况下合并空闲块,因此,当清除阶段处理空闲列表时,它需要识别何时有两个相邻的内存块,它们在内存中是立即相邻的。

所以如果我有两个连续的块,我真正想要做的是将它们合并成一个大的块,并在空闲列表中只保留一个条目,以对抗内存碎片化,现在一个很大的优势,也许是最大的优势是标记和清除,在垃圾收集期间对象不会被移动。

这意味着我不需要更新指向对象的指针,对象保持不动,它们不会作为垃圾收集的一部分移动,这意味着,实际上有可能为像C和C这样的语言适应标记和清除,加加,所以在C和C加加中指针暴露给程序员。

所以程序员可以操作指针和测试指针,在C和C加加中不能移动对象,因为你知道指针是它们的语义的一部分,指针地址,我应该说,是它们语义的一部分,人们实际上已做到,或者你知道,C和C++的标记清除垃圾收集变种。

正是因为对象不会移动。

P87:p87 17-03-_Stop_and_Copy - 加加zero - BV1Mb42177J7

本视频将讨论,第二种垃圾收集技术。

停止并复制,在停止并复制垃圾收集中,内存分为两个区域,有一个旧空间用于分配,程序当前使用的所有数据,都存放在称为旧空间的区域,然后有一个新空间,为垃圾收集器保留,程序不使用这个空间,这是为GC保留的。

停止并复制垃圾收集中的第一个决定是,程序只能使用一半的空间,有一些技术,更高级的停止垃圾收集技术,允许程序使用超过一半的空间,所以这并不像听起来那么糟糕,但本质上,空间的一大部分必须为垃圾收集器保留。

现在,分配方式是在旧空间中有一个堆指针,堆指针左侧的所有内容目前都在使用,这是所有已分配对象所在的区域,我用红色标出的这个区域,当需要分配新对象时,我们简单地将其分配在堆指针处。

因此堆指针将简单地向上移动,并为下一个要执行的对象分配一些块空间,它将不断穿过旧空间进行分配,随着您分配更多对象,好的,因此分配只是推进堆指针,停止并复制的一个实际优势是简单快速的分配策略。

现在,呃,最终,当然,如果我们一遍又一遍地分配,我们将填满旧空间,垃圾收集将开始,GC将在旧空间满时开始,它将做什么,它将复制所有可达对象,所有可达对象从旧空间复制到新空间,这个想法的美妙之处在于。

当你复制可达对象时,垃圾被留下,所以你只需捡起你正在使用的所有数据,将其移动到新空间,而你不再需要的所有垃圾都留在旧空间,然后在您将东西复制到新空间后,首先,由于你留下了垃圾。

收集后使用的空间比以前少了,新空间现在有空余,然后交换旧空间和新空间的角色,旧新空间反转,旧的变新,新的变旧,程序继续。

看个快速例子,了解如何工作,假设这是旧空间,这是旧空间,有一个根,是对象a,我们要做什么,复制从a可达的所有对象,并移到新空间,那会是什么样子,这是之后的样子,我们追踪一下,从a开始,跟随指针。

从main看到指向c的指针,好的,c可达,然后指向f的指针,然后f指向a,所有可达对象,复制它们,复制时,也复制指针,现在指针都改变了,在a的复制中,现在指向c的复制,好的,当然c将指向f的复制。

这里有点问题,这条线不在正确位置,所以应该像这样,然后f指向a的复制,我们不仅移动对象,也移动指针,并调整它们,所以真的将整个对象图复制到新空间,现在使用较少空间,这里有些空闲,好的,这将成为旧空间。

这是现在的旧空间,嗯,这是现在的空间,将用于下次垃圾收集。

总结讨论,停止复制的一个基本问题是确保,找到所有可达对象,我们同样看到标记清除垃圾收集的问题,真正区分停止和复制的是我们将复制这些对象,因此,当我们找到一个可达对象时,我们将其复制到新空间。

这意味着我们必须找到并修复所有指向该对象的指针,实际上,正确地做到这一点并不明显,因为当你找到一个对象时,当然,你看不到所有指向该对象的指针,我们如何做到这一点,好吧,这是一个想法,当我们复制对象时。

将存储旧版本,它被称为指向新复制的转发指针,所以让我们看看那会是,那看起来像什么,我们有旧空间,我们有新空间,假设我们在旧空间中发现可访问对象a,所以我们要做的是,我们将在这里制作它的副本,在新空间。

这很容易做到,但接下来我们要做的是,我们将重用其空间,并将存储称为转发指针的东西,我们将在其中,首先我们将标记,以某种方式表明已被复制,这将有一些特殊标记,我将简单地,你知道,用紫色标这里。

条形图或类似,将以某种方式标记,以便我们知道该对象已复制,然后在对象的一个显眼位置,我们将存储转发指针,可以将其视为转发地址,所以如果你知道某人住哪,你可以去他家,如果他们已搬走,可询问转寄地址。

正是如此,然后可去他们新家,无论他们搬去哪,可能找到他们,这就是将发生的事,若稍后有指针指向此对象,甚至很久后,在垃圾回收中,我们可能发现此指针,我们可能跟随此指针,找出此对象的点,意识到此对象已移动。

因为我们已标记它且对象已移动,然后可用前向指针找出新对象位置,然后更新此指针,使其指向新对象。

现在与标记和清除类似,我们仍需解决如何实现对象图遍历,而无需使用任何额外空间,当这些垃圾收集算法,它们仅被使用时,仅在低内存情况下运行,不能假设能构建无限数据结构,与垃圾收集器一起使用。

垃圾收集器需要在恒定空间工作,这是将使用的想法,用于停止复制算法以解决问题,我们将划分新空间,这是新空间,分为三个连续区域,我们将有,嗯,最右边那个怎么了,我们将分配新对象的空白区域。

有一个分配指针指向该区域的开始,这是我们填充的,正在复制的对象,现在这是未使用的空白空间,嗯,该区域左侧是已复制但未扫描的对象,但未扫描,好的,这是复制的,不是扫描的,那意味着什么?

这意味着对象已被复制,所以我们实际上,你知道,在新空间中复制了对象,但我们尚未查看其指针,我们尚未查看对象内的指针以了解它们指向何处,在那左边是被复制和扫描的对象,这些是已被复制的对象。

已处理所有对象内指针,可视为此区域。

扫描指针与分配指针间,工作列表,仍需处理的对象,已复制对象,可能仍指向未复制对象,需检查指针的对象,看是否指向需复制对象,完成垃圾回收,回到我们的小例子,现在我将逐步讲解。

停止和复制垃圾收集器如何逐步收集这个特定堆,请注意,我们只有一个根对象,它是okay,我只想指出a有一个指针指向对象c。

好的,所以第一步我们要做的是,我们将a对象复制到新空间,好的,这实际上是一个位对位复制,所以我们只是取a的位,并做一个复制,你知道,不做任何内部对象的检查,将其复制到新空间,那怎么工作,当然。

我们的分配指针不在,最初在这里,在新空间的开始,然后我们复制这个对象,然后这意味着分配一个对象,现在分配指针指向我们刚刚分配的内存的第一个字,超出对象,好的,当我们复制它时会发生什么。

因为这只是一个位对位复制,a中的所有指针仍然指向它们之前指向的对象,即旧空间中的对象,请注意,这个a的副本指向旧空间中的对象c,我们还做了另一件事,即在旧副本的a中留下一个转发指针。

所以我们标记a已经被复制,这就是它灰色的原因,这表示该对象已经移动,在这个虚线中,表示在a的某个地方我们存储了一个指向新a副本的指针,现在我们可以开始算法了,请注意。

这里有一些已经被复制但未被扫描的对象,所以这是我们的工作列表,所以现在我们将不断处理这些对象,我们如何知道那里有对象呢?我们只需比较扫描和分配指针,所以如果它们,如果它们不同。

如果在扫描和分配指针之间有一个对象,至少两个指针之间有一个对象,那么就有工作要做,有一个对象需要被扫描,呃,那,呃,和,并可能导致更多对象被移动和分配。

接下来会发生什么,我们处理对象a,我们遍历a,找到所有指针,复制a指向但未移动的对象,之前我们说a指向,这个a的副本指向旧c副本,现在我们发现c对象未被移动,仍在旧空间,我们复制它。

更新a的指针指向新c副本,当然,扫描指针划过一个,已扫描所有指针,好的,分配指针也移动,因为要为c分配空间,当然,他只是一个,旧空间的逐位副本,所以任何指针,指向尚未移动的物体,就指向旧空间。

这种情况下,对象c指向旧空间中的对象f,我大概应该在这里指出,这是原始的分割线,你知道,这里是旧空间,这里是新空间,最后我们标记c已被复制,好的,它已被移动到新空间,并留下了一个转发指针,如有问题。

可修复指针,指向未来可能遇到的问题,现在,嗯,需继续扫描已复制但未扫描的对象,可见扫描与分配指针间有对象c,现处理c内所有指针。

接着扫描c,发现其指向f,f尚未移动,因此将f复制到新空间,我们更新指针c,现在c已复制并扫描,好的,扫描指针越过c,当然f也是逐位复制,因此,所有指向旧空间的指针仍指向旧空间,特别是f指向a。

分配指针再次移动,因为我们移动到了f,现在我们必须处理f。

这将是我们要移动的最后一个对象,那么会发生什么呢,我们发现f指向a,好的,a已经被标记为已移动,并且有一个转发指针,而不是复制a,我们仅更新f中指向旧版a的指针,指向a的副本,所以现在f完全扫描。

f中的所有指针都已处理,我们没有分配任何新对象,因此分配指针没有移动,现在扫描指针和分配指针相等,它们之间没有对象,因此我们的工作列表为空,这是垃圾收集堆,这是一个完全图,一个完整副本。

应该说从旧空间可达对象图。

所以现在完成,我们简单地交换新和老空间的角色,然后我们恢复程序,当程序再次运行时,它将从这个区域分配并超出分配指针,直到填满现在旧的空间,这将用于下次垃圾回收的新空间。

伪代码算法概述如何停止复制垃圾回收,扫描和分配指针不同,记住,我们一直运行,直到扫描指针赶上分配指针,它们相等,我们要做的是,我们将查看扫描指针处的对象,称为对象o,然后对于每个指针。

我们将执行以下操作,我们将找到指针指向的对象o',对于每个指针,我们将找到其指向的对象o',然后有两种情况,一种是没转发指针,若没转发指针,需将对象复制到新空间,涉及分配新对象和更新分配指针。

然后设置这里,第一个词不应强调,第一个词设一个字,所以是个特殊词,这才是重要的,我们要知道用哪个词,它总是同一个词,但无论如何,我们用旧物体的词指代新副本,标记旧物体已复制 标记旧物体。

已复制,好的,这样我们就能知道,当我们,如果我们遇到一两点,再次,我们知道它已被移动,然后更改指针指向新的o'副本,好吧,如果有,我们就这样做,如果没有转发指针,如果有转发指针。

那么我们只需更新指针指向转发指针指向的地方,然后我们重复这个循环一遍又一遍,呃,直到我们扫描了所有已复制的对象,所以就像标记和清除一样,当我们扫描一个对象时,我们必须知道它有多大。

我们还需要知道对象中的指针在哪里,所以,如果我们考虑一下这一点,假设我们正在扫描这个对象,所以这是我们的扫描指针,现在我们想处理它所有的指针,我们必须知道指针在哪里,如果这里有一个指针,这里有一个指针。

我们需要能够找到这些指针,我们不想将它们与对象的其他字段混淆,那些可能看起来像指针的字段,所以我知道整数的位模式可能看起来非常像一个指针,现在这不是一个大问题,因为编译器,当然,确定堆中对象的结构。

并且可以将该信息存储在某个地方,以便垃圾收集器可以通信,所以他们将能够找到指针,所以你可以很容易地想象,程序中存储的一些信息指示每种类型,指针在哪里,同样,一旦我们扫描了这个对象。

我们需要能够将扫描指针向前移动,仅超出该对象,以便我们可以找到下一个对象的开始,这就是为什么我们需要知道这个大小,好的,我们需要知道这个大小,以便扫描指针可以,呃,移动,通过对象。

我们可以找到下一个对象的开始,另一个问题是,每当我们进行垃圾收集时,我之前没有提到这一点,但应该清楚我们还需要扫描和复制栈中指向的对象,我们还需要更新栈中的指针,这实际上可以成为一种昂贵的操作。

与停止复制一起,因为你知道你仍然必须每次进行收集时遍历整个堆栈,以确保你已经复制了栈中指向的所有对象,总结:停止并复制,我认为可以这么说,普遍认为是垃圾收集的最快技术,当然。

我相信基于停止和复制的变种是最有效的方法,众所周知,自动内存管理分配非常便宜,好吧,因此,你所要做的就是增加热指针,你只需将单个指针向前移动以分配空间,没有复杂的空闲列表需要遍历或关于。

知道在哪里放置对象的决定,你知道你只会直接在分配指针处分配它,因此,内存管理的这一部分非常便宜,同时,收集也相对便宜,有趣的是,它特别便宜,如果有大量的垃圾,因为,因为我们正在复制可达对象,嗯。

停止复制仅接触可达对象,它没有,特别是没有接触垃圾,所以如果你考虑一下这一点,这意味着垃圾收集,嗯,是停止和复制的大小,所以无论你正在复制的子图是什么,那就是垃圾收集的成本,这与标记和清除形成对比。

因为成本与您使用的所有内存成正比,因为您有清扫阶段,您必须遍历并接触每个单个对象,无论是活的还是垃圾,好的,因此,如果您有相对较多的垃圾和相对较少的活跃对象,停止复制实际上比标记和清除快得多,当然。

停止复制的缺点是它在某些语言中移动对象,特别是C和C++不允许您移动对象,因为对象居住的地址实际上是可见的,在程序中暴露,是对象语义的组成部分,在那里,您真的必须使用标记和清除。

P88:p88 17-04-_Conservative_Col - 加加zero - BV1Mb42177J7

在这段短视频中,我将谈论一种称为保守垃圾收集的技术,可用于类似C和C++的语言。

加加,回顾自动内存管理依赖于能够找到所有可达对象,还需要能够找到对象中的所有指针,现在,为类似C或C++的语言进行垃圾收集的困难在于,很难甚至不可能以100%的可靠性识别内存中的对象内容,因此。

如果我们看到内存中的两个单词,你知道它可能是一个列表单元,具有数据和下一个字段,所以如果我们只看到这里的两个单词,并且这里有一些位模式,零和一,好吧,我们如何知道这些是否都是指针,我的意思是。

它可能是一个指针,另一个不是,在列表单元的情况下,所以这些字段中的一个只是数据,如整数,然后另一个是一个指针,或者它可能像二叉树节点一样,这两个单词都是指针,由于C和C++类型系统的弱点。

我们无法保证我们知道所有指针的位置,现在,事实证明,有可能扩展垃圾收集技术。

以与类似C和C++的语言一起工作,基本想法或见解是,总是可以保守,如果我们不确定某物将来是否会被使用,那么我们就会保留它,并记住图可达性已经是保守的技术,我们真正想要保留的是未来将被使用的对象。

但对象图中的可达性是对此的近似,因为可达对象可能被使用,现在,C和C++的问题是我们不知道指针在哪里,我们没有从类型系统中关于指针位置的确切保证,因此,基本技巧是,如果某物看起来像指针。

那么我们将把它当作指针对待,我们只需要保守,如果我们不确定内存中的某个单词是否是指针,那么我们就可以把它当作指针对待,并保留它所指向的内容,嗯,如果我们不确定,我们不会移动或更改它,那将没问题。

那么如何决定内存中的某个单词是否是指针,它应该对齐,意味着你知道它应该以一些零结尾,以指示它指向,如果它是一个指针,指向一个字边界,如果是的话,然后无论是什么位模式,如果我们解释为地址。

它必须是一个有效地址,因此它们应指向数据段,注意,你知道这两个条件将排除内存中所有类型的数据,所以,例如,任何小整数可能无法解释为数据段中的有效地址,所以你知道,最有可能,只有指针。

或非常少不是指针的东西将被视为指针,我们要做的是,然后如果看起来像指针,我们将考虑它为指针,我们将跟随它,然后我们将高估可达对象的范围,我们可能会保留一些完全不可达的东西,但没关系。

保留比必要更多的总是好的,现在我们仍然不能移动对象,对吧,因为我们不能更新指向它们的指针,如果我们不知道某物是指针,我们当然不想改变它,好的,你知道所以,例如,如果我们认为某物是指针。

它实际上是一个帐号号码,然后我们更新了指针,当我们移动对象时,我们完全改变了程序的作用,所以这仅适用于标记和清除。

P89:p89 17-05-_Reference_Counti - 加加zero - BV1Mb42177J7

本视频中,我们将结束关于自动内存管理的讨论,以我们要讨论的垃圾收集的第三种也是最后一种技术。

称为偏好计数,计数。

基本思想是,而不是等待内存完全耗尽,我们将尝试收集一个对象,一旦没有更多指针指向它,因此,一旦我们丢弃最后一个指向对象的指针,它变得不可达,我们将在那时尝试收集它,我们如何做到这一点呢,正如名称所示。

我们将计算每个对象的引用次数,因此,在每个对象中我们将存储指向该对象的指针数量,因此,如果我在内存中有对象,并且它有来自其他对象的三个指针,并且在这个对象中某个地方会有一个专门的字段包含数字三。

如果这个数字降到零,如果我们丢弃这些指针并且这个数字变成零,那么我们就知道没有人指向这个对象,它可以被释放,这意味着每次赋值都必须操纵引用计数,以保持指向对象指针数量的准确计数。

因此,分配新对象将返回一个引用计数为1的对象,因此,对象由new创建,它将已经有一个引用计数为1,返回的指针是对象的唯一引用,我们将写对象的引用计数,X是X的引用计数,现在当我们有一个赋值时。

X被赋予Y,嗯,我们将不得不更新X指向的对象和Y指向的对象的引用计数,在赋值之前,所以这里发生了什么,所以如果Y指向P,让我们在这里画我们的对象,所以Y是一个局部变量,它指向内存中的某个对象P。

X也是一个局部变量,它指向某个对象,O好吧,所以现在X正在获取Y的值,这将移动这个指针从我之前指向的地方,指向与Y相同的东西,那么会发生什么呢,P的引用计数将增加1,而O的引用计数将减少1。

因为我们减小了O的引用计数,当我们丢弃了这个指向对象O的指针,O,需检查引用计数是否为零,若引用计数降为零,可释放o的内存,然后,除更新引用计数和检查o的引用计数是否为零外,实际还需执行赋值本身。

因此每次赋值,我想强调,程序中每个赋值现在都转化为需执行的四个操作以维护。

引用计数,引用计数有优缺点,其一大优点是增量收集垃圾,执行时无长时间暂停,因此对于大型暂停可能成问题的应用,如实时应用或交互应用,引用计数有很大帮助因为它最小化了最长暂停时间,好的,因此程序不会暂停。

不会在一段时间内停止运行,因为它在收集垃圾,它总是以小增量收集垃圾,因此你不会看到长暂停,或至少基本引用计数实现也相当容易,很容易遍历并修改代码以添加引用计数,可以很容易地想象一个代码生成器。

它会为添加引用计数的实现生成不同的代码,所以实际上,简单实现引用计数对编译器所需的变化,并不那么广泛,现在有一些缺点,嗯,关于引用计数,嗯一个是在每次赋值时操作引用计数真的很慢。

所以如果你记得发生了什么,我们有两个引用计数的更新,所以我们必须更新,你知道两个对象的引用计数,嗯,为了做到这一点,这是执行赋值的代码,然后我们有一个if语句,然后我们实际执行赋值。

所以有两个引用计数更新,检查引用计数是否为零的测试,然后我们实际做赋值,所以开销很大,你正在将程序中的每个赋值,将其成本至少膨胀四到五倍,这将对许多程序的性能产生非常明显的影响,现在可以优化引用计数。

例如,如果我们对同一对象有两次更新,假设,在一个基本块内,甚至在一个控制流图中,编译器,一个智能优化编译器可以经常合并这些引用计数操作,所以,而不是更新对象的引用计数两次,它可以只更新一次,同样。

如果有更多的引用更新到同一对象,潜在的所有这些都可以在程序的某个区域合并,问题是,这变得非常棘手才能正确,一个简单的引用计数实现相当慢,但容易正确,一个非常高级的引用计数实现。

或高度优化的引用计数实现稍微快些,但仍然有明显的性能影响,如果你对所有对象进行引用计数,但它比简单实现快得多,然而,它很难正确实现,引用计数的另一个问题是它不能直接收集循环结构,为了看到这一点。

让我们画一个小堆,带有循环结构,假设我们有一个局部变量,X,它指向堆中的某个对象,该对象有一个指针指向另一个对象,好吧,然后第二个对象有一个指针回到第一个对象,好的。

所以这里x指向一个长度为二的循环链表,好的,如果我们在这里添加引用计数,它们会是什么样子呢?这个对象在这里,第二个对象这里只有一个引用指向它,所以它的引用计数是1,而这个第一个对象有两个指针指向它。

一个来自x,另一个来自另一个对象,所以它的引用计数是2,好的,这是我们的小堆,我们可以看到这里没有垃圾,因为所有对象都可以从局部变量或程序变量中访问,如果我们给x赋一个新值。

假设我们有了赋值语句x得到null,好吧,这个指针就消失了,那么会发生什么呢?当我们做那个赋值时,我们将改变这个对象的引用计数,现在它将变成1,如果我们看这里,嗯,热量,我们现在看到这些物体。

这两个物体不可达,好的,所以这些是不可达的,但请注意它们的引用计数不为零,因此我们不能收集它们,垃圾收集器或引用计数实现将检查引用计数并看到,哦,这些是1,因此我们不能删除它们。

它看不到的是这些对象的唯一引用来自其他,不可达的对象,所以底线是引用计数不能收集循环结构,处理它的方法只有两种,一种是程序员记得,每当循环结构即将变得不可达时,以某种方式打破循环,例如。

如果在摧毁指向x的指针之前,我们记得进去说,设置,你知道这个指针到这里为空,如果我们在这个循环中清除了一个指针,所以不再有循环,那么引用计数将正确工作,因为当这个指针从x中删除时。

这个对象的引用计数将变为零,然后,这个对象的引用计数也将变为零,在删除这个对象之后,好的,另一种可能性是回溯引用计数,通过其他垃圾收集技术来收集循环,因此在某些引用类型的系统中,例如。

大部分垃圾收集是通过引用计数完成的,但每隔一段时间,每隔很长时间,你可能会运行一个标记和清除收集器,来清理任何循环但不可达的数据结构。

我们现在准备结束关于自动内存管理的讨论,所以我只想在这里做出一些高级别的观点,首先,毫无疑问自动内存管理是一件好事,它防止了非常严重的存储错误,编程中最困难的错误之一,当你使用垃圾收集语言时。

你真的有一类事情不必担心,因此,它确实是一种更高效的方式进行编程,所以如果你的问题,如果你的程序非常适合自动内存管理,若使用提供那种支持的系统,你才疯狂,管理的缺点是减少程序员控制。

不再控制内存中数据的布局,不再控制内存何时释放,不再控制数据在内存中的位置,对程序使用的内存量控制也非常有限,那么,如果这两件事不重要,如果你的,如果你的应用不是极度数据密集型。

数据在内存中的精确布局和内存中保留的数据量很重要,垃圾回收可能效果很好,但有些应用程序,特别是高端数据处理,和科学应用,使用大量数据,需要高效使用内存,垃圾回收实际上变得太低效,无法做好工作。

那些领域的人,仍使用手动内存管理,实时应用中,暂停可能成问题,因此,如果程序需要保证,截止日期,许多与外界交互的嵌入式系统,如控制危险机械,和类似的东西,它们必须有响应时间,以确保不会发生可怕的事情。

你知道,引入可能暂停任意时间的自动内存管理系统,你知道,使确保那个非常困难的问题,因此,实时应用中并不总是使用垃圾收集,过去几年进步很大,实时垃圾收集器,自动内存管理的程序员,可能面临内存泄漏问题。

自动内存管理防止内存损坏,但无法防止保留过多数据,可能严重影响程序性能,垃圾收集语言可能出现内存泄漏,甚至可能很常见,我认为你知道,你不太清楚或不必太清楚,记忆是如何被使用的,更容易出现内存泄漏。

在Java程序中,你将有一些变量,比如x指向某个数据结构,这个数据结构很大,好的,假设这是编译器的抽象语法树,计算中可能出现一个点,不再需要抽象语法树,假设已转换为中间语言,从抽象语法树。

余下的编译处理将在中间语言上进行,表示和生成代码,不再回头查看抽象和文本,嗯,编译器,我的意思是,抱歉,这,垃圾收集器,不知道,你将来不会再使用抽象语法树,如果你有一个指向这个巨大数据结构的变量。

即使你不使用它,它也会停留,并且会占用内存,所以正确做法是当你到达程序的某个点,你将不再使用这个数据结构,将x赋空值,在那时将x赋空,实质上丢弃指向数据结构的指针,现在垃圾收集器,无论何种形式标记清除。

停止并复制引用计数,将看到这不再可达并收集这个大结构,这非常,在生产Java程序中很常见有这种内存泄漏,你只是有忘记的指针,不再使用的数据。

如前几课所述,垃圾回收很重要,每个程序员都应了解其优缺点,也是编程语言实现的一个有趣方面,有比这些讲座中讨论的更先进的垃圾回收算法,人们主要考虑改进垃圾回收的维度,使垃圾回收并发。

这意味着允许程序在收集时继续运行,程序运行时,另一常见情况是,实际上生产收集器中有一个称为代收集器的东西,基本思想是我们不想不断,检查那些非常长寿的对象,每次收集时都会发生很多收集。

有些对象会存活很长时间,程序中大部分时间存在的巨大数据结构,一旦我们在几次收集中看到它们,我们可以假设它们将在未来几次收集中继续存在,因此在一代集合中,抱歉,下一代收集器,旧物件。

存在一段时间的物件放入单独区域,收集频率较低,这使收集器能专注于最可能成为垃圾的物件,即最近分配的物件,我们已稍作讨论实时,因此有尝试限制长度或界限的收集器,最长暂停长度,对程序的最大中断。

最后是并行收集器,因此垃圾收集系统中,实际上有多个垃圾收集器同时运行,并协调他们的行动。

P9:p09 03-03-_Regula - 加加zero - BV1Mb42177J7

本视频将讨论,正规语言,用于指定编程语言的词法结构。

简述编程语言的词法结构,是一组标记类,每个标记类包含一些字符串,需要指定每个标记类包含哪些字符串,通常使用正规语言,本视频将介绍正规语言。

然后看示例,在真实编程语言中使用,定义正规语言,通常使用正则表达式,每个正则表达式表示一个集合,有两个基本正则表达式,若写单个字符c,是一个表达式,表示包含一个字符串的语言,即,单个字符c,好的。

一种基本形式,对于任何单个字符,得到一个包含一个字符串的语言,仅包含,该字符,另一个基本元素是正则表达式epsilon,包含,仅一个字符串,空字符串,重要的是记住,epsilon不是空语言,好的。

这不是对应空字符串和空字符串集,它是一个包含,一个字符串的语言,即空字符串,除了两个基本正则表达式。

还有三个复合正则表达式,按顺序逐一介绍,第一个是a加b,对应语言a和b的并集,这是集合a,使得a属于大a语言,小a属于大a并,小b使得b属于小b语言,即两个字符串集的并,连接类似字符串连接。

连接两个集合的字符串,构成新的集合,如果有两种语言a和b,则a和b的连接等于所有字符串,小a连接小b,其中a来自大a语言,b来自大b语言,这是一个笛卡尔积操作,从a中选择字符串,从大B中选择字符串。

然后合并,将a的字符串放在前面,以所有可能的方式选择这些字符串,形成所有可能的组合字符串,这就是语言a连接b,最后有一种循环结构,嗯,这读作a星,或称为清洁迭代或清洁闭包。

a星等于对于i大于等于0的并集,a的i次方a的i次方,次方,那是什么意思,嗯,年龄,第i,次方就是a连接自己,i次,所以这是a的i次方,注意因为i可以等于0,这里有一种可能是a的0次方。

所以a连接自己0次,那是什么,那是空语言epsilon,所以空字符串总是a星的元素,总结一下最后几页。

关于某个字母表sigma的正则表达式是最小的表达式集合,所以让我们定义它,所以正则表达式等于,嗯,epsilon总是正则表达式,另一种可能是单个字符c,其中c是字母表中的元素,好的,这一点很重要。

正则表达式是相对于某个字母表定义的,所以我们必须选择一个字符家族,这些字符将形成正则表达式的基本情况,这里你知道,对于字母表中的每个字符,我们都有一个基本正则表达式,然后我们有复合表达式。

所以另一种可能是正则表达式是两种正则表达式的并集,另一个是正则表达式是两种正则表达式的连接,最后一个可能是正则表达式的迭代,所以这五个情况是某个给定字母表上的所有正则表达式。

所以这五个情况是某个给定字母表上的所有正则表达式,这里描述正则表达式的语法,右边的不同情况,如果你以前没见过,这叫做语法,这对这堂课不重要,这不是这,嗯,这,这堂课是关于,但我们会讲到语法。

当我们讲到解析。

接下来举几个构建正规语言的例子,写出它们并思考含义,如我们所说,谈论正规语言时,首先要说字母表是什么,对于这些例子,我们就用0和1的字母表,这些将是包含0和1字符串的语言,让我们从一个简单的例子开始。

考虑这个语言,一颗星,那又怎样,嗯,描述得如此好的语言,我们知道星星的定义,如果记得那是联合,大于或等于0的1到i次方,好的,那等于什么?那就是,嗯,重复i次的1,这就是11连乘i的意思,好的。

意味着1与自己连乘,i次,所以这将是,嗯,空字符串,这就是1与自己连乘,0后1111连自己,3后1连自己,4后1连自己,任意次,好的,所有1串相等,现在好,做第2例,嗯,考虑语言,一加零,与语言连接。

一,好的,和,记得连接是如何工作的,是跨乘积的,所以我们将第一个表达式的每个字符串与第二个表达式的每个字符串组合,所以这将等于字符串a b,其中a来自一加零,b来自一,好吧,那可能是什么?

有两个选择给a,a可以是1或0,b可以是1,实际上这等于集合,嗯,一一和字符串一一,第二件事,字符串一一和一零,好吧,再做另一个例子,嗯,它稍微复杂一些,让我们逐步构建到有两个迭代的并集。

所以有零星加一心星,想想那等于什么?我们已经知道一心星等于什么,那等于所有一的字符串,因此,类比,零星必须是所有零的字符串,然后我们取这两个东西的并集,所以这实际上很容易写出来,让我们用这种符号写出来。

所以我们有零的i次方,对于i大于等于0,好的,所以它是零星并上一的i次方,对于i大于等于0,那就是所有一的字符串,所以这是这个表达式表示的集合,对于我们的最后一个例子,让我们想想,嗯,零加一,现在迭代。

好的,所以我们把星号放在两个单独字符的并集的周围,而不是在两个字符的单独星号上并集两个东西,所以,这个,这个表达式等于什么?让我们使用星号的定义,所以我们知道这是a的并集,对于i大于等于0的零。

加一i,那看起来像什么,首先看起来像,那里是空字符串,然后在这个语言中另一个字符串是,嗯,呃,是打扰一下,是从零加一开始,所以抱歉,我不应该说另一个字符串,但另一组字符串来自语言。

零加一再然后零加一再连接自身,好的,通常它将是这样,嗯,零加一再连接自身,乘以I次,好吧,那意味着什么,这意味着在每一个位置,如果我们有一个长度为I的字符串,在每一个位置,我们可以选择零或一插入。

这适用于任何长度的字符串,这将是所有长度的字符串都正确的,事实上,这个语言就只是所有字符串,由零和一组成,事实上这意味着它是集合,如果我们回头看字母表,字母表只包含零和一。

这是你可以用整个字母表形成的所有字符串的集合,它有一个特殊的名字,当这种情况发生时,当你有一个正则表达式,它表示你可以用字母表形成的所有字符串,我们将其写为sigma星,好的。

所以只是意味着字母表的所有字符串迭代任意多次,嗯,在继续之前我想在这里做的最后一件事,是实际上有很多种方式可以写每个不同的语言,没有唯一的写法来写这些,所以例如,让我们只拿这里的这个语言。

我们做的第二个,让我换个颜色,另一种写这个的方式,因为我们知道它的意思是这两个字符串,一一和一零,我本可以写成一一加一零,那将意味着完全相同的事情,这两个表达式表示完全相同的集合,类似地,一个星号。

我可以写成一个星号,加1,因为这不会改变任何东西,加入单个字符串,1不会改变任何东西,因为1已经包含在一个星号中,这可能是一种愚蠢的方式来写那个集合,但没关系,它有意义,它意味着完全相同的东西。

就像一个星号,再次强调的是,有不止一种方式写下相同的集合,嗯,呃,写作的权利,呃,呃,你可以写多个正则表达式来表示相同的集合,嗯,我们来到了这个视频的结尾和总结,我们研究了正则表达式。

它们用于定义正规语言,正则表达式是语法,那是我们写下的表达式,它表示一个字符串集,这是正规语言,这就是正则表达式的含义,在标准定义中有五种类型的正则表达式,有一个表示空字符串的表达式。

它由epsilon表示,然后我们有所有的一个字符字符串,然后有三种复合复合表达式,从其他正则表达式构建新正则表达式的三种方式,并集。

P90:p90 18-01-_Java - 加加zero - BV1Mb42177J7

在接下来的视频中,我们将应用课堂所学,分析Java的各种特性,这将让我们有机会看看真正的编程语言及其设计,嗯。

已经完成,也会讨论一些不酷的特性,课程中尚未涵盖的。

从这个角度看,Java是一种超级酷的语言,很酷,加上更多特性,核心中有更多特性,Java和酷非常相似,Java和酷都是类型化面向对象垃圾收集语言,它们都在20世纪90年代初设计。

因此它们共享当时的共同文化,所以在这段视频中我将简单介绍Java的历史,这将是这段相对较短视频的重点,然后在接下来的视频中我们将讨论,Java中不在酷中的所有特性,并使用我们一直在讨论的想法。

通过课程来解释这些想法,但我觉得这些都是重要的语言结构,不幸的是,嗯,因为太耗时或太复杂而无法添加到课程项目中,因此我认为使用像Java这样的语言来说明,嗯,这些想法是如何工作的,以及存在哪些问题。

所以Java最初是一个名为Oak的项目,在Sun Microsystems,最初目标是机顶盒设备,这是一个小盒子,将放在你的电视上,所以你有你的电视屏幕,然后上面会有这个小东西,将放在电视上。

它将控制你所有的有线电视节目,这基本上将连接到某种类型的网络,它将帮助你,你知道让电视更具互动性,所以这是在每台电视本身都不是电脑的时代,Oak的初步开发花了几年时间。

我相信项目从大约91年到94年运行,至少,据我了解,机顶盒市场从未真正起飞,所以这个从未真正流行起来,嗯,消费者从未接受过从不,因此,实际上存在有限的上升空间,或橡树在当时有有限的潜力,然后发生了某事。

互联网发生了,因此,在90年代初,互联网革命真正开始加速,每个人都开始上网,并在1993年左右变得明显,1994年,将需要真正解决互联网特定问题的编程语言,特别是人们非常关心安全性。

他们不想下载大量二进制文件,这些文件是由C语言编写的,并在互联网上传递,因为那些程序是否能按预期工作没有保证,也不会崩溃你的机器,因此,需要在互联网上共享代码,来自你不完全信任的其他人。

这意味着我们需要比C和C++更安全的语言,因此,那里有一个新的语言的机会,实际上有几个候选人,嗯,除了Java,Tickle和Python,嗯,都是非常认真的候选人,成为互联网编程语言,最终。

太阳微系统的支持,太阳对Java的支持,帮助它在互联网上真正获得非常强大的影响力,但你知道这个故事的重点是,每一种新语言都需要杀手级应用,每一种编程语言都骑在一些应用程序的背上进入世界,因此。

必须有一种新的应用程序,人们想要编写,现有的语言服务得不是很好,并提供机会,这使得人们学习新的编程语言变得值得,因此,Java是一种非常安全的语言,它有垃圾收集,它有类型系统,使其在当时。

非常适合互联网编程的兴起需求,它变得非常流行,我认为主要是因为这个原因,如果你嗯,如果你记得,早期有一场关于编程语言经济的讲座或视频,实际上,我会推荐,如果你还没有看过那个,那么回去看看,因为在其中。

我讨论了这些关于语言如何被采用的想法更详细。

因此,Java也在特定的技术环境中出现,这种情况很常见,新语言常大量借鉴前辈,新语言常为旧思想新设计,或许加入些创新,Java受特定影响,至少我理解如此,Java类型系统,或其对类型的承诺。

人们尝试建语言,现实方式扩展大系统,但也要强类型,Java面向对象源于类似Objective-C的语言,C和C++,以及Eiffel,它们也有接口的概念,这是Java的一个显著特征,最后。

Java相当动态,意味着许多事情不是静态完成的,而是动态完成的,如反射就是其中一个例子,实际上还有相当多的其他特性,那里有些历史,有些共享文化,这是一种或曾是函数式家族语言,但它也是,非常动态的语言。

如我开头所说,这个视频只是介绍和概览,在接下来的几个视频中,我们将研究Java的特定功能,以及它们的工作原理,这将包括异常,接口和线程等,还有诸多特性稍后讨论,要明白Java是一门大语言,并不简单。

Java语言手册数百页,功能众多,并且,更重要的是,设计语言难点在于,当然,确保所有功能交互正确,所有特征的组合,和,你知道。

P91:p91 18-02-_Java_Arrays - 加加zero - BV1Mb42177J7

本视频将探讨Java数组。

假设有两个类a和b,且b是a的子类,考虑执行以下代码会发生什么,首先将分配一个蜜蜂数组,这是一个用于存储蜜蜂的数组,有一个数组变量小b指向它,然后有一个变量数组a,也指向与b相同的数组,注意a的类型。

a是一个a类型的数组,b是一个b类型的数组,现在我们将执行以下操作,将一个新a对象赋值给a[0],这应该没问题,因为a是一个a类型的数组,看起来应该没问题,第一个位置将有一个a,然后访问b[0]。

因为a和b指向相同的数组,与a[0]相同,将调用未在a中声明的某个方法,记住b是a的子类,所以b具有a的所有方法,但b可能还有更多方法,由于这是一个b类型的数组,应该能够调用所有b方法。

但当我们调用在b中声明但在a中未声明的某个方法时,将出现运行时错误,因为数组中存储的对象实际上是一个a对象,要理解这个例子,需要查看Java的子类型规则,如果b继承自a,这是其中一种情况。

如果b直接继承自a,那么b是a的子类,就像其他面向对象语言一样。

我们从类型检查的讲座中非常熟悉这一点,类型子类型是传递的,如果c是b的子类,b是a的子类,那么c也是a的子类,这也是完全标准的,a和b是子类型,如果b继承自a,这是其中一种情况,如果b直接继承自a。

那么b是a的子类,就像其他面向对象语言一样,我们从类型检查的讲座中非常熟悉这一点,类型子类型是传递的,如果c是b的子类,b是a的子类,但还有另一个非标准的规则,或它绝对是非标准的。

那就是数组b是数组a的子类型,如果元素类型有子类型关系,所以如果b是a的类型,那么数组b是数组a的子类型,很酷,Cool没有这样的东西,Cool没有raise,所以它甚至没有机会有类似的东西。

但这也是不正确的做法,其他有对象和子类型的语言也不是这样做的。

让我们再次看看我们的小例子,让我以稍微不同的方式解释一下,所以问题是,我们有一块内存,实际上这里并不重要,这不一定是数组,重要的是它是内存的可更新部分,所以我们有指针指向它,我们有两个指针指向它。

A和b,它们都可以读写这部分内存,这可以只是一个单元格,它不需要是多个单元格的数组,但重要的是,有一个内存位置,这两个都指向它,它们都可以读写,好的,麻烦来了,嗯,顺便说一句,这有一个名字,叫做别名。

好的,所以当你有两个名字,两个程序名称指向同一部分内存,这叫做别名,在这里,你知道,我们有两个数组a和b指向同一块内存,好的,现在,别名在真实程序中很常见,它本身并不坏。

但在这个例子中的问题是a和b有不同的类型,好的,一般来说,如果有别名可更新的引用,好的,意味着你有两个名字指向同一个位置,这个位置既可读又可写,所以可以通过两个名字更新,如果这两个名字有不同的类型。

那么这将是不安全的,好的,你将不会有健全的类型系统,要看到问题,让我们说在这里这个例子中我们有什么,嗯,b类型是a的子类型,好的,那意味着什么,嗯,这意味着我们可以通过这个指针做右操作,好的。

并将a对象写入此位置,然后我们可以通过这个指针读取它作为p对象,但现在它没有a的所有方法和字段,将其视为b对象,我们可能会对其实施一些未定义的操作,你可以看到这没有帮助,如果我们交换a和b的角色。

所以你知道如果我们特别地,如果我们反转子类型关系,因此a是b的子类型,我们得到完全相同的问题,因为别名是对称的,然后我们会通过这个b指针做右操作,并从a指针读取,交换读写操作的角色。

我们拥有完全相同的问题,总的来说,多个不同类型的可更新位置的别名是不安全的,实际上这个问题已经在许多不同的编程语言中出现过,Java并不是唯一遇到这个问题的编程语言,它是类型系统的一个相当微妙的方面。

在许多语言中已经做了类似Java的事情,他们实际上为静态类型系统创造了一个问题,嗯。

通过希望数组中的子类型工作,现在标准的解决方案或是在,我应该说在许多语言中,并且在编程语言研究社区中可能最广泛接受的,是您需要为数组设置不同的子类型规则,所以我们会说你知道通常使用的规则。

解决这个问题的标准解决方案是在类型级别上做以下事情,所以嗯,您只允许对数组进行子类型化,所以你知道b数组是a数组的子类型,仅当b和a是相同类型时,如果b等于a,如果你考虑一下,如果我们有一个数组。

现在我们有指向它的两个指针,a和b,我们知道a的类型,b类型的子类型,那仅发生在元素类型相等时,因此我们不能创建两个指向可更新位置的不同类型的引用,这将确保类型系统的正确性。

因此Java以不同方式修复问题,因此,Java不是静态检查数组访问是否都有类型,正确的Java在运行时这样做,因此,每当数组中执行赋值操作时,Java检查被赋值的对象的类型是否与数组的类型兼容。

所以当你在Java中说new b sub ten时,Java,记得在数组内部,这应该是一个b的数组,然后每当您向数组中赋值时,它将检查您要赋值的项是否是,B或B的子类型,显然,这会在数组计算上增加开销。

因此,对数组的每个赋值都将有一个,有一个类型,检查在运行时,幸运的是,虽然,嗯,最常见的数组是原始类型的数组,特别是int数组和浮点型数组,这些不受影响,因为原始类型不是类,它们没有子类型,因此。

你永远无法创建数组,例如,带有任何可能导致此问题的子类型关系的浮点数字符数组,所以对于原始类型,我们得救了或处于更好的状态,它们不需要这些额外检查,但如果你有对象数组,那么在Java中。

我们会向这些数组中赋值。

因此,存在额外的运行时开销。

P92:p92 18-03-_Java_Exceptions - 加加zero - BV1Mb42177J7

本视频中,将讨论程序员定义的。

异常,考虑以下典型编程场景,深入复杂代码部分,遇到可能出现意外错误的地方,可能发生违反程序重要属性的事,例如,可能发现内存不足的地方,或数据结构不满足要求,某些不变量,所以一个应排序的列表,并非如此。

或诸如此类,问题是如何处理这些错误,如何编写代码以优雅地处理错误,而不是使程序非常丑陋。

许多语言中解决此问题的流行方案,包括Java,是在语言中添加一种新的值类型,称为异常,我们将有控制结构处理异常,这是最流行的两个,它们在Java中的样子,我们可以抛出异常,这将在此时创建异常。

抛出发生的地方,异常将简单地传播出程序,它将,它将基本上在那时停止程序执行,程序将在那时停止,包含结构也会抛出异常,因此异常将,你知道,简单地向上传播到当前执行的代码,直到遇到try catch。

这如何工作良好,我们可以尝试,我们可以执行此表达式,这将是某个表达式,如果此表达式抛出异常,若表达式内抛出异常,则捕获该异常,可在此绑定,指定异常值名,类似于let,捕获此处的异常,命名为x。

然后执行清理代码,处理异常,以某种方式,此设计处理异常的基本思想,嗯,异常发生在你遇到的地方,实际检测异常的地方,可能在代码深处,不是一个处理异常的好地方,你想做的是退出那部分代码,回到更高层次的点。

可以清理并处理异常,然后可能重试较大块的代码。

这是一个使用异常的小例子,呃,异常,所以这里我们有main方法,我们要做什么,我们有一个try块,只调用函数x,如果它抛出异常,那么我们将捕获异常,在这种情况下我们不做任何事情,我们只打印出一条消息。

说出现错误,并终止程序,我们并没有做任何聪明的事情,但我们确实捕获了异常,至少打印出错误,而不是仅仅终止,那么x做什么呢,X简单地抛出一个异常,这个函数x分配了一个异常对象,这只是一个值。

它就像其他类一样,但它有一个特殊属性,它可以被抛出,所以当我们抛出它时,x异常终止,我们最终回到main方法中的try catch表达式中的catch块。

在最后几张幻灯片中,我对异常的工作方式给出了一个非正式描述,可能不是很清楚,事实上,我认为很难给出一个非常清晰的描述,如果没有某种形式的正式描述来说明异常应该如何行为,但幸运的是。

我们在本课程中已经学习了操作语义,所以现在你熟悉了那种语言行为描述,我可以非常简洁地描述try catch实际上是如何真正工作的,好吧,所以我们将为try catch表达式提供操作规则。

并且只是指出这里,有一些字体问题,所以我不得不手工在幻灯片中写下转义字符,所以这些手写的字符都应该是转义字符,现在更重要的事情,异常有一些区别,好吧,异常对象有两种状态,它可能只是一个普通值。

当我说Java中的新异常对象,当我说你知道的新东西,那是异常类的实例,它只是一个普通值,在那个点上它就像任何其他对象一样行为,但当对象被抛出时,有一个区别,所以当异常实际上被抛出时。

它变成了一种特殊类型的值,它被以不同的方式对待,好吧,我们将区分普通对象,V 好,和被抛出的对象,好吧,这些是活动异常,好吧,所以让我们看看异常构造的操作规则,这是try catch块的一个规则。

如果这个表达式评估为一个普通值,如果没有抛出异常,那么try catch块的结果就是那个值,try catch块的工作方式是你评估try块中的表达式,如果它以正常方式终止并带有值。

那么整个表达式的结果就是那个值,好吧现在,另一种可能是你将评估一个try catch块,当你尝试评估try块中的表达式时,你,它将抛出异常,所以它可能会导致抛出异常,所以这是这种情况,好吧,那个抱歉。

是这种情况,其中e1评估为这些特殊值之一,被标记为已抛出的异常,我们该怎么办,嗯,我们解开异常,我们取出被抛出异常中的值,好吧,我们将其绑定到一些局部名称,好吧,这是在catch表达式中命名的。

然后我们评估清理代码,所以有了异常值可用,我们评估e2,无论e2的结果是什么,那就是try catch块的结果。

throw是如何工作的,呃,它非常简单,所以throw只接受一个表达式,它评估表达式以获取值b,然后将其标记为抛出的值,表示为抛出异常,所以它用t这个东西包裹该值,这表示该异常现在已被抛出。

我们只需要谈论语言的其他部分,语言中所有其他结构如何处理这些抛出异常,这非常简单,我们希望这些抛出异常简单地传播到其他任何类型的表达式之外,例如,我们只做一个例子,因为每种语言结构的想法相同。

假设我们评估e一加e二,所以首先我们需要做的是,当然,是评估e一,如果这恰好引发异常,所以出了什么问题,在评估e一时,如果e一评估为抛出异常,那么我们就停止加号的评估,我们甚至不评估e二,注意e。

以上未提及2,1是评估项之一,因此如果e,1异常终止,抛出异常,整个编辑的结果是该异常,其他结构类似,如果if子表达式之一结果为异常,实际上,如果,如果一旦子表达式之一结果为异常。

它们停止评估并传播该异常,唯一阻止异常传播的方法,是在try catch块中捕获。

实现异常的方式有很多,这是一个简单的实现方法,当我们遇到try表达式时,将在栈中标记当前位置,将在栈中标记位置,遇到try的位置,例如,这里是我们的栈,假设栈是这样运行的,遇到try表达式。

在栈中放置标记,表示那里遇到了try,然后继续,你知道,评估try内的内容,可能会向栈中添加更多内容,现在,抛出异常时,如果在这里,突然抛出时我们在这一点,我在执行中,会发生什么,我们将展开堆栈。

我们将所有东西弹出堆栈,我们将弹出所有这些东西,所以都回到第一次尝试,然后执行相应的catch,所以这里我们标记了,你知道代码中的位置,有一个try,我们可以用它找到表达式。

代码中有相应catch块的片段,并将堆栈展开到这一点,然后从catch开始评估,所以这种设计有一个缺点,try实际上是有成本的,即使你不抛出x,即使你不抛出异常。

执行try catch块仍然要付出一些代价,你至少要标记堆栈并记住取消标记,当然,当你弹出堆栈上的东西时,当你离开try块时,所以更复杂的技术试图减少try和throw的成本,并在它们之间进行权衡。

通常你想做的是,因为异常可能在,在大多数程序中相对罕见,使try的成本尽可能低,可能以使throw稍微更昂贵为代价。

现在有一个关于java的小问题,未被捕获的异常在对象终结时会发生什么,如果你不知道对象终结是什么,那么,当一个对象被收集时,当一个对象被垃圾收集时,有可能在该对象上运行一个方法来清理它。

在垃圾收集器实际分配它之前,这被称为终结方法,所以对象可以在java中有终结方法,这些方法本质上是由垃圾收集器调用的,所以当垃圾收集器发现某个对象是垃圾时,它将清理它,它将首先调用终结方法。

你为什么要这样做呢?比如说,我们有一个对象,它可能有一个文件句柄,它可能有一个指向打开文件的指针之类的东西,现在当这个对象变得不可达时,它将由垃圾收集器收集,但如果你不关闭文件,那将会导致问题。

程序中有许多打开的文件,使用它们可能会导致后续问题,特别是当你用完文件句柄时,操作系统通常提供固定数量的文件句柄,所以正确做法是,在垃圾回收时首先关闭文件,并实质上删除这个指针,好的,然后释放对象。

这就是对象终结的作用,所以再次,可在Java中定义方法,垃圾回收器运行前清理资源,若终结方法抛出异常,谁捕获,因垃圾回收器调用,调用时间不可预测,异常处理位置不明确,答案为,无人处理该方法或异常被丢弃。

对象终结期间未处理的异常,仅在方法内处理,否则丢弃。

Java的一个创新是异常是方法接口的一部分,编译器会检查它们,所以在我讲座开始举的例子中有一个方法,X可能抛出异常,我的异常,注意X的声明实际上声明了X可以抛出该异常,它是X接口的一部分。

X的检查接口的一部分,它可以抛出一个特定异常,为什么你想在编译时检查这个,原Java项目中实际观察到的,Java程序可能抛出许多异常,人们容易忽略可能抛出的异常,他们不知道要处理哪些异常,实际上。

当他们将这个添加到语言时,编译器将强制执行,现在,一个方法声明了它可以抛出的所有异常,他们在编译器中发现了许多地方,抛出异常但未正确处理,这导致了更好的错误处理,嗯,Java编译器本身。

人们普遍认为这是个好主意,因为它帮助程序员编写更健壮的代码,因为他们能看到必须处理的异常,现在有一些例外,特别是,有,有一些运行时错误,不必成为方法签名的部分。

因为静态检查方法是否永远不会引发它们非常困难,如解引用等,空指针或整数溢出不必处理和声明在接口,但大体上,任何方法可能抛出的异常必须在Java接口中声明。

然后还有其他关于Java中异常设计的平凡类型规则,例如,Throw必须应用于异常类型的对象,不能应用于任意类型的对象,但总的来说Java中的异常处理得很好,特别是这个想法。

关于声明方法可能抛出的异常类型的想法。

是Java中的新想法。

P93:p93 18-04-_Java_Interfaces - 加加zero - BV1Mb42177J7

在这段视频中,我们将探讨Java中的接口。

接口定义了类之间的关系,不使用继承,这是一个例子,我们有一个名为Point的接口,Point接口可以包含许多方法,我们只声明这些方法的签名,你也可以有其他东西,不仅仅是方法,但通常它们主要用于方法接口。

这是一个特定方法的示例,移动方法,并接受参数,有特定返回类型,任何其他类或类,抱歉,将实现点接口的类必须提供相同签名的方法,因此这样,因为点接口有移动方法,点类将必须有移动方法。

与声明的接口中的移动方法相同签名,如果点接口有其他方法,那么点类也需实现那些方法,你知道,有同名方法,带适当参数和结果类型。

现在,Java语言手册说Java程序可使用接口,使相关类无需共享抽象超类,或为对象添加方法,翻译是接口在C++中起多重继承作用,因此接口确实类似于多重继承,原因是类可实现多个接口,若我有类x。

实现3接口,A、B和C,这意味着x对象可视为A对象,B对象,或C对象(适当上下文),所以就像,或几乎如x有3个父类,A、B和C,现在有一些重要区别,但这是效果,若要类具备功能,或实现多个接口功能。

Java中可直接实现,声明类实现所有接口,这是应用示例,考虑斯坦福等大学研究生,研究生通常是学生,对,他们上课,具学生属性,获学位和成绩等,研究生通常也为大学工作,他们常是课堂助教或研究助理。

因此我还有其他角色,即大学雇员,如果我花了很多精力在我的大学人事管理软件中,实现处理学生的功能,实现处理员工的功能,那么我想利用这一点,当我开始考虑如何实现研究生的功能时,有一种方法。

如果我有一个实现了,如果我有一个抱歉,员工接口和学生接口的类,那么可以说研究生既是员工也是学生,因此研究生可以实现员工接口和学生接口,这样做的原因是实际上很难做到这一点,如果你只有单继承。

如果你考虑一下,如果我设置了一些员工类和一些学生类,现在我想让研究生成为其中之一,我该怎么办,如果我有一个员工类,我可以让研究生成为它的子类,但现在如何获得学生功能,同样,如果我有一个学生类。

我可以让研究生成为它的子类,但现在如何获得员工功能,所以在单继承中你被迫选择一个类来继承,接口的优势是它们将允许你获得或实现功能,或表达关系,至少是功能到多种不同的事物,因此我可以有一个。

一个研究生类实现了员工和学生功能,所以接口与继承有何不同。

可能,最大的区别是实现接口不如继承高效,这就是为什么你两者都有,因此你倾向于使用继承,如果可以,因为它将比接口更高效,接口效率低的原因是什么,主要原因是实现接口的类不必处于固定偏移量,实际上,我们通常。

不能,通常,将接口中的方法分配到类实现或对象实现的固定偏移量中,所以让我们看一个例子,这是我们的点接口再次,现在让我们说我们有一个类点,我们之前看到的那个实现了点接口,并实现move方法。

必须实现move方法,然后我们有另一个类也实现Point接口,但还实现其他功能,好的,所以它可能实现不属于该接口的其他方法,那么现在如何决定放置move方法的位置呢,我们讨论过的自然实现方式。

比如cool,C是方法按声明顺序排列,因此如果我们明确这样做,移动方法将不在这些类的第一个位置,现在我们可以想象一个单独的编译器过程,以便说点接口的所有方法,在实现点接口的任何类中。

但只要我们实现多个接口就不起作用了,所以让我们说点二类这里实现了另一个接口a好吧,我们已经决定对于点接口,移动方法应该首先,它应该是类的第一个方法,若对A界面做类似决定,你知道该接口中应首列的方法。

那么将产生冲突,通常无完全排序,可给所有方法和接口,使它们可在实现这些接口的所有类中维护,至少无无需预知所有声明的类,和所有声明的接口的完全排序,这在Java中有点,因为我们不想提前知道。

强制声明所有类和接口,未来不可扩展,好吧,接口中的方法不在类中固定偏移。

那么,如何实现接口,实现分发将比平常复杂,对方法f说,e具有某种接口类型,所以如果e i作为它本身,你知道某些类型,如果他被归类为具有某些接口,现在我们在调用该接口的f方法。

然后我们将不得不做更多的工作,这是一种方法,这种方法实际上相当低效,但你可以看到它会起作用,还有,还有其他更高效的方法,但在这里,这里有一种方法可行,所以每个实现接口的类,都会有一个关联的查找表。

将方法名,字符串方法名映射到这些方法本身,然后我们可以对方法名进行哈希以加快查找,实际上我们可以在编译时计算这些哈希,所以想法是,当我们有一个对象时,可能在对象内部,可能在分发指针处,分发指针。

你知道将指向一系列方法,类中正常的方法,但可能在分发表的末尾。

将会有另一个指针指向某种查找表,它将名称,映射到两个方法到代码,好的,所以与每个类的每个对象相关联,我们将有这个查找表,它将映射接口方法名称。

P94:p94 18-05-_Java_Coercions - 加加zero - BV1Mb42177J7

本视频将讨论,类型系统中的强制转换,许多语言中都有此特性,我们将专门研究Java中的强制转换。

Java允许在某些上下文中强制转换原始类型,意味着从一种类型转换为另一种类型,例如,考虑表达式1加2。0,此表达式的困难在于,虽然这里的1是整数,而2。0是浮点数,无法直接将整数与浮点数相加。

我们需要将整数转换为浮点数,然后以浮点数进行相加,或将浮点数转换为整数,然后以整数相加,在执行操作之前必须将它们转换为通用表示,通常的做法,也是Java所做的,是将整数转换为浮点数,1。0。

我认为正确看待强制转换的方式,它们是编译器为您插入的原始函数,就像您遗漏了一个函数调用,编译器注意到这一点并将其插入,在这种特定情况下,函数调用会是什么?我们可以认为有一个原始函数将整数转换为浮点数。

以明显的方式,因此,此表达式实际上被转换为表达式intToFloat,应用于数字1加2。0,好的,强制转换可能最好被视为,程序员的一种便利,让您避免编写某些函数调用,当类型转换显而易见时。

编译器可以插入执行该类型转换的函数,大多数语言实际上都有广泛的强制转换,这些转换非常,非常常见,特别是在数字类型之间,这不仅仅是Java,这实际上是许多不同风格的编程语言中的许多不同类型的强制转换。

Java特别区分两种类型的强制转换和强制类型转换,您有扩展强制转换,这些将始终成功,这意味着我会将它们放入,编译器或运行时系统不会抱怨它们,我们已经看到其中一个。

从int到float的转换是一个扩展强制转换的例子,窄化转换可能失败,特别是float到int,这可以正常工作,类似2。0可以明显转换为2,但如果你转换,没有整数表示的东西,比如2。5。

你知道这里有个问题,好的,对于这样的窄化转换,是否应该继续,是否应该截断或向上取整,那么Java会报错并阻止你,好的,嗯,你知道,窄化转换的一个更好例子,Java会抱怨的是向下转型,所以如果我有一个。

嗯,呃,两个类A和B,B是A的子类,然后我有一个A类型的东西,嗯,我可以把它转换为B,可以说,假设我有x,它是A类型的,好的,然后我可以有一个表达式,尝试将x转换为B对象,这里有一个转换。

我表明我想将x表达式视为B对象,这会类型检查,好的,编译器会让它通过,因为B是A的子类,但在运行时它会实际检查,x是否实际上是一个B对象,如果不是,你会得到一个异常,所以这可能在运行时失败。

如果x在转换点实际持有的对象不是B对象,Java的规则是窄化转换必须明确,你必须实际放入函数,你必须在代码中放入类型转换。

这样就很明显你确实想这样做,但宽化转换和强制转换可以是隐式的,如果你在宽化,如果你在提升到超类,整数类型之间,当明确一种类型嵌入另一种时,嗯,编译器会为你填充这些,现在有一个关于Java的小问题。

所以结果表明Java中有一个类型,对于该类型没有定义强制转换或类型转换,好的,所以没有隐式转换,甚至没有从该类型到其他任何类型的显式转换,问题的答案是,哪个是唯一的答案是bool,好的。

因此只有布尔类型没有强制转换或类型转换到其他类型。

我个人并不喜欢强制转换,我认为它显然是为程序员提供的便利,它显然是被广泛接受为编程语言中必要的,因为嗯类型转换,隐式类型转换和转换非常普遍,但我确实认为它倾向于导致程序的行为,与程序员可能预期的不同。

这是一个很好的例子来自语言po one,我们都为之站立的,编程语言,由IBM在20世纪60年代设计并具有许多功能,我们在课堂上已经谈论过几次pone,并且pone有一个非常广泛的类型转换和强制转换。

这可能会导致一些令人惊讶的行为,所以这是一个例子,我们有a、b和c是三个字符的字符串,所以重要的是要知道长度三是类型的一部分,所以b是一个123的字符串,所以您使用字符串4,五六,然后a将是b加c。

问题是a是什么,你可能不会猜到,让我向您展示我认为是正确的答案,所以首先的问题是这里的这个加操作会发生什么,嗯,所以这将被解释为整数加,所以b和c都将被强制转换为整数,并且这将作为整数,算术。

所以b将被转换为数字1,23 c将被转换为数字456,好的,然后我们会将它们相加并得到数字579,好的,所以此表达式的结果是579,但a也是一个三个字符的字符串,这必须转回字符串,结果这个转换分两步。

首先,这个数字转成默认长度的字符串,好的,默认长度恰好是六,所以这转成字符串,看起来像这样,3个空格后是579,然后这6个字符的字符串转成3个字符的字符串,我们只取前3个字符,所以得到这个,因此。

程序存储3个空格的字符串在,这可能不是预期的。

P95:p95 18-06-_Java_Threads - 加加zero - BV1Mb42177J7

本视频将讨论,编程语言中的并发,特别是,Java中的线程使用。

Java通过线程内置并发,本视频不解释线程的基础,假设有一定背景,这里简单说下线程,线程像自己的程序,有自己的程序计数器,意味着执行一条指令,有自己的局部变量和激活记录,Java程序。

或任何有线程的语言程序,同时可能有多个线程,抽象地,可认为线程是执行语句,每个线程有自己的局部变量,但可引用共享数据,可引用相同堆数据结构,每个线程执行特定指令,假设线程都在这,有三个线程,一二三。

它们在程序中的某指令,然后有个调度器,每次执行,调度器选一个线程执行,执行一条语句,这是概念上的,不是通常实现方式,然后重复循环,选一个线程,执行该线程一条语句,不断重复,比如,调度可能选线程一。

执行第一条语句,然后选线程二,执行这条语句,然后选线程三,执行该语句,可能决定再执行线程二,然后执行线程一多条语句,然后可能回到线程三,线程二可能再执行一段时间,等等,线程按顺序执行,每次执行时。

哪个线程执行不确定,它将执行多少指令,线程可能交错指令,线程可能交错,实际上,完全随机的顺序,好吧,回到如何在Java线程中实现,Java中的所有对象都有线程类,因此,你必须继承的特殊类,以成为线程。

当你从线程类继承时,你将拥有开始和停止方法,以开始和结束线程,好吧,线程有一些特殊属性,特别是线程可以同步对象,因此,线程可以通过同步构造获取对象锁,所以在Java中。

如果我说synchronized x e。

这意味着程序将在执行a之前获取x的锁,所以这里的程序将是锁定x,然后评估e,然后解锁x,好的,这是一个结构化的同步构造,执行表达式e时,将锁定x,这是主要方式,Java中几乎唯一同步多线程的方式。

这样可控制交错执行,一个线程执行这段代码时,其他线程不能执行这段代码,也不能锁定同一对象x,现在,两个线程能否执行相同语法结构?若局部变量x指向不同对象,但互不干扰,若尝试锁定同一对象x,不会交错。

Java中有种简写,比这种同步结构常用,同步可应用于方法,可以说,嗯,同步,F(这是方法定义),好的,这意味着当这个方法被调用时,这个对象将被锁定,所以这里将被锁定的对象是隐式的。

当synchronize附加到方法名或方法声明时,那总是意味着这个参数将是同步或锁定的对象,让我们看一个简单的例子,并思考如果我们有两个方法,其中一个调用类Simple的方法two。

另一个调用类Simple的方法fro,所以让我们看看那个,让我们来看看,假设有一线程一和线程二,现在线程一会调用方法二,线程二会调用方法fro,所以有一种可能性,假设方法二运行完成。

在fro执行任何东西之前,那么a等于三,b等于四,好的,然后fro会运行,它会打印出,呃,字符串a等于三,b等于四,好的,所以这是一个相对简单直接的情况,另一种可能是线程二在线程一执行任何事情之前运行。

线程二执行完所有指令,在线程一执行任何东西之前,在这种情况下会打印什么,fro会打印出a等于一,b等于二,然后线程二会运行,在fro执行后设置,所以fro执行完成后,它会将a设为三,b设为四。

所以这是另一种可能性,这两种情况都可以,但还有其他一些奇怪的可能性,让我们看看其中一个,如果线程实际上以非平凡的方式交错,所以让我们考虑以下可能性,假设线程二执行了赋值,a等于三,现在fro执行,呃。

打印的第一部分,所以它读取a并开始构建输出字符串,好的,所以它会打印出这里,呃,a等于三,好的,然后假设fro实际上继续运行,并且它也继续打印出,呃,这个的其余部分,好的。

所以它实际上进行了第二次读取b,所以它会打印b等于二,好的,然后有人将跑完其余的路,抱歉,B等于4,因此我们得到了一个看起来不太正确的输出,我们得到了,我们能够看到一个中间状态,线程一仅部分执行。

因此这里输出的在fro show,你知道,只是变量a和b的部分更新,所以一个已被写入,但另一个没有,如果我们不想这样做,如果我们认为这是错误的,我们将不得不使用同步来控制它,所以让我们看一下。

然后使用同步尝试防止这种情况发生。

并且我要提前告诉你,这段代码,或这次尝试是错误的,它实际上并没有解决这个问题,但它也说明了Java程序员最常见的线程编程错误,许多人包括专业程序员都会犯这个错误,许多生产Java程序都有这个特定错误。

所以这是一个非常有教育意义的例子,我认为,所以让我们看一下这里,让我们看看当我们有两个线程时,嗯,将要调用二的线程,将要调用fro的线程,并且让我们说在我们的堆中只有一个对象simple,嗯。

我们只称它为s,所以这是全局在整个堆中,只有一个对象,作为,嗯,简单类的所有对象,所以什么,让我们说线程一将首先执行,并且它首先要做的是因为它是一个同步方法,它将锁定调用参数,因为只有一个简单。

简单类的唯一对象必须是对象s,所以它将锁定s,这将阻止任何其他线程在,嗯,线程一持有该锁时获取s的锁,所以然后线程一可以继续执行语句a等于3,现在尽管我们可以中断,线程二可以运行并注意到这里。

线程二没有检查锁,它,它继续执行这里的代码并在fro方法中,但这不是同步的,那里没有同步关键字,因此,仅仅因为其他人持有简单对象的锁,并不阻止其他方法访问该对象的字段或数据,如果另一个方法本身。

不检查锁,因此,如果另一个方法未同步,它将继续执行,忽略另一个线程持有对象锁的事实,因此,在这种情况下,这可以只是运行完成,我们将打印出a等于三,B等于二,好的,因此,我们只看到两个更新中的一个。

然后调度器可以回来,嗯,让另一个线程运行,它将运行完成并解锁对象,你可以看到,这种特定的修复尝试没有取得任何成果,实际上,在没有同步的情况下,两个方法的所有可能交错仍然存在。

如果只有两个方法中的一个被同步,这个错误常见的原因是人们经常认为,你知道,读取是好的,我可以总是并行读取东西,这不会引起任何问题,因为我没有更改任何数据,是我的写入需要被同步,所以。

如果我要写入对象的字段,嗯,那需要与其他方法协调,因为写入是危险的,但读取似乎不会干扰,这里的观点是,如果只有一个方法,或只有对共享数据的两个访问中的一个被同步,没有帮助,因为同步只有。

如果每个人都检查锁,所以,读者和写者都需要检查锁,以限制这两个方法可能交错集的,所以,正确的方法是什么?只是将同步关键字放在两个方法上,现在不可能有我们之前看到的交错,所以现在只有两种可能的结果。

一个和只有两个可能打印的字符串,一个是a等于一和b等于二,在这种情况下,fro方法在two方法之前执行,所以是fro在two之前,好的,就这样,我的意思是,在所有方法之前,所有行,另一种可能是a等于三。

B等于四,好吧,然后在fro方法之前执行两种方法的全部,当这里的方法都同步时,这些将成为仅有的两种可能的交错。

我将结束这个视频,通过对Java线程做出一些其他评论,因此,我们希望的一个属性是,即使没有同步,变量应该只持有实际上由某个线程写入的值,我指的是什么,假设我们有两个赋值,这是在线程一中。

我们将a赋值为3。14,然后在线程二中,我们将a赋值为2。78,因此,在这些赋值完成后,在它们以某种顺序执行之后,我们期望什么,我们期望a最终等于3。14或2。78,好吧现在。

我们不希望a最终成为其他值,好吧,我的意思是,如果a最终成为3。78,例如,好吧,这会很糟糕,我们不想这样,因为这个值3。78从未由任何线程写入,好吧,这个值是某种方式制造的,我选择3。

78来暗示可能出错的情况,如果我们最终得到了线程一和线程二的位混合,或者数字的片段从线程一和线程二,它们以某种奇怪的方式重新组合,那么我们就可以创建一个从未分配给a的值,好吧。

它从未实际上在任何一个线程中写入,现在,Java确实保证正确性,所以值是原子的,意味着,如果我写入一个值,如果我给一个原始类型赋值,这将原子地发生,并且不会被对同一内存位置的另一个赋值干扰。

除了浮点双精度,所以这并不适用于双精度,它们不一定是原子的,为什么会这样呢?因为双精度,它是一个浮点数字,但它消耗了双倍的内存,这就是为什么它被称为双精度,它消耗两个字,好的,这意味着如果a是双精度。

假设a是双精度,这意味着3。14的右边实际上翻译成两条机器指令,我们需要写a的高位,等于某物,然后a的低位,因此,写代表a的两个字需要两条机器指令,你需要写高位和低位,好的,大多数机器上没有原生双字。

线程二也会发生同样情况,这将被拆分为对半部分的两次赋值,根据之前讨论,这些可能以某种方式交错,你可能会遇到不幸的情况,线程一写了表示a的高半部分,线程二写了表示a的低半部分。

然后你可能得到一个这样的数字,你知道,不是正好3。78,而是来自线程一和二的位混合,线程二的权利,你将创建所谓的凭空值。

显然凭空值是坏的,好的,你不想要那些,并且Java再次保证,几乎所有原始数据类型的权利将是原子的,所以你不会得到凭空值,但出于性能原因,对于双精度数不是这样,好的,一般般,呃,作为,手册上说。

这是对当前硬件的让步,他们不需要双精度权利的原子性,除非你,作为程序员,去标记类型为volatile,所以你必须声明双精度为volatile,如果你那样做,那么它们将保证原子权利,好的。

若写Java程序用Java线程,编程线程读写double并发,需小心声明double变量为volatile,至少目前,未来可能改变,我确信他们想改变,但目前需声明double为volatile。

确保读写原子性,嗯,更广义上,实际上有点独立,这实际上是一个独立观点,Java并发语义实际上很难理解细节,这个,嗯,关于凭空出现值的议题是一个方面,还有其他几个方面,这并不是Java的错。

并发语义实际上很难,实际上,这处于研究前沿,我们并不完全了解我们想要什么,或如何在并发环境中指定语言行为的正确方法,这并不是说我们什么都不懂,我们确实有一些语言具有很好的并发语义。

但在像Java这样功能丰富且完整的语言中,有一些东西在特定机器上如何实现并不完全清楚,在这个问题上已经做了大量的工作,特别是针对Java,Java实际上是最早拥有第一类线程的主流语言。

并试图将其与其他语言特性集成,所有其他现代语言特性我们都喜欢,所以并不奇怪,实际上我们遇到了一些麻烦,理解它们应该在所有情况下如何工作,所以这是Java的一个领域,我认为仍在争论中,而对于他们,嗯。

如果你用线程做相对直接的事情,一切都会很好,如果你在做,语言中有些区域,如果你尝试用线程使用它们,你可能会遇到一点麻烦,所以真正值得尝试理解Java并发和线程,如果你在编写重要的并发Java程序。

P96:p96 18-07-_Other_Topics - 加加zero - BV1Mb42177J7

本视频中,我们将结束关于Java的讨论,通过查看几个附加主题及其如何融入语言设计。

与Java的动态性一致,嗯,Java允许运行时加载类,但这意味着你可以实际向正在执行的Java程序添加功能,通过运行时加载新类,这可能会导致类型,安全性和安全性问题,现在编译时和加载时有区别。

源代码类型检查在编译时进行,这是我们之前视频中讨论的类型检查,但加载器在真正加载类时,加载的是字节码,不是源代码,不会被再次类型检查,这些字节码可能来自不可信的源。

这些字节码可能不是类型检查编译器的输出,在生成字节码之前,字节码可能不满足任务实现的类型假设,所以我们必须再次检查字节码,当类被加载时,一个称为字节码验证的过程就会发生。

字节码验证实际上是对字节码的类型检查,这就是它所做的,该过程略有不同,因为我们没有,这里的代码级别要低得多,因此算法看起来有点不同,但他们真正做的是类型检查,检查字节码,现在加载策略由类加载器处理。

类加载器是Java中的一个特殊类,它决定哪些类可以加载并实际在Java早期,发现了一堆安全问题,攻击者可以控制类加载器,安装自己的类加载器,这会比Java标准类加载器更宽松并破坏系统。

但这些问题很久以前就修复了,Java的另一个有趣之处是类也可能被卸载,所以你知道你不仅可以加载类,你也可以卸载类,上次我检查时,这在定义中并未明确说明,所以当你卸载类时含义有些模糊。

以及所有现有对象发生了什么,例如那个类的对象,现在我想花几分钟谈谈Java的初始化,这相当复杂,这并不奇怪,因为如果你记得Cool的初始化也很复杂,Java仅是酷的超集,所以它具有酷的所有初始化问题。

更多,现在主要复杂性来源是并发,但其他语言特性也增加了Java初始化的复杂性,事实上,嗯,如果你想理解一门新的面向对象语言,可以,研究它是如何进行对象初始化和类初始化的,因为在初始化中,本质上。

语言的所有特性都将相互作用,你必须解释所有这些交互,并解决它们,以便有一个明确的初始化过程,好吧,所以现在让我们谈谈类初始化,我们不会谈论对象初始化,我们只谈论初始化类,这就是。

代表类的对象实际上是如何在类首次引入程序时初始化的,所以首先要知道的是,类在,当类中的符号首次使用时初始化,好的,不是当类加载时,好吧,所以如果你在类中引用了任何符号,在第一次发生时。

这将导致类被初始化,这样做的原因是,如果你在类初始化中有错误,这将导致错误在可预测的地方发生,所以如果你有一个错误,你运行程序五次,你知道这个错误可能会在每次发生时都在同一个地方。

所以它是可重复和可预测的错误发生的地方,如果我们不是在加载类时发生错误,当类可能在许多不同的时间加载时,所以这里的错误,类初始化的错误将成为非确定性的,如果我们不,如果我们不在你知道。

执行中的确定性点延迟初始化。

所以现在我将讨论Java中初始化类对象的过程,首先我应该强调,类对象这个概念是Java有的,而酷没有我提到过这一点,但为了完全清楚,什么是类对象,类对象听起来就像它是什么,它是类的对象,它代表一个类。

好的,这不是类的实例,这是一个对象,它是类,好的,这不是类的实例,这是一个对象,它是类,这是一个表示类的对象,它包含有关类的所有信息,所以你知道它告诉你类的类型,类的字段和其他一切,这用于内省或反射。

在Java中这是必要的,因为有动态加载等功能,所以如果你想要,如果你动态加载一个类,那么你想能够使用那个类,你必须有一种查询类的,本课有哪些方法和物,这就是类对象的作用,所以有一个对象。

Java中每个类都有一个类对象,好的,所以当你加载一个类,首先必须初始化类对象,那怎么做呢,我们锁定该类的类对象,如果该对象已被其他线程锁定,那我们就等锁,好的,所以我们将等到有人告诉我们现在可以继续。

一旦我们获得类锁,我们必须检查类是否已被初始化,好的,结果可能是我们的线程,是同一个线程,已经在初始化类,那怎么会发生呢,记住,一个类,可以有,相同类型的字段,所以我可以有一个,名为x的类。

然后它可以有一个,类型为x的字段,类的初始化方式,如果我们需要初始化,类本身,然后,通过递归初始化所有字段的类,至少确保所有字段的类已初始化,如果我们有一个递归结构,其中字段中提到的类,如名称所示。

如包含类的名称,那么我们会遇到这种情况,初始化类的线程可能尝试再次初始化相同的类,因此,如果我们发现我们正在初始化这个类,我们只需释放锁并返回,另一种可能是类已初始化,所以当我们最终拿到锁。

我们发现其他线程已经进入,并在我们有机会之前初始化了类,那么就没有什么可做的,我们正常返回,如果这些都不是真的,好的,如果我们拿到锁并发现类尚未初始化,并且我们不是已经在初始化过程中。

那么我们将标记类以表示由该线程进行初始化,好的,我们将,我们将指示,你知道类正在初始化,我们正在初始化它,然后我们会解锁类,好的,接下来发生的事情,我们将要初始化超类,这意味着。

然后我们将按文本顺序初始化所有字段,但由于Java中有所谓的静态和最终字段,我们将首先初始化这些,好的,因此,静态最终字段将在文本顺序中的任何其他字段之前初始化,当然,在初始化之前。

我们必须要给每个字段一个默认值,就像在Cool中一样,所以步骤五与Cool中发生的情况非常相似,嗯,如果在初始化时出现错误,呃,初始化的一部分抛出异常,那么我们将标记该类为错误,好的。

我们将标记这个类为不好,不能使用,这是我们能做到的最好,如果在初始化时出现异常,我们只能放弃这个类,所以它有一个特殊的标记,说它是错误的,如果没有错误,如果我们成功初始化了类,并且没有任何错误。

那么我们将再次锁定类,我们将标记类为已初始化,然后我们会通知等待类对象的线程,所以任何被阻塞,等待类对象的人现在将被提醒对象已准备就绪,然后我们会解锁类,好的,就这样,这就是Java类初始化的概要。

我跳过了几件事,简化了它,所以这不是完整的描述,但这些都是主要观点,它们说明了语言的各种特性如何相互作用,所以你需要担心并发,你需要担心异常,你需要担心静态和最终字段,你需要担心继承。

我的意思是所有这些事情都必须一起处理,在设计一个单一的算法来执行类初始化时。

退一步说,关于Java类初始化的讨论说明了设计复杂系统的一般观点,所以在任何具有一定数量特性的系统中,每个系统都将具有一些特性,让我们称之为n,因为你想提供一些功能,显然系统应该做的事情。

所以它将有一些特性来做这些事情,但随着你添加特性,你会得到很多交互,潜在特性之间的交互,如果我们只考虑成对交互,那里的观点是,当然,是当我添加特性时,可能的交互数量以超线性方式增长。

它增长得比特性数量更快,所以添加下一个特性,你必须考虑系统中已经存在的所有先前特性,以及这个新特性如何影响它们,这就是为什么很难扩展或构建具有许多特性的系统的原因,对,这只是成对特性,这些只是。

这只是考虑一个特性与其他特性之间的成对交互,如果我要开始担心特性的子集,并考虑所有可能特性的子集如何相互交互,那么潜在交互的数量将不会,只是它将增长指数,实际上呈指数增长,所以将是,你知道。

远超过二次方,所以大而全的系统很难理解,你知道,这是一个你知道,计算机科学的一般教训,在任何想要设计复杂系统的学科中,并且这个教训适用于编程语言,它适用于您可能想要构建的任何其他类型的软件系统。

但它在编程语言中具有特别的力量,因为这些特征间的交互,发生在非常细的粒度,这些东西可以任意组合,因此在语言设计中,现在,必须确定所有交互,以便程序员能真正理解和有效使用语言,这确实我认为是。

课程中讨论的重大思想之一,也是我希望你们带走的东西之一,从这次讲座开始,至少特别地。

因此,总结并结束我们对Java的讨论,我认为Java是一种按生产标准做得很好的语言,它做得非常出色,因此,它是当今使用中设计最佳、规格最佳的语言之一,它将几个重要思想引入主流,所以当它是新的。

它带来了已经存在了很长时间的想法,但还没有找到进入非常广泛使用的生产语言的方式,特别是Java,是第一种广泛使用的语言,在商业环境中,具有强静态类型,它提供了类型系统提供的真实保证。

并且是一种受管理的语言,具有垃圾收集内存,但这并不意味着它是完美的,Java还包括一些在当时设计时,我们并没有完全理解的功能,现在可以说,这些可能是,Java设计中仍然存在粗糙的区域。

并发时内存语义有效,你知道,可能仍存在,多数人会同意,我认为现在有些问题和小灰色地带,作为程序员,你可能现在想避开,另一件事是工作,我刚有很多特性,如我之前所说,当你有很多特性,你将有更多功能交互。

导致复杂性,难以管理。

P97:p97 DeduceIt_Demo - 加加zero - BV1Mb42177J7

你好,在这段视频中,我将展示斯坦福大学演绎CEA研究项目的演示,以帮助学生在在线课程中学习正式系统,演绎它的基本思想是让学生完成正式推导,并使用他们的改进技术,这将检查那些推导是否正确。

这样你实际上可以学习形式推理的细节,所以让我们看看一个例子,这是一个代数小练习,我们的目标是证明x等于十一,好吧,这是我们正在努力实现的目标,我们从这个等式开始,2x减4等于x加7,一般来说。

这个特定练习中可能不止一个初始给定假设,这里只有一个,我们想从那个等式开始,我们想证明x等于十一,为了到达那里,我们可以使用任何列出的规则,我用鼠标圈出的那些,这些规则分为两类,这里有必需的规则。

这是顶部的这个第一组,必需的规则是必需的,因此,每当我们的推导步骤使用这些规则之一时,我们必须明确命名它,我们必须明确显示那一步,然后有一些规则被认为是自由的,这些规则我们不需要显示,所以,例如。

我们不需要显示所有涉及加法和乘法结合律的步骤,假设我们的老师已经说过我们理解了这一点,我们允许跳过这些步骤,系统将尝试填补它们,所以这些下面的规则,我们可以显示它们,如果我们喜欢,但它们是可选的。

我们允许跳过这些步骤,好吧,所以让我们开始,呃,处理这个,并且推导的每一步都将有三个部分,它将有一个结论,所以一些我们在这一步中正在证明的东西,我们会有理由,所以这一步遵循的规则,最后。

我们将使用哪些先前的事实,我们已经知道是真的,好吧,我们开始用哪个规则?然后我们从中得出什么结论?如何在这个例子中取得进步?我们可以在等式两边加4,为什么这样做合理?我们用了哪个规则?这是平衡方程。

使用上面的加法规则,它说在等式两边加相同的东西是可以的。

所以我们会从可能规则列表中选择这个规则,然后,嗯,我们使用了什么假设?目前我们只有一件事,这是我们开始时给定的基础,所以这是我们推导的一步,我们认为这是正确的,我们点击更新证明,系统回来确认确实如此。

是的,这是推导中有效的一步,好的,现在让我们再做一步,实际上让我们看看这里会发生什么,如果我们正确平衡方程,假设我们两边没加相同项,那么让我们试试,我们得到什么?我们看到现在变红了,表示有错误。

这里没给下一步填充,好,因为这一步不对,但我们也看到这有个问号图标,我们可以点击它。

它告诉我们出了什么问题,给了我们一些建议,平衡方程意味着两边都要加相同的值,好的。

所以有了这个建议,我们或许能找出哪里错了并纠正这一步,然后回到我们现在的正轨上,应该说不是每个错误步骤都有建议,但如果有了,你知道你,你可以点击了解详情,你可能确实做错了,现在让我们继续,看看这个。

看我们能做好什么,我们尝试简化左半部分,所以左边等于2x,右边,X加7加4,我们认为这里可以,因为你知道4加负4,只是加常数,这是我们免费规则之一,好的,所以我们可以加常数,你知道,和和和得到零。

我们应该能做些什么免费的事,好吧,这遵循我们之前的步骤,所以我们选择那个,现在我们可以更新,它回来说,哦,我们做错了什么,所以这不立即遵循前一步。

实际上这里有一个提示,所以我们可以看到为什么,我们看到,哦,加法恒等式是必需的规则。

那么我们犯了什么错误呢,你知道,把四和负四加在一起得到零是可以的,不需要显示,因为那只是乘法常数,但随后的一步,我们说的2x加零等于2x,这是我们实际上需要在这个练习中显示的。

所以每当我们使用加法恒等式,我们必须明确地说,所以我们可以我们可以修复它,说是的,实际上这一步实际上遵循加法恒等式从上一个,和所有在那一步中使用的免费规则,所以我们不需要命名它们,对吧。

所以现在我们可以继续,我们也可以简化左边的,你最好清理一下,这只是免费规则,我们不需要确切地说我们使用的是哪个规则,我想我那里没有更新,好的,所以现在,嗯,我想我意外地按了两次,呃。

所以让我们去掉其中一个步骤,好的,所以现在,嗯,下一步我们要执行什么,嗯,看起来我们需要把x移到左边,所以我们要在两边都加上一个负x,所以我有2x加负x等于x加11加负x,好的,那又是。

使用加法规则的平衡方程,这遵循我们上一步,好的,我们可以去掉那个,当我们更新那个,那没问题,好的,现在我们可以对左边的进行简化,我的意思是,抱歉,在右边,请原谅,因为所有这些都等于十一,好的,呃。

然后我们必须使用x加负x等于0,然后我们只是加十一和零,所以那可以算作加常数,所以这应该遵循,呃,从之前的加法逆元步骤,如果我们更新那个,是的,那解决了,现在我们只需要处理左边,我们有两个x加负x。

为了把它变成可以简化的形式,我们需要使用分配律,我们需要拉出常数,呃,那些在x前面的,我们可以做到,我们可以说,嗯,二加负一乘x等于十一,所以我们使用了分配律,但我们也在使用一元负数规则,说。

你知道负x等于负一乘x,但那是免费规则,所以我们不必担心那个,所以我们只需要命名分配律,并且那遵循之前的推导步骤,我们更新那个,好的,现在我们就快到了,所以现在我想一步就能完成简化。

我们可以从二减一得到一,然后有一乘x等于十一,那是乘法恒等式,这是这个特定练习所需的规则,遵循之前的步骤,像所有其他,我喜欢所有之前的步骤,呃,现在我们完成了,现在我们已经证明了x等于十一。

系统知道我们已经完成了作业,这是背后的基本想法,演绎它,这个例子虽为代数,任何形式系统皆可,呈现练习规则集。