00:00
在我上一个视频中,我谈到了在代码中提供变量的大小,如何帮助编译器加速你的程序。我在那个视频的结尾提到,你可能希望你的类型在运行时动态增长或缩小。但这是个问题,因为在低级编程语言中,编译器在你未明确指定大小时会提示错误,所以你不能声明带有数组的自定义类型。除非你给数组一个特定的大小,但这意味着你的数组在程序的整个生命周期中都将具有那个大小。到这个视频结束时,你将理解这个限制背后的原因。嗨,朋友们,我是乔治,这里是call dump, 今天我们将要看看站那个每个人都告诉你超级快,你应该总是尝试在上面放置数据,但几乎没有人告诉你为什么的内存区域。站是一种遵循后进先出原则的数据结构,这意味着添加到站中的最后一个元素是第一个被移除的,通常它的大小是有限的,所以如果我们推送了太多元素,并且空间不足,我们说战役出了。
01:10
这对你来说可能听起来很熟悉,尽管最近由于大型语言模型的崛起,这个网站已经死了,比如我。我的意思是像GPT4这样的,无论如何,这个视频中的站不是软件站,尽管它们之间有关系,正如你将看到的。我们现在研究的是执行站,有时也称为程序站或甚至是硬件站。当我们运行一个程序时,理论上没有什么可以阻止我们在内存中的任何地方写入我们的值。尽管我们指定了我们的值的大小,但随机放置它们可能会导致空间浪费。为了理解这一点,考虑操作系统的作用至关重要。你可能熟悉操作系统始终充当程序和硬件之间的中介的概念。嗯,内存也不例外。当一个程序被执行时,它不能简单的开始在物理内存的任何地方存储数据,因为其他程序可能已经在利用那个空间的一部分。
02:10
因此,在此之前,他必须从操作系统请求内存。操作系从反过来会寻找我们的程序可以写入的可用空间。如果我们尝试从操作系统没有分配给我们的程序的内存地址读取或写入,它将立即出于安全原因终止程序的执行。这时,我们就遇到了臭名昭著的错误,称为断错误call dump这个频道的名字就来源于此。这里的重要部分是,操作系统不会精确的按我们需要的量分配内存,相反,它提供了一个内存块,我们可以自由的读写。然而,如果我们不组织我们的数据,只是随意放置,我们可能会面临这个问题,有足够的空闲内存,但由于缺乏连续空间,我们无法存储数据。
03:00
这个问题通常被称为外部碎片。在这种情况下,我们在内存中存储我们的值的唯一方法是请求另一个快。如果我们更有效率地组织了我们的数据,就不需要请求额外的内存。值得注意的是,请求更多内存在性能方面可能相当昂贵。作为一个简短的预览,这就是为什么许多人建议尽量减少使用堆的原因。我们将在我的下一个关于堆的视频中更深入的探讨这个话题,所以考虑订阅以获取更多细节。此外,当操作系统为我们的程序分配内存时,它不会跟踪我们是否使用了那个内存,它唯一的意识是指定的内存区域属于我们的程序。如果另一个程序需要内存,它将不得不把它的数据放在其他地方,即使在我们的程序内存区域有足够的空闲连续内存。
04:00
但内存是有限的,那么如果所有的内存已经被其他程序使用了,我们该怎么办?再一次,答案在于操作系统。现代操作系统专门设计来解决这类问题,在没有可用内存的情况下,他们利用存储空间,就好像他是额外的内存。你见过Linux系统中称为交换分区的那个分区吗?现在你知道它是做什么的了。这就是我们所说的虚拟内存。它非常有用,因为它不仅创造了比实际存在的内存多得多的假象,而且它还给人一种直接写入物理内存的外观。所以作为程序员,我们不必担心所有这些底层概念。我可以花30分钟谈论虚拟内存,但这超出了这个视频的范围。然而,重要的是要注意依赖存储来扩展主内存是一把双刃剑。存储比RAM慢数千倍,操作系统提供的这个功能应该谨慎使用。
05:01
在我上一个视频中,有人评论说,在现代,由于几乎无限的内存可用,我们的数据和内存大小可能看起来不重要。虽然对于我们作为程序员来说,内存看起来是无限的,但重要的是要谨慎行事,并考虑到性能仍然扮演着重要的角色。说到性能,事实证明即使是内存也可能成为瓶颈。尽管现代CPU的速度非踌,但从内存中获取数据会产生显著的成本。为了解决这个问题,制造商在现代芯片中集成立一个名为缓存的功能。缓存本质上是处理器内部的一小部分专用内存,其中存储了内存区域的副本。这样当CPU需要数据进行操作或操作时,它可以迅速从缓存中检索它,而不是在主内存中搜索它。当必要的信息在缓存中时,我们称之为缓存命中。然而,如果所需的数据不在缓存中,就会导致缓存未命中。
06:05
在这种情况下,CPU被迫等待数据从主内存中获取,增加了额外的时间。再次,这对我们程序员来说是完全看不见的。在这种情况下,是硬件而不是操作系统决定哪个内存区域保留在缓存中。如果我们关心性能,我们能做的最好的事情就是尽可能保持我们的数据紧凑。这样它更有可能适合缓存。这种接近性增加了缓存命中的机会。这种将数据尽可能靠近CPU的新概念是低级人员所知道的局部性。现在你有第二个保持数据紧凑的原因,性能。现在你可能会想,这一切与站有什么关系?好吧,事实证明,如果我们想要数据保持紧凑,一种好的组织方式是使用站,让我们来看看。
07:03
我们可以做的第一件事是加快速度,是在程序启动时预先分配内存。这样每当我们需要存储一些值时,我们已经有一个可以写入的地方。由于我们的目标是避免在运行时从操作系统请求内存这个预先分配区域的大小在整个程序的生命周期中保持固定。因此,我们在确定其大小时必须小心。虽然没有什么可以阻止我们为这个区域分配千兆字节,但考虑我们程序的简单性是必要的。如果它只使用了几个变量,它可能永远不会完全占据这么大的内存空间。正如前面提到的,这将导致内存浪费,因为操作系统会为我们的程序保留那个空闲空间,阻止其他程序使用它。因此,这个预先分配区域的大小通常保持较小。确切的大小取决于我们使用的编译器,但通常不会超过几兆字节。
08:00
乍一看,这可能看起来不足,但如果你做一下数学,在一个照字节中,你可以容纳超过13万个64位整数。当我们的程序启动时,主函数作为我们的入口点,每次遇到一个新变量,它的值就被堆叠到这个指定的区域。这就是术语站变得明显的地方标记。站开始的内存地址称为站原点,而站指针只是站上的当前最高数据。当然占指针存储在某个地方。它可以在内存中完成,但现代架构将一个寄存器专门用于直接在CPU内保存站指针值。因此,处理器不需要等待它从内存中获取,甚至从缓存中获取。内存分配在站上高效的原因之一在于占指针的持续指导。要在站上写入某些内容,所需要做的就是获取站指针地址,将1加到该值上,结果就是我们可以在其中写入更多数据的内存地址。就这样。
09:06
这就是在站上分配内存所需的全部。正如你将在我即将发布的视频中发现的,当在堆上分配内存时,这个过程并不那么简单。在那种情况下,我们必须从操作系统请求内存,等待它找到可用空间。并且由于系统不会返回所需的确切内存量,我们必须采用巧妙的策略来避免碎片化。所有这些都是使过程变慢的额外步骤。当调用一个函数时,它的所有局部值都被推到站上。但重要的是要记住这些局部值的起始点,因为当函数结束时,我们需要将站指针重置为函数调用之前的位置。这些不同的子区域被称为战争。当被调用的函数预期要返回一个值时,例如,在这个例子中,我们分配了一个称为返回链接的独立空间。接下来,我们开始推送我们的局部值。
10:04
函数结束后,返回值直接写入返回链接。随后,被调用函数的战帧被移除,除了返回值,它现在集成到调用者函数的战争中。在这里重要的是要注意,为了使编译器能够生成高效的机器代码,它需要知道返回链接的确切大小。这就是为什么系统编程语言要求指定函数返回值的确切大小,你通过提供返回类型来实现。最后,让我们考虑一些重要的限制。在这个例子中,我们在站上写入一些值,一个数字,一个数组,然后是更多的数字。假设我们想向数组添加一些元素。看到这里的问题了吗?在站上的数组中添加元素,意味着我们可能不得不覆盖我们以后可能需要的其他值。
11:00
这正是我在上一个视频中谈到的。如果你不为任何数组指定大小,编译器会提示错误,即使数组是自定义类型的成员。如果我让你等了很久才得到这个答案,我很抱歉。但我认为我到目前为止谈到的所有内容都是必要的,以便理解这一点。当你指定数组的大小时,编译器获得了关于在站中容纳该值所需的空间量的确切知识。但由于站的有限大小,即使提供了大小,超出这个限制也可能导致程序崩溃,导致站1出错误。这种情况在处理递归时特别相关。当一个函数调用自身时,新的战争会连续的被推入战中,用于每个递归调用。如果没有定义一个基本情况来停止递归,程序将不断添加战争直到溢出。此外,即使有一个基本情况,如果程序在超过占容量之前没有达到它占一出也可能发生。因此,有时建议使用迭代解决方案,而不是递归解决方案。
12:10
重要的是要澄清站的行为受到编程语言及其编译器的影响。你在这段视频中看到的,可能不会完全反映每种语言的实现。因此,重要的是要理解,我的目标是简化这些视频中的概念,以便于说明。总之,今天我们知道了,保持数据紧凑对于通过避免碎片化减少内存使用和通过增加缓存命中概率提高性能至关重要。指定变量的大小,使编译器能够在站中更有效和可预测的组织我们的数据。这种组织方式有助于以无缝和快速的方式处理函数调用和局部变量。我们还探讨了站的限制。它具有大小限制,并且缺乏类型动态增长或缩小的灵活性。此外,占作为单线程实体运行。
13:04
在多线程的上下文中,每个线程都需要自己的站,这在尝试在线程之间共享内存时构成限制。所有这些挑战都找到了一个共同的解决方案及所谓的堆。我们将在未来的视频中探讨这个话题。这就是站的工作方式,非常非常简化的版本,希望你现在对它为什么这么快有了一个概念,如果你学到了一些东西,就按那个喜欢按钮。这对我帮助很大,制作这些视频有点耗时,但我一直很喜欢,所以如果你想了解更多,考虑订阅。
我来说两句