机器学习和神经网络 - 页 30

 

第 11 讲。存储分配



第 11 讲。存储分配

讲师在此视频中讨论了存储分配,包括内存分配和释放。解决了不同类型的存储,包括堆栈和堆,以及固定和可变大小的分配方案。还讨论了由内存块分散引起的外部碎片,以及每个磁盘页面的空闲列表或位图等解决方案。还介绍了垃圾收集的概念,包括引用计数和该算法的局限性。还解释了“标记和清除”和“停止和复制”垃圾收集过程,重点是后者如何解决碎片和指针更新可能产生的正确性问题。该视频以关于停止和复制算法的时间和空间复杂度及其消除外部碎片的注释结束。

该视频涵盖了与动态存储分配相关的各种主题,包括 Buddy 系统、标记和清除程序、优化、分代和实时垃圾收集、多线程存储分配和并行垃圾收集。讲师解释说,分代垃圾收集基于更频繁地扫描年轻对象的想法,而实时垃圾收集在程序执行期间在后台运行,但如果对象和指针图不断变化,可能会导致正确性问题。最后,讲座总结了不同类型的存储分配,包括堆栈和堆,以及不同的垃圾收集方法,例如引用计数、标记清除和停止复制。讲师提到学生将在即将到来的家庭作业中尝试其中的一些方法。

  • 00:00:00 在本节中,讲师讨论存储分配,包括内存分配和释放。最简单的存储形式是堆栈,其中通过递增堆栈指针来分配内存,并通过递减堆栈指针来释放内存。然而,堆栈的适用性有限,因为它只能释放最后分配的东西,而不能释放已使用区域中间的任何东西。讲师还指出,出于效率原因,编译器通常不会检查堆栈溢出。

  • 00:05:00 在这一节中,讲师讲了两种存储方式:栈和堆。栈是一种固定大小的存储,效率高但不能用于所有事情,而堆更通用但难以高效组织和使用。对于堆分配,可以使用 malloc 和 free C 函数,但由于 C 和 C++ 不提供垃圾收集器,程序员必须自己管理内存,这会导致内存泄漏、悬垂指针和双重释放。讲师建议使用地址清理器和 Valgrind 等内存检查器来减少程序中的内存错误数量。

  • 00:10:00 在本节中,演讲者讨论了不同的存储分配方式,从固定大小分配开始,其中每个存储块具有相同的大小,一些块已使用,而另一些未使用。未使用的块保存在一个称为空闲列表的列表中,空闲列表中的每个块都有一个指向列表中下一个块的指针。为了从空闲列表中分配一个对象,程序将指针设置为空闲,然后将空闲指针设置为指向空闲列表中的下一个对象,返回指向空闲列表中第一个对象的指针。要释放,只需将对象的下一个指针设置为释放并将释放设置为等于要释放的对象。使用空闲列表,分配和释放需要恒定的时间,并且还具有良好的时间局部性。

  • 00:15:00 在本节中,介绍了外部碎片的概念,当使用的内存块散布在所有内存空间时,就会发生这种情况。这会导致更大的页表,从而导致磁盘抖动并降低查找翻译的效率。为了减轻外部碎片,可以使用每个磁盘页面的空闲列表或位图,并且可以从最满的页面进行分配、释放故事块和调出空页面。固定大小的堆分配也可用于减少外部碎片。最后,可变大小的堆分配可以与 bin 空闲列表一起使用,它针对不同大小的已分配内存利用空闲列表的效率。

  • 00:20:00 在本节中,介绍了一种在内存分配系统中存储不同大小的内存块的方案。该方案涉及根据块的大小将块分成 bin,每个 bin 包含指定大小的块,这些块是 2 的幂。分配内存时,根据请求的大小选择合适的bin,如果为空,则将较大的非空bin拆分成较小的块以获得所需的大小。但是,这种方案会导致内存内部碎片化,造成浪费,因此限制使用的bin数量,将小块分组到页中以减少开销。如果需要,操作系统可用于提供更多内存,并且有系统调用可用于此目的。

  • 00:25:00 在本节中,视频讨论了内存分配的工作原理,并指出“malloc”的标准实现依赖于“break”等命令从操作系统获取内存。操作系统提供了大量内存,存储分配系统将其分解为更小的块。本节还介绍了内存分配器的变体,包括用于存储块大小的不同方案以及虚拟内存地址空间在程序中的布局方式。它注意到堆栈和堆相互增长但从不相交。最后,提到在程序中预先计算大型常量表会增加加载时间,因为它需要从数据段读取。

  • 00:30:00 在本节中,讨论了虚拟内存的碎片问题,如果内存分散,它可能导致外部碎片、页表性能下降、磁盘抖动和 TLB 命中率低。存储分配的目标是使用尽可能少的虚拟内存,同时保持已使用的内存部分紧凑。分析了 bin free list 分配方案,有一个定理表明堆存储消耗的虚拟内存量上限为 M log M。将较小的块合并为较大的块可以改进 bin free list 方案,但开销根据 Charles Leiserson 的一篇论文,与标准 bin free list 算法相比,这种方法可能比最佳分配器差一个常数因子。

  • 00:35:00 在本节中,演讲者解释了存储分配的概念以及它如何在处理堆存储时减少碎片。他还讨论了垃圾收集的想法,它使程序员不必手动释放对象。演讲者解释了三种类型的内存对象——根、活对象和死对象——以及垃圾收集如何回收后者。最后,他描述了引用计数,这是一种简单的垃圾收集形式,它计算引用一个对象的指针的数量以确定它是否可以被释放。

  • 00:40:00 在本节中,演讲者介绍了引用计数作为垃圾收集方案的概念,并解释了引用计数算法的工作原理。然而,指出了该算法的局限性,即无法收集参考图中的循环。演讲者随后讨论了在某些编程语言中使用强指针和弱指针来避免这种限制。最后,演讲者预览了另外两种垃圾收集方案,“标记和清除”和“停止和复制”,它们在处理引用图中的循环时避免了引用计数的限制。

  • 00:45:00 在本节中,演讲者讨论了使用广度优先搜索算法来查找内存中的所有活动对象。具有两个指针的数组用于表示用于搜索的 FIFO 队列,算法标记每个可从根到达的活动对象。搜索完成后,未标记的对象可用于垃圾收集。标记和清除过程包括两个阶段:标记阶段,涉及广度优先搜索算法;清除阶段,涉及扫描内存以释放未标记对象。但是,此方案存在局限性,例如需要扫描所有内存以及与查看未引用对象相关的开销。

  • 00:50:00 在本节中,视频介绍了处理碎片的“停止和复制”垃圾收集程序。此过程类似于标记和清除算法,但采用不同的方法来确保活动对象在内存中是连续的。该过程使用两个独立的内存空间——from 空间和 to 空间——并从 from 空间分配和释放对象,直到它变满。然后运行垃圾回收算法,将二次空间作为队列进行广度优先搜索,识别存活对象,在二次空间内存中连续放置。但是,使用此算法可能会导致正确性问题,即指向源空间中对象的指针可能不再正确。为了解决这个问题,该过程在源空间中的相应对象中存储一个转发指针,并在广度优先搜索中从队列中删除对象时,根据这些转发指针更新所有指针。

  • 00:55:00 在本节中,演讲者讨论了停止和复制垃圾收集算法、其时间复杂度以及它如何处理外部碎片。停止和复制算法涉及将对象从 from 空间复制到 to 空间,然后更新 to 空间中这些对象的指针。这种算法在时间和空间上是线性的,这使得它成为一种更高效和有效的垃圾收集方式。当from空间变满时,用户可以请求一个新的堆空间并将from空间的大小加倍。该方案所需的虚拟内存空间为常数倍最优,该算法消除了外部碎片。

  • 01:00:00 在本节中,演讲者讨论了更多与动态存储分配相关的主题,例如用于合并的 Buddy 系统、标记和清除过程的变体、提高性能的优化、分代垃圾收集、实时垃圾收集、多线程存储分配和并行垃圾收集。分代垃圾收集基于这样一种想法,即许多对象都是短暂的,因此大多数时候只扫描较年轻的对象。实时垃圾收集在程序运行时在后台工作,但如果对象和指针的图形发生变化,它可能会导致正确性问题。多线程存储分配和并行垃圾收集有多个线程运行,这使得算法更难以处理竞争和其他正确性问题。演讲者还总结了不同类型的存储分配,包括堆栈和堆,以及进行垃圾收集的不同方式,例如引用计数、标记和清除以及停止和复制。

  • 01:05:00 在本节中,讲师提到他们将进一步研究存储分配方案,学生将有机会在即将到来的家庭作业中尝试其中的一些方案。然后,讲师结束了讲座,并开始提问。
 

第 12 讲。并行存储分配



第 12 讲。并行存储分配

该视频讨论了各种内存分配策略及其权衡。解释了 malloc 和对齐分配的使用,以及使用 free 正确释放内存的重要性。还介绍了使用 Mmap 进行虚拟内存分配,以及外部碎片和性能低下的问题。探讨了 C 和 C++ 编程中堆栈的概念,重点放在用于分配堆栈帧的基于堆的仙人掌堆栈方法上,这可以带来更好的空间限制证明和上限。还讨论了线性堆栈池的使用,以及优化小块以最小化碎片的重要性。该视频最后讨论了全局和局部堆方法及其潜在问题,以及用于解决这些问题的 bin 空闲列表和定期内存重新平衡等方法。

该视频还讨论了并行存储分配的解决方案,并介绍了 Hoard 分配器作为解决方案。 Hoard 分配器结合使用本地和全局堆以及可以在堆之间移动的大型超级块,以减少错误共享、提高可伸缩性并减少外部碎片。全局堆中分配的最大存储最多是本地堆中分配的最大存储,占用空间上限为用户占用空间加上本地堆中已分配但未使用的存储空间。该视频还简要讨论了其他分配器,例如 Je malloc 和 Super malloc,基准测试结果表明 Super malloc 的性能优于 Je malloc 和 Hoard。讨论最后引用了有关垃圾收集详细信息的在线幻灯片。

  • 00:00:00 在本节中,讲师回顾了内存分配原语,包括 malloc 和对齐分配。对齐分配可用于将内存与缓存行对齐,以便访问缓存行中的对象仅需要一次缓存访问,从而减少缓存未命中的次数。讲师还讨论了使用 free 函数正确释放内存以及避免内存泄漏和双重释放的重要性。最后,讲师介绍了系统调用 Mmap,它可以用于在没有备份文件的情况下分配虚拟内存。

  • 00:05:00 在本节中,演讲者解释了使用 M 映射分配内存的过程,这是一个系统调用,它在应用程序的地址空间中找到一个连续的未使用区域,该区域的大小足以容纳所请求的内存大小,并更新页表包含分配页的条目。与 malloc 不同,malloc 是库调用和 C 库的堆管理代码的一部分,M map 不会立即为请求的内存分配物理内存,而是使用指向特殊零页的条目填充页表,该零页被修改和仅当用户首次写入时才转换为物理内存。此外,M map 负责从操作系统获取内存,而 malloc 负责重用之前分配的内存以减少碎片并改善时间局部性。

  • 00:10:00 在本节中,视频讨论了使用 MMAP 和 MALLOC 进行存储分配的区别,强调 MMAP 相对较重,即使是小分配也会分配整个页面,从而导致外部碎片和性能下降。该视频还回顾了地址转换的过程,其中虚拟地址用于访问内存,硬件在页表中查找合适的物理地址。该视频解释了 TLB 如何通过缓存最近的页表查找来工作,并指出具有空间或时间局部性的程序会将许多最近的访问存储在 TLB 中,从而提高性能。

  • 00:15:00 在本节中,演讲者讨论了地址转换在现代机器中的工作原理,并深入探讨了 C 和 C++ 编程中堆栈的概念。堆栈用于跟踪函数调用和局部变量并遵循线性顺序。演讲者强调了传统线性堆栈中指针的一个规则:父级可以将指向其堆栈变量的指针向下传递给其子级,但不能反过来,以避免覆盖变量。演讲者还建议了一些选项,例如在堆上或在父级堆栈上分配内存,以便将内存从子函数传回父级。

  • 00:20:00 并行堆分配正确,使用基于堆的仙人掌堆栈的潜在问题是内存碎片,其中可能没有足够的连续内存留给未来分配,导致空间浪费并可能耗尽内存或使程序崩溃。虽然早期的并行编程系统使用了这种策略,但优化代码以最大限度地减少性能影响并考虑内存碎片的后果非常重要。

  • 00:25:00 在本节中,视频讨论了使用基于堆的仙人掌堆栈方法来分配堆栈帧,而不对最大深度设置上限。然而,当尝试将传统的线性堆栈代码与这个基于堆的仙人掌堆栈一起使用时,互操作性是一个问题。这可以使用一种称为线程局部内存映射的方法来解决,但这种方法需要更改操作系统。尽管如此,基于堆的仙人掌堆栈方法在实践中仍然很好,可以用来证明使用它的 Silk 程序的空间限制。这个空间限制表明,使用基于堆的仙人掌堆栈的 P worker 执行的堆栈空间上限为 P 乘以 s1,其中 s1 是串行执行 Silk 程序所需的堆栈空间。这是基于堆的仙人掌堆栈方法的一个很好的特性。

  • 00:30:00 在本节中,分析了分治矩阵乘法代码,以了解如何将其应用于时空权衡定理。该代码进行了八次递归调用,每次调用的大小为 N/2,并且在每次调用之前,它执行 malloc 以获得大小为 N 平方的临时空间,然后在函数返回之前释放该空间。由于分配遵循堆栈规则,即使在堆外分配时,上一张幻灯片中的空间限制也可用于限制 P 工作空间。工作是 N 的立方,跨度是对数平方 N 的阶数。空间的递归是 N/2 的 s + N 平方的 theta,求解为 N 平方。因此,空间边界的忙叶性质和定理表明,在 P 个处理器上,空间的边界为 P 乘以 N 的平方。

  • 00:35:00 在本节中,演讲者向听众介绍了如何为上一节中讨论的算法证明更强的空间界限。通过分析算法的结构并着重于在递归树顶部附近尽可能多地分支,演讲者能够证明 P 的空间界限为 N 平方的三分之一,这比之前的上限要好.演讲者指出,与简单地应用一般定理相比,分析算法的结构可以产生更强的空间限制证明。本节最后讨论并行函数如何无法与遗留和第三方串行二进制文件互操作。

  • 00:40:00 在本节中,演讲者讨论了线性堆栈池在存储分配中的使用,可用于为遗留代码维护堆栈池。池中堆栈的数量应该是处理器数量的常数倍,以便保留空间限制。然而,这种策略会影响工作窃取算法的时间限制,因为如果没有更多可用的堆栈,工作人员可能无法窃取。演讲者随后讨论了堆存储分配器的基本属性,包括分配器速度和用户足迹,并强调了针对小块进行优化的重要性,因为分配中可能存在碎片和开销。碎片定义为分配器占用空间除以用户占用空间。

  • 00:45:00 在视频的这一部分,演讲者讨论了如何使分配的足迹尽可能接近用户足迹,以最大限度地减少两者的比率。一种解决方案是使用称为 M advise 的东西,它告诉操作系统不再需要某个内存页,但应将其保留在虚拟内存中。演讲者还提到了上一讲的定理,即 bin free lists 的碎片是 U 的 order log base 2,其中 U 是分配的内存总量,并解释了空间开销、内部碎片和外部碎片之间的区别。最后,演讲者指出,现代 64 位处理器提供大约 2 到 48 字节的虚拟地址空间,这对于大多数程序来说已经足够,但仍然超过典型机器上的物理内存。

  • 00:50:00 在本节中,视频探讨了使用具有互斥保护的全局堆的并行堆分配策略,这是默认 C++ 分配器的工作方式。这种策略的缺点之一是,与串行分配器相比,它不需要额外的空间。然而,这种方法的潜在问题是性能下降,因为每次分配和释放都获取锁很慢,并且随着处理器的增加而变得更糟。锁争用是可扩展性差的主要原因,由于更高的请求率,这对于小分配来说问题更大,而大块分配需要更多的工作。

  • 00:55:00 在本节中,视频讨论了使用本地堆方法的潜在问题,即它可能导致内存漂移和无限爆炸。本质上,如果您在一个线程中分配所有内存但在另一个线程中释放它,则系统中可能存在未使用的内存,分配器不够智能,无法重用。因此,在多个处理器上运行的程序最终可能会耗尽内存,但如果它仅在单个内核上运行,则不会发生这种情况。该视频建议使用 bin free list 方法来解决此问题,这涉及在 bin free list 数据结构中设置指针,以便释放的块可以出现在其中一个链表中。另一种策略是定期重新平衡内存,但也讨论了一种更简单的方法。

  • 01:00:00 在本节中,视频讨论了如何更改内存分配器以实现让每个线程访问其自己的堆而不需要全局堆的预期行为。一种方法是用所有者标记每个对象,并在它被释放时将其返回到所有者的堆中。这样,本地对象可以快速分配和释放,而远程对象需要一些同步但不像全局堆那么多。可分配的最大内存量受限于 X,即顺序程序使用的内存量,而爆破率上限受限于 P,即堆的数量。这种方法还对错误共享具有弹性,错误共享会在多个处理器访问不同的内存位置但位于同一缓存行时发生。

  • 01:05:00 在本节中,演讲者讨论了并行堆分配如何导致错误共享,并介绍了 hoard 分配器作为解决方案。 hoard 分配器使用局部堆和全局堆的组合,将内存组织成可以在堆之间移动的大型超级块。这种方法快速、可扩展且对错误共享具有弹性。进行分配时,分配器检查本地堆中的空闲对象,如果存在则获取它。如果没有,它会进入全局堆以获取更多内存。

  • 01:10:00 在本节中,演讲者解释了 Horde 分配器如何通过从最满的非满超级块中分配一个空闲对象来密集超级块的移动来减少外部碎片。如果在本地堆中找不到该对象,它会检查全局堆,如果全局堆为空,它会从操作系统调用一个新的超级块。 Horde 分配器维护一个不变量,其中堆最多使用了一半,如果它检测到堆未被充分利用,它会将半空的超级块移动到全局堆。然后演讲者提出一个引理,指出全局堆中分配的最大存储最多是本地堆中分配的最大存储。最后,演讲者证明了 Horde 分配器的占用空间上限为 u 加 SP 的定理,其中 u 是程序的用户占用空间,SP 是本地堆中已分配但未使用的存储空间。然后将放大计算为一阶 SP 除以 u。

  • 01:15:00 在本节中,演讲者讨论了不同的分配器,包括 Je malloc 和 Super malloc。 Je malloc 对不同的分配大小有单独的全局锁,并使用 em advice 释放空页,这使得它对不同的分配跟踪具有鲁棒性。另一方面,Super malloc 代码行数极少,发展速度非常快。演讲者分享了基准测试结果,其中 Super malloc 的性能优于 J malloc,J malloc 的性能优于 Horde,而默认分配器的性能较差。讨论还扩展到垃圾收集,但是,演讲者将其细节推迟到在线幻灯片。
 

第 13 讲 Cilk 运行时系统



第 13 讲 Cilk 运行时系统

Cilk 运行时系统负责并行处理器上的调度和负载平衡计算,使用随机工作窃取调度程序在运行时将计算映射到处理器。该系统旨在优化程序的串行执行,即使以窃取额外成本为代价。 worker 维护一个单独的 deck 数据结构,其中包含指向堆栈帧的指针并使用头指针和尾指针。可被窃取的帧有一个额外的本地结构,其中包含必要的信息,以便在 deck 位于调用堆栈外部时发生窃取。该部分解释了系统如何使处理器能够从正在运行的函数的中间开始执行,以及当到达无法执行超出该点的同步语句时处理器之间的同步如何变得复杂,因为特定处理器仍在等待计算完成。此外,演讲者还介绍了系统的性能、设计考虑和数据结构,以及系统如何使用 THC 协议优化执行,涉及两组协议,一组用于执行工作的工人,另一组用于小偷。

Cilk 运行时系统使用设置跳转和跳远协议来处理从受害进程的执行平台窃取计算。 Cactus Stack 抽象允许窃贼进程拥有自己的调用堆栈,以防止损坏受害者的堆栈。系统的同步协议使用计算树和 Cactus Stack 来确保同步仅发生在函数内的嵌套计算之间。 Full Frame Tree 维护计算与未完成子计算之间的父子关系,以简化同步过程。此外,运行时系统优化了当前函数没有未完成的子计算并且所有工作进程都忙的常见情况。支持的其他功能包括 C++ 异常、reducer 超对象和谱系。

  • 00:00:00 在本节中,Tibi 解释了 Cilk 运行时系统,该系统负责并行处理器上的调度和负载平衡计算。运行时系统使用随机工作窃取调度程序在运行时将计算映射到处理器,确保高效执行。 Tibi 指出,Cilk 运行时系统是一个复杂的软件,但其底层的魔力是通过 Cilk 编译器和运行时库的协作实现的。此外,他还展示了一个简单的 Cilk 程序编译后的伪代码,突出了 Cilk 运行时系统背后的复杂过程。

  • 00:05:00 在本节中,演讲者解释了 Cilk 运行时系统所需的功能以及性能注意事项。他使用并行化斐波那契例程的示例来说明 Cilk 程序如何执行计算 dag,它会随着程序在一个处理器上运行而动态展开。但是,当遇到silk spawn语句时,为3的fib创建了一个新的frame,并且有另一条strand可以并行执行。然后处理器开始执行 fib of 3 ,就好像它是一个普通的函数调用一样。当指令指针返回到 fib 例程的开头时,该过程会自行重复。

  • 00:10:00 在本节中,我们将看到多个处理器如何在 Cilk 运行时系统的帮助下并行执行一个 fib 例程。尽管有独立的寄存器,但每个处理器都会跳到正在执行的函数的中间并开始运行它,这就引出了一个问题,即 Silk 系统如何使处理器能够从正在运行的函数的中间开始执行。此外,当到达无法执行超出该点的同步语句时,处理器之间的同步变得复杂,因为特定处理器仍在等待计算完成,这需要在嵌套模式中进行细粒度同步。

  • 00:15:00 在本节中,演讲者讨论了 Cilk 运行时系统的实施注意事项。他们提到了三个主要考虑因素:单个工作人员能够执行程序、在执行功能中间跳转并从其他处理器停止的地方接手的能力以及同步。此外,Silk 有一个仙人掌堆栈的概念,需要正确实现它以使堆栈的所有视图可用且一致。最后,系统需要通过允许处理器相互窃取帧来允许工作窃取。

  • 00:20:00 在本节中,演讲者讨论了 Cilk 运行时系统所需的功能,其中包括串行执行、窃贼跳入运行函数的中间、同步以嵌套细粒度方式进行同步、仙人掌栈用于所有工作人员都可以看到,并处理生成帧和被调用帧的混合,这些帧在窃取计算时可能可用。然后该部分继续讨论系统的性能,我们需要足够的并行性,T1 over T infinity 应该比 P 大很多,并且我们希望在增加运行程序的处理器数量时线性加速。本节还介绍了 TCP 在 P 个处理器上的预期运行时间,它与计算工作量除以处理器数量再加上计算跨度的数量级成正比。

  • 00:25:00 在本节中,我们将了解 Cilk Runtime System 及其在具有足够并行性的程序中保持高工作效率的目标。 Silk Runtime System 通过优化程序的串行执行来遵守工作至上的原则,即使以钢材的一些额外成本为代价。运行时系统库处理并行执行的问题,并在出现故障时处理较慢的执行路径。 worker 维护一个单独的 deck 数据结构,其中包含指向堆栈帧的指针并使用头指针和尾指针。可被窃取的帧有一个额外的本地结构,其中包含必要的信息,以便在 deck 位于调用堆栈外部时发生窃取。

  • 00:30:00 在本节中,我们将了解 Cilk Runtime System 的设计及其基本数据结构。系统为每个计算生成一个 spawn helper 函数,它分别为每个 spawning 函数和 spawn helper 的实例化维护一个 worker 结构和 Silk 栈帧结构。 Silk RTS 堆栈帧包含缓冲区和标志整数等字段以总结 Silk 堆栈帧的状态,以及指向调用堆栈中父 Silk 堆栈帧的指针。 worker 结构维护一个 deck 和一个指向当前 Silk RTS 堆栈帧的指针。这些数据结构是Intel so+运行时系统所阐述的Cilk Runtime System的精髓。

  • 00:35:00 在本节中,视频探讨了生成函数“foo”和生成辅助函数的代码。 “foo”的代码包括初始化堆栈帧的调用、使用设置跳转例程设置生成、调用生成辅助函数“生成栏”、Silk RTS 中接收器的条件代码块,并调用 pop 和 leaf 框架进行清理。 spawn helper 的代码包括初始化堆栈帧的调用和分离 Silk RTS 的调用,以及用于堆栈结构的额外清理函数。设置例程被描述为一个函数,它允许窃贼窃取函数的延续,将缓冲区作为参数来存储恢复函数位置所需的信息。
     
  • 00:40:00 在本节中,演讲者讨论如何限制寄存器集并将它们保存到预定的集合中以供函数调用。保存寄存器的责任落在函数身上,但指令指针和堆栈指针被保存到缓冲区中。本节继续讨论 spawn helper 函数以及它如何更新 worker 结构和当前堆栈帧。最后,该部分解释了 enter frame fast 例程如何在调用堆栈中建立父指针并更新 worker 的当前堆栈帧以指向底部。

  • 00:45:00 在本节中,名为“The Cilk Runtime System”的 YouTube 视频的文字记录解释了 Silk RTS 分离功能如何允许窃取计算,一旦 silk 艺术执行完毕,小偷可能会来窃取丝产卵的延续。 pop frame负责清理栈帧结构,更新当前栈帧指向父级。这个函数调用可能返回也可能不返回,这两种情况哪个对runtime系统优化更重要就是成功案例,基于两者先工作的原则。

  • 00:50:00 在本节中,演讲者讨论了 Cilk 运行时系统在案例一中对工作人员执行的优化,其中假设工作人员进行定期串行执行。他们还解释了 worker stealing 是如何工作的,小偷瞄准受害者甲板的顶部,使项目出队,并更新甲板的头部和当前堆栈帧指针。生成的函数的结果返回到原始 SIL 代码中的父堆栈帧。

  • 00:55:00 在本节中,演讲者解释了 Cilk 运行时系统使用称为 THC 协议的协议处理涉及多个处理器的并发访问的方法。该协议涉及两组协议,一组用于执行工作的工人,另一组用于小偷。工人的协议通过尝试从甲板底部弹出东西来优化,只有在失败时它才会获得甲板上的锁,而小偷总是在执行任何甲板操作之前抓住锁。然后,窃贼通过调用 long jump 函数,传递从 set jump 函数检索到的堆栈帧缓冲区,并将其寄存器状态设置为被盗 continuation 的状态,神奇地恢复被盗的 continuation。

  • 01:00:00 在本节中,演讲者讨论了 Cilk 运行时系统中设置跳转和跳远调用之间的交互。他们解释了,当一个长跳例程被调用时,它如何有效地第二次从设置跳转返回,这次在第二个参数中指定了不同的值。使用适当的缓冲区和第二个参数,窃贼可以执行长跳转以跳过受害者代码中的特定计算。演讲者指出,可以静态计算跳过呼叫所需的距离并使用不同的方法,但设置跳转和长跳转协议为 Cilk 运行时系统提供了一种更灵活的方法。总的来说,演讲者强调了窃贼可以使用 Cilk 从受害者的甲板上窃取计算的各种技术。

  • 01:05:00 在本节中,讨论了 Cilk 运行时系统对仙人掌堆栈抽象的需求。据解释,使用受害者的堆栈可能会导致受害者堆栈的损坏,并导致在所有工作人员之间保持一致性的问题。因此,需要一个单独的窃贼调用堆栈。仙人掌栈的实现涉及小偷窃取延续并将其栈指针设置为自己的栈,同时仍然能够通过 RB p 的偏移量访问受害者栈上函数的状态。

  • 01:10:00 在本节中,演讲者解释了 SIL 运行时系统如何处理不同处理器上的计算同步。该系统使用仙人掌堆栈和计算树来确保同步只发生在函数下的嵌套子计算之间,而不是一般的程序。当工作人员在所有子计算返回之前到达 silk sync 语句时,它就变成了小偷并继续在被盗计算的堆栈帧上工作。这允许系统避免浪费工人并确保嵌套计算正确同步。系统特别指示编译器不要使用可能与此方法冲突的优化。

  • 01:15:00 在本节中,Cilk 运行时系统被描述为维护一个称为全帧的状态树,它跟踪哪些计算正在等待哪些其他计算完成。该系统使用完整的框架树来跟踪并行执行的信息负载,包括指向父框架的指针,可能指向子框架,以及未完成的子计算如何相互关联。当 worker 遇到 sync 时,如果它有一个未完成的子计算,它就不能同步并且必须暂停它的整个框架,直到它成为一个窃贼来窃取更多的工作。该系统允许程序具有足够的并行性并简化同步协议。

  • 01:20:00 在本节中,演讲者讨论了 Cilk Runtime 系统如何优化执行函数没有未完成子项且系统上所有工作人员都忙于自己的任务的常见情况。运行时系统使用来自关联堆栈帧的标志字段的位来评估同步对于 silk 同步是否必要。演讲者还提到了运行时系统支持的其他几个特性,包括 C++ 异常、reducer hyper objects 和 pedigrees。
 

第 14 讲 缓存和缓存高效算法



第 14 讲 缓存和缓存高效算法

在关于缓存和缓存高效算法的视频中,讲师解释了现代机器的缓存层次结构、完全关联缓存和直接映射缓存之间的区别,以及集合关联缓存的优缺点。该视频还介绍了理想的缓存模型和缓存高效算法的概念。演讲者讨论了缓存未命中引理、高缓存假设和子矩阵缓存引理。他们还分析读取子矩阵和矩阵乘法期间发生的缓存未命中。该视频最后介绍了分块矩阵乘法的概念以及它如何显着提高性能,但也指出它不可移植,并且针对多级缓存进行优化可能代价高昂。

本讲座以递归矩阵乘法为例,介绍了缓存和缓存高效算法。演讲者强调了分别分析工作和缓存未命中的重要性,并指出由于缓存大小不同,缓存感知算法可能并非对所有机器都是最佳的。演讲者还讨论了针对任何缓存大小自动调整的缓存无关算法,并提到了并行缓存无关代码的开发。最后,目标是设计缓存感知或缓存无关的算法,关于缓存无关算法设计的讨论将在下一讲中继续。

  • 00:00:00 在关于缓存和缓存高效算法的这一部分中,讲师讨论了现代机器的缓存层次结构,其中包括每个处理器的私有 L1 数据和指令缓存、私有 L2 缓存和最后一级缓存在所有处理器之间共享。这些高速缓存的大小随着内存层次结构的向上移动而增加,其中 DRAM 速度最慢但最大。随着内存层次结构的向上移动,缓存的关联性和延迟也会增加,其中 L1 缓存速度最快且关联性最强。讲师还提到缓存一致性协议的重要性,以确保并行处理的内存地址访问一致性。了解如何利用程序中的局部性可以帮助有效地使用更快的内存缓存。

  • 00:05:00 在本节中,将讨论完全关联缓存和直接映射缓存之间的区别。完全关联的缓存需要搜索整个缓存才能找到一个块,这可能会很慢且效率低下,因为找到一个块可能需要访问多行。另一方面,直接映射缓存每个块只有一个可能的位置,这使得访问块更快,冲突更少。地址空间的三个组成部分,offset,set,tag在划分虚拟内存地址确定缓存块位置的时候也有说明。标签标识了缓存块对应于哪个内存块,集合决定了块进入缓存中的哪个位置,总地址空间为W位。

  • 00:10:00 在本节中,视频讨论了直接映射缓存的优点和缺点,这种缓存速度很快,因为只需要搜索缓存中的一个位置,但容易出现缓存块不断相互逐出的冲突未命中,降低性能。然后视频介绍了集合关联缓存,这是一种混合解决方案,其中每个集合包含多行,允许每个缓存块在缓存中有多个可能的位置。该视频还提到了不同类型缓存未命中的分类,包括第一次访问缓存块时发生且无法避免的冷未命中。

  • 00:15:00 在本节中,视频讨论了不同类型的缓存未命中,包括容量未命中、冲突未命中和共享未命中。当缓存已满并且无法容纳所有需要访问的缓存块时会发生容量未命中,而当来自同一组的太多块映射到缓存时,在组关联缓存中会发生冲突未命中。最后,当多个处理器正在访问同一个高速缓存行并且至少其中一个正在写入时,在并行上下文中会发生共享未命中。该视频还提供了一个示例,说明在访问以行主顺序存储的较大矩阵中的子矩阵时发生冲突未命中的坏情况。

  • 00:20:00 在本节中,演讲者讨论缓存和缓存高效算法。他们使用访问较大矩阵中的子矩阵的示例,以及当缓存仅为 4 向关联时如何发生冲突未命中。演讲者建议在矩阵中每一行的末尾添加一个恒定量的空间,因此每一行都长于 2^15 字节,这有助于减少冲突未命中。

  • 00:25:00 在本节中,演讲者讨论了理想的缓存模型,该模型用于分析算法的缓存性能。该模型假定一个两级缓存层次结构、一个完全关联的缓存和一个最优的 Nishant 替换策略。演讲者解释说,两个重要的性能指标是 N 的顺序和缓存未命中数,其中理想情况是在不增加传统标准算法的工作量的情况下实现少量缓存未命中。还提到了 1985 年证明的 LRU 引理,它指出在大小为 M 的理想缓存上引发 Q 缓存未命中的算法将在使用 LRU 策略的大小为 2M 的全关联缓存上引发 2Q 缓存未命中。

  • 00:30:00 在本节中,演讲者介绍了缓存高效算法的概念,以及它们如何通过最大限度地减少缓存未命中次数来提高程序性能。他解释了最近最少使用 (LRU) 替换策略,该策略指出使用 LRU 的理想缓存大小两倍的完全关联缓存将导致缓存未命中次数不超过两倍。这允许在执行渐近分析时更容易地分析缓存未命中。演讲者还提出了一个缓存未命中引理,指出如果程序读取一组数据段,其大小小于缓存大小的三分之一,并且其平均大小至少为缓存行的大小,则全部读取它们的缓存未命中最多是段总大小除以缓存行大小的三倍。

  • 00:35:00 在本节中,视频讨论了与缓存相关的两个假设 - 缓存未命中引理和高缓存假设。缓存未命中引理指出,如果数据段的平均长度大于缓存块大小,则缓存未命中数只会增加一个常数因子。高缓存假设假设缓存的高度大于宽度,并且通常在实践中得到满足。该视频还解释了子矩阵缓存引理,该引理讨论了短缓存在将子矩阵拟合到缓存行中的问题,以及为什么高缓存假设有助于解决此问题。

  • 00:40:00 在本节中,演讲者讨论缓存和缓存高效算法。他们分析了将一个方形子矩阵读入缓存时发生的缓存未命中数,并使用缓存未命中引理表明将矩阵的所有元素读入缓存所需的缓存未命中数至多为3n^2/B .然后,他们分析了标准立方工作矩阵乘法算法中发生的缓存未命中数,假设矩阵按行主顺序排列并且满足高缓存假设。他们考虑了三种情况,并对第二种和第三种情况假设了 LRU,最终展示了缓存优化在算法设计中的重要性。

  • 00:45:00 在本节中,演讲者讨论缓存和缓存高效算法。他们分析了矩阵乘法的两种情况,其中 n 大于 B 上的 M 和 n 小于 C 乘以 M 上的 B。他们使用 LRU 替换策略并计算访问矩阵 B 时发生的缓存未命中数。在第一个在这种情况下,他们发现他们需要 n 个立方缓存未命中的 theta,导致算法中的局部性没有任何好处。在第二种情况下,他们可以利用空间局部性并将缓存未命中数减少 B 倍,从而使 n 的立方比 B 缓存未命中数的 theta 更高效。

  • 00:50:00 在视频的这一部分,演讲者讨论了缓存和缓存高效算法。他们首先分析了整个矩阵适合缓存的情况,导致缓存未命中总数为 n 的平方 B。然后他们通过交换内部两个循环的顺序来讨论优化,以受益于空间局部性和将缓存未命中的总数减少到 n 的三次方 B。但是,他们指出,可以通过使用称为平铺的优化来做得更好,其中六个 for 循环用于循环平铺并为每个子完成计算-矩阵,然后再继续下一个。然后对大小为 s × s 的子矩阵的工作进行 s 立方。

  • 00:55:00 在本节中,介绍并详细讨论了分块矩阵乘法的概念。该算法的目标是将子矩阵放入缓存中,以便所有计算都可以在缓存中完成,而不会再有缓存未命中。分析cache miss次数,发现cache miss次数为n/s^3次s^2 /B,共n^3/B * sqrt(M)次cache miss。这个数字对于提高矩阵乘法代码的性能非常重要。然而,该算法出现了一个问题:它不可移植,因为它在很大程度上取决于运行它的机器的缓存大小。此外,多维调整优化变得更加昂贵,并且在针对多级缓存进行优化时代码变得更加混乱。

  • 01:00:00 在本节中,讲座探讨了缓存和缓存高效算法。演讲者讨论了为缓存高效算法调整参数并针对特定机器对其进行优化的挑战。他们引入了一种递归矩阵乘法算法,其中输入矩阵被分成四个子矩阵或象限。对于输出矩阵的每个象限,递归地出现两个矩阵相乘的和。最后,演讲者分析了该算法的工作,并得出结论,这是一个更简单的设计,仍然保持良好的缓存性能。

  • 01:05:00 在本节中,演讲者讨论了缓存和缓存高效算法,并使用矩阵乘法的示例来解释分析工作和缓存未命中之间的区别。演讲者画了一个递归树来可视化大小问题和分支子问题,并注意到直到叶子的层数是 n 的以 2 为底的对数。叶子的数量计算为 n 的以 2 为底的对数 8,这与 n 的立方相同。工作量只是 theta n 的立方,使用不同的基本情况分析缓存未命中,其中子矩阵适合缓存,并使用缓存未命中的 N 平方的 theta。演讲者强调,级别的数量不仅仅是 n 的以 2 为底的对数,还取决于缓存的大小,从而导致不同的叶子数量,即 n 的立方 m 的 theta 的三等分。

  • 01:10:00 在本节中,演讲者以递归矩阵乘法代码为例,解释了如何在高效缓存算法中分析缓存未命中数。该代码是缓存无视的,这意味着它对缓存没有任何明确的了解,并且它会根据其运行的机器的特定缓存大小被动地自动调整自身。演讲者还提到,当今最好的缓存不经意代码适用于任意矩形矩阵,并执行二进制拆分而不是八向拆分。演讲者讨论了并行上下文并解释了如何分析在具有专用缓存的多个处理器上运行的确定性单元计算中的缓存未命中数。
     
  • 01:15:00 在本节中,演讲者讨论缓存和缓存高效算法。通过最小化串行错觉中的缓存未命中,我们基本上可以在低跨度算法的并行执行中最小化它们。演讲者为并行递归矩阵乘法算法提供了缓存未命中边界,这与串行执行相同。目标是设计具有缓存明确知识或缓存无视的算法。演讲者提供了两者的示例,并提到将在下一讲中进一步讨论缓存无关算法设计。
 

第 15 讲缓存无关算法



第 15 讲缓存无关算法

该视频讨论了缓存无关算法,可以自动调整机器上的缓存大小,实现良好的缓存效率,并且不需要知道机器的缓存参数。该视频展示了如何使用模板方法在二维矩阵上通过微分方程编写代码来模拟热扩散。该代码同时具有循环和梯形版本,由于循环代码访问模式的可预测性,后者的缓存效率更高,但在 2D 模拟中速度并不明显。该视频还讨论了缓存和并行之间的相互作用以及潜在并行加速瓶颈的诊断。最后,演讲者解释了模板计算以及数组中的每个点如何使用称为模板的固定模式进行更新,模板可能会遭受缓存未命中,并且可以使用利用时间和空间局部性的高效算法来减少存储需求。

视频的第二部分讨论了用于排序和合并的无缓存算法。具体来说,视频涵盖了归并排序的缓存复杂度,介绍了多路归并的概念,讲解了多路归并算法的缓存复杂度。该视频还讨论了漏斗排序算法,这是一种缓存不经意的排序算法,可以合并 K 个平方元素和 K 个排序列表。漏斗排序算法是最优的,用 K 个漏斗的平方根递归构造。该视频解释了还有许多其他无需缓存的算法和数据结构,例如 B 树、有序文件维护和优先级队列。总的来说,该视频为那些有兴趣进一步了解该主题的人提供了缓存遗忘算法的介绍。

  • 00:00:00 在本节中,视频讨论了缓存无关算法,该算法可以自动调整机器上的缓存大小并实现良好的缓存效率,而代码无需了解任何缓存参数机器。该视频以通过微分方程模拟热扩散的例子来展示科学计算如何经常依赖微分方程来描述物理过程,然后必须编写高效的代码来模拟该过程。该视频演示了如何编写基于有限差分近似法的代码来模拟一维热方程。

  • 00:05:00 在本节中,演讲者将介绍如何为允许使用模板方法进行有限差分近似的仿真方程编写代码。该方程使用 U 相对于 T 和 X 的偏导数,演讲者展示了如何使用近似方法获得这些偏导数。二维空间使用矩阵表示,X 和 T 值分别在水平和垂直方向表示。演讲者解释了矩阵的边界并使用等式计算 U。

  • 00:10:00 在本节中,演示者解释了模板计算,这是一种在科学计算中广泛用于各种目的的方法。在此方法中,数组中的每个点都使用称为模板的固定模式进行更新。计算取决于其他三个值,这被称为三点立场。尽管模板计算可用于较大的 X 值,但性能可能会因缓存而受到影响,从而导致缓存未命中。此外,通过仅存储两行(当前行和先前行)来更新点值,可以减少存储数据所需的存储空间。

  • 00:15:00 有效:在每个时间步长中,我们仅使用前一行的值更新特定行的 X 值。因此,我们可以交替更新哪一行和将哪一行用作前一行,并且只需要保留大约一行额外的值。然而,这段代码的缓存效率不是很高,我们可以使用平铺或缓存无关算法来提高效率。理想的缓存模型假设一个完全关联的缓存具有最优或 LRU 替换策略,并捕获容量未命中但不是串行执行中的冲突未命中。尽管如此,它对于设计利用时间和空间局部性的高效算法仍然很有用。

  • 00:20:00 在本节中,演讲者解释了如何使用缓存遗忘算法来计算 2D 矩阵中梯形空间内的点,而无需查看外部。计算涉及一个内核函数,该函数将指向 UT mod 的指针取为 X,并返回 W 的 0 加上 alpha 乘以负 1 的 W 减去 2 乘以 W 的 0 加上 W 的 1。假设 LRU 替换策略分析了缓存行为,并且高速缓存未命中数是每行 B 上 NT 的 Theta。然而,演讲者指出,通过平铺可以实现更好的性能,但他们继续讨论无缓存版本。梯形在 T1 处有一个顶底,在 T0 处有一个底底。该算法使用涉及 T、t0、t1、X、x0、x1、DX0、DX1 的不等式条件计算梯形内的所有点,其中 DX0 和 DX1 为 -1、0 或 1,表示斜率。

  • 00:25:00 在本节中,演讲者描述了梯形法则算法的分治法。该方法有一个基本情况,其中梯形的高度为 1,并且循环根据计算顺序计算所有值。该算法有两种递归情况,即空间切割和时间切割,它们分别取决于梯形的宽度和高度。当梯形太宽时,应用空间切割,其中梯形被垂直切割,斜率为负一的线穿过梯形的中心。相反,当梯形太高时应用时间切割,并通过中心水平线切割。递归算法进行两次调用,分别以特定顺序遍历梯形的左侧和右侧以及底部和顶部,以防止点之间的相互依赖。

  • 00:30:00 在本节中,演讲者讨论了缓存无关算法的缓存复杂性。他们分析了递归树方法,发现该算法在每个级别将自身分为两个子问题,直到达到基本情况,它表示 HW 点的 theta,其中 H 是 W 的 Theta。每个叶子的缓存未命中数是 theta W在 B 上,叶子的数量是 NT 在 HW 上的 Theta。内部节点对高速缓存的复杂性没有实质性贡献。缓存复杂度泛化为多维,如果 d 为一,则导致 NT 超过 MB。

  • 00:35:00 在本节中,演讲者解释了循环代码和梯形代码之间的区别,梯形代码通过节省 M 因子(其中 M 是缓存行数)具有更好的缓存复杂度。仿真表明,与循环代码相比,梯形代码会导致更少的缓存未命中,从而更快地完成计算。但是,演讲者指出此模拟仅适用于一维,并继续演示二维中发生的情况。

  • 00:40:00 在这一部分中,演示者演示了二维空间中热扩散的实时模拟,其中屏幕上的颜色对应于温度。演示者比较了循环代码和梯形代码的性能,发现虽然梯形代码导致缓存未命中的次数少得多,但它只比循环代码快一点点。建议这可能是由于硬件预取有助于循环代码,因为它的访问模式是规则的且易于预测。

  • 00:45:00 在本节中,演讲者讨论了缓存和并行性之间的相互作用。他们解释了一个定理,即在串行执行中最小化高速缓存未命中可以在假设低跨度算法的情况下在并行执行中最小化它们。然后,他们演示了梯形算法如何使用 V 形切割并行工作,其中两个独立的梯形并行执行,灰色梯形随后执行。他们还提到用于倒梯形的倒置 V 形切割。

  • 00:50:00 在本节中,演讲者讨论了并行循环代码和并行梯形代码与其串行代码相比的性能。尽管具有比梯形代码更多的潜在并行性,但并行循环代码实现的潜在加速比不到一半。这是因为,在并行情况下,存在内存带宽瓶颈,与内存带宽充足的串行情况相比,这会阻止预取帮助并行循环代码。相比之下,梯形代码实现了近线性加速,这与缓存效率在较大输入大小中发挥更大作用的事实一致,其中缓存非常小,与输入大小相比。

  • 00:55:00 在本节中,演讲者讨论了如何诊断并行加速瓶颈并确定可能导致瓶颈的几种情况,例如并行度不足、调度开销、内存带宽不足和争用。他们提出了几种诊断这些问题的方法,包括使用 Silk Scale 来测量代码中的并行性以及执行硬件计数器来测量内存带宽。然而,诊断争用特别具有挑战性,演讲者建议在评估争用是否是问题之前查看前三个潜在原因。最后,演讲者继续讨论模板计算。

  • 01:00:00 在本节中,演讲者通过首先分析合并两个已排序输入数组的复杂性来讨论分析合并排序的缓存复杂性。执行此操作的时间与输入数组大小的总和成正比。合并 n 个元素时发生的缓存未命中数是 B 上的 theta n,其中 B 是缓存行中的字节数。合并排序对一半大小的输入进行两次递归调用,并合并其递归调用的排序输出。归并排序的整体工作是theta of n log n,使用递归树的方法进行分析。还讨论了归并排序的缓存复杂度,表明缓存未命中数与存储数据所需的缓存块数成正比,即 theta n over B log M,其中 M 是最大尺寸 a子块可以有。

  • 01:05:00 在本节中,演讲者解释了归并排序缓存复杂性的重现。基本情况发生在问题大小适合缓存时,导致 Θ(n/B) 缓存未命中。否则,该算法有两个大小为 n/2 和 Θ(n/B) 缓存未命中的递归调用来合并结果。分析涉及递归树的层数,即n/CM的以2为底的对数,其中CM为常数。每片叶子的缓存未命中数是 Θ(M/B * n/CM),给出了 Θ(n/B * log (n/CM)) 的缓存未命中总数,在第一项中节省了 B 因子.然而,对于较大的问题规模,与工作量相比,我们只节省了 B 的一个因子,而对于较小的问题规模,我们节省了 B log n 的一个因子。演讲者询问是否有更好的解决方案,答案总是肯定的。

  • 01:10:00 在本节中,演讲者解释了如何使用多路合并来合并已排序的子数组,并介绍了锦标赛树的思想来确定子数组的最小元素。他们还解释了这种方法的工作和缓存复杂性,这种方法用于缓存不经意的排序算法。多路归并的工作复杂度与二进制归并排序相同,而缓存复杂度由填充锦标赛树和访问输入数组所需的缓存未命中数决定,如果R小于可以优化C*M/B 为小常数 C。

  • 01:15:00 在本节中,演讲者讨论了多路归并排序算法的缓存复杂度,并将其与二进制归并排序算法进行了比较。多路合并排序算法涉及将问题划分为大小为 n/R 的子问题,并支付 n/B 缓存未命中以合并它们。递归树的层数为n/cm的对数底R,叶子大小为cm。 speaker 将 R 设置为等于 M/B 的 theta,使其尽可能大,并分析了缓存复杂度,结果是 thetan n log n 除以 B log M。与二进制归并排序算法相比,多-way 合并排序算法在缓存未命中中节省了 log M 的一个因子。然而,该算法不是缓存无关的,并且需要针对特定机器调整 R 的值,如果其他作业正在运行竞争缓存,这可能会成为问题。

  • 01:20:00 在本节中,演讲者讨论了漏斗排序算法,这是一种缓存遗忘排序算法,可以合并 K 个平方元素和 K 个排序列表。此合并的成本显示与多路合并排序算法的成本相同,但漏斗排序算法是缓存无关的,其边界是最优的。演讲者展示了 K 漏斗的图片,并解释了该算法是使用 K 漏斗的平方根递归构建的,它将元素提供给缓冲区,缓冲区又将元素提供给 K 漏斗的最终输出平方根,从而产生K 漏斗的输出。演讲者还提到还有许多其他缓存无关算法和数据结构,例如 b 树、有序文件维护和优先级队列,并邀请观众在线了解更多信息。
 

第 16 讲。非确定性并行编程



第 16 讲。非确定性并行编程

该视频讨论了与确定性和非确定性并行编程相关的各种概念。演讲者强调了避免非确定性的重要性,因为它会导致异常行为和调试困难。管理非确定性的策略包括使用固定值、记录回放、分析工具、封装和单元测试。详细探讨了互斥体的使用,包括自旋与屈服、可重入与不可重入以及公平与不公平。演讲者还谈到了上下文切换的概念和并行编程上下文中的“滑雪租赁问题”。该部分最后讨论了多核处理器中性能工程的基本原理。

视频的第二部分介绍了并行编程中的死锁问题并提供了避免死锁的解决方案,例如保证没有等待循环的互斥锁的线性排序。此外,演讲者还介绍了事务内存的概念,它通过将关键区域表示为一次提交所有更新的事务来确保原子性。然后,该视频介绍了一种算法,该算法使用基于锁的方法以及有限所有权数组和释放排序 require 方法来防止死锁和饥饿,而无需全局锁。最后解释了release sort and reacquire算法,避免了多个锁同时尝试获取一个锁,避免了性能问题。

  • 00:00:00 在本节中,讲师介绍了编程中的确定性概念及其与并行计算的关系。如果每个内存位置在每次执行中都使用相同的值序列更新,并且程序始终表现相同,则程序是确定性的。确定性程序具有可重复的优点,使它们更容易调试。讲师强调永远不要编写不确定的并行程序的重要性,因为它们会表现出难以调试的异常行为。然而,在实践中,避免并行计算中的非确定性可能具有挑战性。

  • 00:05:00 在本节中,演讲者讨论了非确定性并行编程以及它有时可以带来更好性能的事实,但除非必要,否则仍应避免。如果您必须以这种方式编写程序,演讲者建议设计一种测试策略来管理非确定性。策略的示例包括关闭非确定性、对随机数使用相同的种子或为程序输入提供固定值,否则这些值可能会随机变化。演讲者强调了有一种方法来处理由非确定性引起的程序错误的重要性。

  • 00:10:00 在本节中,演讲者讨论了处理非确定性编程的策略,例如记录重放、封装非确定性、替代确定性替代方案、使用分析工具和单元测试。演讲者强调了控制非确定性以提高发现代码错误的机会的重要性。演讲者还提供了一个在哈希表中使用互斥和原子性的例子来说明如何处理非确定性编程。

  • 00:15:00 在本节中,演讲者讨论了访问同一位置的并行指令如何导致竞争错误并破坏系统的完整性。标准的解决方案是使一些指令成为原子指令,这意味着它们被系统的其余部分视为完全执行或根本不执行。为此,使用互斥锁或互斥锁,它是一个具有锁定和解锁成员函数的对象。每个槽都做成一个结构体,带有互斥锁和指向槽上下文的指针头,允许在访问列表之前使用锁定和解锁功能,从而确保系统的正确性。

  • 00:20:00 在本节中,视频探讨了使用互斥体来实现原子性以及它与确定性竞赛的关系。互斥锁可以确保关键部分是原子的,但由于确定性竞争,结果代码是不确定的,这在某些情况下可能是需要的。该视频强调了理解数据竞争和确定性竞争之间差异的重要性,并指出简单地消除数据竞争并不一定意味着代码中不存在错误。重要的是要有一种方法来检测非确定性,以避免代码中的误报或遗漏竞争错误。

  • 00:25:00 在本节中,演讲者解释说没有数据竞争并不一定意味着代码没有错误。虽然没有数据竞争是代码的积极方面,但提供原子性的锁定示例可能会导致违反原子性。此外,可能会出现良性竞争,例如当两个并行进程访问相同的值时,这可能会导致相同的结果,但也可能导致某些体系结构上的值不正确。演讲者认为,虽然有些人认为良性种族是无害的,但识别和避免它们仍然很重要。

  • 00:30:00 在本节中,演讲者解释了非确定性编程的危险,特别是当多个进程尝试在同一位置设置值时可能发生的竞争条件。演讲者介绍了“Silks”的概念,它允许有意的比赛,但如果使用不当可能会很危险。竞争的复杂性也会使调试变得困难,并混淆旨在帮助检测正确代码的工具。演讲者还讨论了互斥锁的实现及其参数如何影响它们的行为,例如它们是屈服还是自旋。

  • 00:35:00 在本节中,视频解释了并行编程中互斥锁的三个基本属性:自旋与屈服、可重入与不可重入以及公平与不公平。旋转与屈服的概念是这样的想法,即线程不会闲置并不断检查是否可以访问锁,而是将控制权交给操作系统以实现更高效的执行。可重入互斥允许已经持有锁的线程再次获取它,而非可重入锁将在线程试图重新获取它已经拥有的互斥锁时死锁。最后,公平互斥确保等待时间最长的线程最先获得锁,以防止出现饥饿问题的可能性。该视频还提供了一个简单的汇编语言旋转互斥锁的实现。
     
  • 00:40:00 在本节中,视频讨论了并行编程中互斥锁的使用以及为什么使用交换指令而不是仅仅获取互斥锁。说明交换操作类似于权利,为了行使权利,它所在的现金行必须在其他现金行上无效并保持修改或排他状态。这会在内存网络上产生流量并减慢进程。另一方面,通过使用交换指令,缓存行被置于共享状态,从而保持进程更快并产生更少的流量。

  • 00:45:00 在本节中,演讲者讨论了旋转互斥锁和屈服互斥锁之间的区别。在旋转互斥锁中,程序不断检查要解锁的互斥锁,而在屈服互斥锁中,程序将控制权交给操作系统,这允许它安排其他事情。演讲者指出,还有另一种称为竞争互斥体的互斥体,它同时实现了旋转互斥体和产生互斥体的目标。演讲者还探讨了使用消息传递或中断来通知等待线程,但指出更简单的解决方案是使用互斥机制。

  • 00:50:00 在本节中,演讲者解释了上下文切换的概念,即操作系统在可用处理器上的线程之间切换的频率。通常,系统每秒进行大约 100 次上下文切换,这意味着每次切换大约需要 10 毫秒。然而,这比一条简单指令的执行时间要长一个数量级以上,这意味着在线程有机会返回并获取锁之前有许多指令可以执行。这个问题可以通过使用旋转和屈服的组合来解决。演讲者将此称为理论文献中的“滑雪租赁问题”。

  • 00:55:00 在本节中,视频以购买或租赁运动器材为例,讨论了为特定任务决定购买还是租赁设备的过程。演讲者鼓励观众考虑租用与购买的成本,并建议先租用直到成本等于购买的成本,然后再购买。该视频还探讨了上下文切换时间、磁盘访问时间的概念,以及同时持有多个锁时的死锁问题。总的来说,这一部分涵盖了多核处理器性能工程的基本原理。

  • 01:00:00 在本节中,演讲者解释了死锁,即两个线程持有另一个线程都需要的资源,从而导致性能损失。死锁存在三个基本条件:互斥(对资源的独占控制)、不可抢占(资源持有直到完成)、循环等待(一个线程循环等待下一个持有的资源)。删除这些约束中的任何一个都可以解决死锁问题。演讲者使用托尼·霍尔根据埃兹格·迪杰斯特拉的试题讲述的故事“哲学家就餐”来说明死锁问题。故事讲的是哲学家们围坐在一张桌子旁,用筷子吃面条,每盘面条夹在两根筷子之间,吃面条需要两根筷子。哲人左右各拿筷子吃面条。但是,如果他们都拿起左边的筷子,他们就会饿死。

  • 01:05:00 在本节中,演讲者讨论了并行编程中的死锁问题,并提供了一种在避免死锁的同时确保非抢占的解决方案:互斥锁的线性排序。通过对每个互斥量进行编号并根据其编号顺序对其进行策略性锁定,程序员可以保证不会出现等待循环(死锁的必要条件)。但是,他提醒程序员注意运行时系统在 Silk 中对锁的封装,因为通过这些锁引入额外的非确定性会导致麻烦。他分享了一个仅用一个锁就可能发生死锁的例子,强调了在设计并行程序时仔细考虑的重要性。

  • 01:10:00 在本节中,演讲者深入探讨了事务性内存的主题,这是一种最近的研究级技术,涉及在数据库事务中隐式发生原子性,而无需指定锁。演讲者举例说明了事务内存如何在并发图计算中发挥作用,即高斯消元法,其中同时消去两个节点会导致违反原子性。事务内存背后的想法是将关键区域表示为一个事务,并且在提交时,关键区域中的所有内存更新看起来就像是立即发生的一样。

  • 01:15:00 在本节中,演讲者讨论了事务内存中的冲突解决、争用解决、转发进度和吞吐量。他们引入了一种算法,该算法使用基于锁的方法和有限所有权数组以及释放排序 require 方法来防止死锁和饥饿,而无需全局锁。该算法记录读取和写入以允许在事务中止时回滚或原子提交。锁数组用于锁定哈希函数映射到的位置,允许公平获取锁。该算法允许并发事务,而不会牺牲性能或导致死锁。

  • 01:20:00 在本节中,演讲者讨论了一种称为释放排序和重新获取的算法。该算法通过贪婪地尝试获取内存位置的锁来工作,如果发生冲突,事务将回滚而不释放它持有的锁。之后,该算法释放所有索引大于它试图访问的位置的索引的锁。然后它获取所需的锁,最后按排序顺序获取释放的锁。重复此过程,直到交易成功。该算法旨在防止多个锁同时尝试获取锁,这会导致性能问题。
 

第 17 讲 无锁同步



第 17 讲 无锁同步

Charles Leiserson 在 YouTube 视频中探讨了无锁同步的概念。他提供了一个示例,说明需要全局线性指令顺序来确保顺序执行,并讨论如何通过顺序一致性实现互斥,避免使用锁的困难和潜在问题。 Leiserson 介绍了 Peterson 的算法作为一种解决方案,该解决方案仅使用加载和存储操作来访问关键部分而不受并发进程的干扰。该视频还涵盖了由于硬件重新排序而导致的通过内存同步的挑战,以及与其他指令保持相对顺序的内存栅栏的概念。 Leiserson 认为,支持并行机器的顺序一致性是有益的,但对硬件设计者来说很难实现。

视频的第二部分讨论了使用内存栅栏和同步指令来防止并行程序中的错误。演讲者探讨了以隐式或显式方式实现内存栅栏的不同方法,以及在处理处理器不同方面的不同团队之间进行精心设计和协调的重要性。此外,讲师讨论了使用CAS指令作为C11语言标准中无锁算法的一部分,仅使用常量空间来实现n线程无死锁互斥算法的互斥量。 Charles Leiserson 解释了在多线程系统中使用锁的问题以及改用 CAS 的解决方案,因为长时间持有锁的线程可能会阻塞等待访问同一资源的其他线程。此外,该视频强调了比较和交换的 ABA 问题的潜在问题,并鼓励对该主题感兴趣的人了解更多有关无锁算法的信息。

  • 00:00:00 和它们的指令交错以产生所有指令的全局线性顺序。这就是顺序一致性内存模型背后的思想,从理论上讲,这是最重要的内存模型。了解内存模型很重要,因为在没有锁的情况下,并行程序的行为取决于内存模型。讲座中提供的一个示例通过询问两个处理器在并行执行代码后是否有可能都具有 0 值来说明这一点,这取决于所使用的内存模型。

  • 00:05:00 在本节中,Charles Leiserson 讨论了无锁同步的概念,其中加载和存储必须遵循某种全局线性顺序才能按顺序执行。他使用交错各种值的示例来演示可能发生的不同执行路径以及硬件如何做任何它想做的事情。他还解释说,尽管可以有许多不同的方法来交错值,但加载和存储必须看起来好像遵循线性顺序才能使执行保持一致。最终,Leiserson 得出结论,没有以两个值都为零而结束的执行,有趣的是,现代计算机都没有提供顺序一致性。

  • 00:10:00 在本节中,讨论了顺序一致性的概念,其中涉及理解指令与其顺序之间的发生关系、右箭头连接、处理器顺序和指令顺序之间的线性关系。此外,通过考虑顺序一致性并通过使用加载和存储实现共享数据结构,可以在不使用重型指令或锁的情况下实现互斥。讲义描述了使用专门操作(如测试和设置或比较和交换)的方法,但所提供的解决方案避免了构建死锁或使用锁时发生的其他不良情况。

  • 00:15:00 在本节中,Charles Leiserson 解释了 Peterson 的算法,该算法仅使用加载和存储操作在并发程序中实现互斥。该算法允许两个并发进程 Alice 和 Bob 通过使用一组变量和轮流机制在互不干扰的情况下执行代码。该代码确保一次只有一个进程可以进入临界区并修改共享资源。与锁不同的是,Peterson 的算法不依赖于获取或释放锁,而是使用加载和存储来实现互斥。

  • 00:20:00 在本节中,演讲者讨论了 Peterson 在不使用锁的情况下实现临界区互斥的算法。他解释说,一次只有一个人可以进入临界区,算法确保想要进入临界区的人可以进入,如果他们是唯一想进入的人的话。然后演讲者给出了该算法的证明,其中包括假设 Alice 和 Bob 发现自己一起处于临界区并推导出矛盾。证明依赖于“先于发生”关系和执行指令的程序顺序。

  • 00:25:00 在本节中,演讲者解释了无锁同步的过程。他们建立指令链并确保它们以正确的顺序出现以避免同步错误。他们使用 Bob 和 Alice 想要访问共享资源的示例作为演示。他们解释说,在同步时,工程师需要小心并审查“发生在事情之前”以确保它们以正确的顺序发生。

  • 00:30:00 在本节中,Charles Leiserson 讨论了模型检查的概念及其在分析网络、安全和缓存协议中的应用。然后他继续解释 Peterson 算法的特性,该算法保证饥饿自由但不适用于两个以上的进程。 Leiserson 还探讨了通过内存进行同步的挑战,以及现代处理器中缺乏顺序一致性,这导致内存一致性松散和难以正确同步。

  • 00:35:00 在不引起加载延迟问题的情况下重新排序指令是否安全?第二个约束是指令之间没有数据依赖性,这意味着指令不共享数据或使用内存中的相同位置。在这种情况下,重新排序指令可以提高性能并覆盖过载延迟,因为可以更早地完成加载,并且可以在等待结果的同时完成其他工作。了解这些硬件级概念有助于推理软件和优化性能。

  • 00:40:00 在本节中,Charles Leiserson 解释了在没有锁的情况下同步重新排序的概念。他澄清说,只要没有并发,重新排序就可以安全执行,尤其是当处理器正在运行或指令流中存在气泡时。这是因为,在现代处理器中,硬件将数据存储在缓冲区中,并通过直接进入内存系统来绕过负载。但是,如果其中一个商店是正在加载的东西,这种绕过可能会导致正确性问题。

  • 00:45:00 在本节中,Charles Leiserson 解释了硬件重新排序是如何发生的,为什么它不满足顺序一致性,以及 x86 如何具有称为总存储顺序的内存一致性模型,该模型比顺序一致性弱。总存储顺序的规则包括从不对负载重新排序,外部处理器以相同的顺序看到负载,从不对存储重新排序存储,以及只对之前存储到不同位置的负载进行重新排序,而不是对之前的负载进行重新排序相同的位置。锁定指令和内存排序遵循总顺序。

  • 00:50:00 在本节中,演讲者讨论了指令重新排序如何违反顺序一致性并导致不正确的值。硬件和编译器都可能发生重新排序,重要的是要知道锁定指令不会在其他指令之前移动。演讲者还指出,如果货物去往不同的地址,则货物可以先于商店运送。重新排序的危险在 Peterson 的算法中得到了证明,如果发生某些重新排序,该算法可能会完全搞砸。因此,重要的是编写确定性代码以避免在通过内存同步时出现这些问题。

  • 00:55:00 在本节中,Charles Leiserson 讨论了编写并行和并发代码时的重新排序问题,其中避免在存储之前发生特定负载很重要。为了处理这种情况,工程师引入了内存栅栏,它与其他指令保持相对顺序,但代价是增加了复杂性和潜在的错误。 Leiserson 还认为,并行机支持顺序一致性是有益的,但硬件设计人员要完成它是一个挑战。

  • 01:00:00 在本节中,演讲者讨论了内存栅栏和同步指令在防止并行程序遇到错误方面的重要性。演讲者描述了可以实现内存栅栏的不同方式,例如显式地作为指令或隐式地通过其他同步指令(例如锁定和交换)。然而,在某些情况下,内存栅栏实际上会减慢程序速度,这表明在处理处理器不同方面的不同团队之间进行精心设计和协调的重要性。此外,演讲者建议使用 volatile 变量和编译器栅栏,以防止编译器优化掉变量并导致并行代码出错。

  • 01:05:00 在本节中,讲师讨论了C11 语言标准中的无锁同步。该标准定义了一个弱内存模型,它允许将事物声明为原子的,需要使用昂贵的操作,如内存栅栏或用于无死锁互斥算法的原子比较和交换。在这里,CAS 指令(比较和交换)作为无锁算法的一部分进行了探索。该指令检查内存中的值是否与旧值相同,然后再将其交换为新值,所有操作均以原子方式完成。 CAS 的使用允许仅使用常量空间来实现 n 线程无死锁互斥算法的互斥量。一个锁指令,它旋转直到它得到值 true,用于交换 true 说明有人持有锁。

  • 01:10:00 在本节中,Charles Leiserson 教授解释了如何使用比较和交换 (CAS) 来解决同步问题。他演示了如何使用 CAS 作为锁并修复了学生提交的代码中的错误。然后,他提供了一个用于计算数组中值总和的错误代码,这导致了竞争条件。他介绍了互斥锁作为典型的解决方案,并解释了一个线程在获取锁后被换出的潜在问题,导致其他线程等待锁,从而阻碍了进展。

  • 01:15:00 在这一节中,Charles Leiserson 解释了在多线程系统中使用锁的问题以及使用 CAS 代替的解决方案。有了锁,一个线程可能会持有锁很长时间,阻塞等待访问同一资源的其他线程。但是,对于 CAS,在计算温度之后加载 x 之后是存储 x,同时还使用变量 old 和 new 来存储旧结果并将临时结果添加到该旧值以产生一个新值,该新值可以是如果旧值未更改,则换入。 Charles 还提到了比较和交换的 ABA 问题,并鼓励对该主题感兴趣的人学习更多关于无锁算法的知识。
 

第 18 讲。领域特定语言和自动调整



第 18 讲。领域特定语言和自动调整

在此视频中,麻省理工学院 EECS 系的 Saman Amarasignhe 教授讨论了在性能工程中使用领域特定语言 (DSL) 和自动调整的好处。他强调了 DSL 的重要性,DSL 捕获难以用通用语言描述的特定领域,允许程序员利用领域专家的知识来获得更好的性能。 Singh 讨论了使用 DSL 优化图形过程,包括图形分区和图形形状在计算中的重要性。他介绍了用于图像处理的 DSL Halide,它可以实现快速代码优化和跨机器的可移植性。他讨论了 Halide 在 Google 和 Adobe 等各个行业中的使用。最后,他强调了在优化代码时尝试不同方法的重要性,同时关注并行性、局部性和冗余工作。

该视频还讨论了性能工程的挑战,以及为程序高效运行寻找最佳参数的挑战。演讲者建议自动调整可以通过自动搜索大参数空间来找到最优值来解决这个问题。他指出,穷举搜索可能不切实际,基于启发式的解决方案可能不是最优的。自动调整定义了可接受值的空间并根据性能结果进行迭代,可以加快寻找最佳解决方案的过程。演讲者还讨论了自动调整在为 Try 生成调度时的应用,它能够比穷举搜索更有效地生成调度。

  • 00:00:00 在本节中,麻省理工学院 EECS 系教授 Saman Amarasignhe 教授谈论领域特定语言和自动调整。他解释说,这些语言具有工程优势,因为它们捕获了难以用通用语言描述的特定领域。特定领域的语言更容易理解和维护,它们允许程序员利用领域专家的知识来获得真正好的性能。此外,如果构建正确,特定领域的语言可以将较低级别的决定留给编译器,从而使优化过程更加简单。

  • 00:05:00 在本节中,演讲者讨论了领域特定语言 (DSL) 在性能工程中的使用。他们鼓励尽可能将性能留给编译器,并引入三种不同的图形处理编程语言/框架:Graffiti、Halide 和 OpenTuner。他们指出图表无处不在,从谷歌搜索到推荐引擎,并深入研究了谷歌用来对网页进行排名的 PageRank 算法。 PageRank 算法查看网页的邻居并根据它们的影响计算新的排名,显示图形处理在性能工程中的重要性。

  • 00:10:00 在本节中,演讲者讨论了图算法,这可能涉及处理大量数据并迭代整个图的计算。为了优化代码的性能,可以使用 DSL。用于图形处理的算法类型包括拓扑驱动算法,其中整个图形参与计算,以及数据驱动算法,其中处理在图形的小区域或部分上完成。也有多种进行图形反转的方法,每种方法都有不同的结果集。

  • 00:15:00 在本节中,讨论了图形分区的主题,其中将一个大图形分成较小的部分。对图进行分区的优点是它允许并行性,并且还提供了良好的局部性,这意味着正在处理的节点足够小以适合缓存。图分区也会对社交网络图产生影响,因为它们具有幂律关系。这意味着某些节点比其他节点有更多的连接或更大的影响,并且在处理这些图时,某些连接和节点需要给予更多的关注,因为它们很重要。

  • 00:20:00 在本节中,演讲者讨论了图的形状在计算中的重要性,特别是图的大小和位置如何影响并行算法的效率。必须仔细平衡诸如并行性、局部性和所需的额外工作等因素,以实现给定算法的最佳性能,并且必须根据图形类型、算法类型和所使用的硬件选择正确的方法。开发了一种特定领域的语言来将常量算法与处理方法分开,以更好地优化这些因素。

  • 00:25:00 在本节中,演讲者讨论了如何使用领域特定语言 (DSL) 来编写更高级别的代码,使其更简单、更优雅。他们给出了不同算法的示例,以及 DSL 如何提供一种简单的计算方法。此外,演讲者还解释了如何优化 DSL 的调度以获得尽可能快的速度,甚至允许并行处理。代码可以根据图形或机器的变化而变化,但算法保持不变。演讲者强调了 DSL 的重要性,它提供简单的编程,同时又足够强大以生成优化的调度。

  • 00:30:00 在本节中,演讲者讨论了另一种领域特定语言 Halide,它侧重于具有密集规则结构的图像处理。 Halide 有助于减少实现优化性能所需的编程量,并提高程序在不同机器上的可移植性。演讲者提供了一个三乘三的模糊示例来演示 Halide 的工作原理。在通过各种技术的不同组合来优化性能方面,Halide 与前面讨论的图形语言具有相似的模式。

  • 00:35:00 在本节中,演讲者讨论了使用领域特定语言 (DSL) 和自动调整来优化代码性能。他提供了一个图像过滤器的示例,与有效的 C 代码相比,使用称为 Halide 的 DSL 运行速度更快。 Halide 可以将算法与计划解耦,从而可以简单地优化正在执行的纯函数的管道。最后,演讲者强调需要在局部性、并行性和冗余工作之间进行权衡,以从可用计算资源中获得最佳性能。

  • 00:40:00 在本节中,演讲者讨论了局部性在图像处理中的重要性。处理大图像时,应用一次对整个图像进行操作的过滤器效率不高,因为它不适合缓存。相反,演讲者建议将图像分解成更小的部分,并对每个部分应用过滤器,重点关注局部性和并行性。这可以通过使用调度框架并优化计算带宽和存储粒度来实现。它还可能需要一些冗余工作来实现更好的局部性和并行性。

  • 00:45:00 在本节中,演讲者讨论领域特定语言和自动调整,重点是 Halide 的使用。 Halide可以将库函数融合在一起,比调用库更快,因为有更多的局部性。演讲者展示了 Halide 如何优化双边滤波器计算和分割算法等计算过程。在一个例子中,一个用 MATLAB 编写的分割算法,一直在调用手动优化的库,当用 Halide 编写时,速度提高了 70 倍。 Halide 是 Google 的重要组成部分,它已经被集成到 Android 手机中,并被用于 Google Glass。

  • 00:50:00 在本节中,演讲者讨论了使用 Halide 进行前端处理的有效性以及它如何在不同行业中得到广泛应用。与以前的版本相比,Halide 的速度提高了 4-5%,考虑到下载的视频数量,这对谷歌来说意义重大。 Adobe 还宣布整个 Photoshop 滤镜都是用 Halide 编写的。骁龙图像处理器和英特尔也开始使用Halide。演讲者还讨论了 Halide 和 Graph 在自动优化方面的相似之处,从而使性能工程师的工作更加轻松。然而,当涉及到算法优化时,它是一个更高层次的变化,需要特定的上下文知识,因此很难实现自动化。尽管如此,预定语言等工具让用户可以选择尝试不同的方法并获得更好的性能。

  • 00:55:00 在本节中,演讲者讨论了现代计算机系统的复杂性以及如何没有一种正确的代码优化方法。他们强调尝试不同方法的重要性以及缓存、局部性和并行性的重要性。他们还讨论了生物学和物理学等各个领域的人们如何花费大量时间优化代码而不是进行研究,因为由于代码复杂性,他们无法足够快地编写程序。演讲者建议,寻找人们花费大部分时间进行黑客攻击和提出抽象的领域可能是一个值得探索的有趣研究领域。总的来说,优化程序的操作空间包括并行性、局部性和冗余工作,在这个空间中发挥作用对性能工程师来说至关重要。

  • 01:00:00 在本节中,演讲者讨论了性能工程的挑战,其中涉及为程序找到最佳运行的正确参数。他解释说,有许多参数可以调整,例如内存分配和块大小,但很难为每个程序的每个参数确定正确的值。然而,演讲者建议自动调整可以解决这个问题,通过自动搜索大参数空间来找到最优值。他解释说,基于启发式的解决方案涉及针对某些情况的硬编码规则,在大多数情况下都可以工作,但可能并不总是最佳的。演讲者还指出,构建系统模型可能会出现问题,因为该模型并未考虑所有重要因素,这可能会导致次优结果。

  • 01:05:00 在本节中,演讲者讨论了面对不断变化的技术或不断变化的需求寻找最佳解决方案的挑战。他指出,程序员使用各种“启发式”来指导他们的解决方案,通常基于可能不再适用的指导方针或经验法则。一种选择是穷举搜索,但考虑到可能排列的绝对数量,这可能是不切实际的。为解决这个问题,演讲者建议使用自动调优来修剪搜索空间并更快地找到最佳解决方案。自动调整涉及定义可接受值的空间、随机选择要测试的值,然后根据性能结果进行迭代。 OpenTuner 是一个框架的例子,它使用了一系列技术,例如爬山者和随机搜索,来加速这个迭代过程。
     
  • 01:10:00 在本节中,演讲者讨论了自动调整的概念以及如何将其应用于为 Try 生成计划。通过了解所涉及的图表和节奏,可以使用自动调整比穷举搜索更有效地生成时间表。这种方法能够在不到两个小时的时间内生成时间表,在某些情况下,甚至比最初认为的最佳时间表还要好。
 

第 19 讲 Leiserchess Codewalk



第 19 讲 Leiserchess Codewalk

在这个名为“19. Leiserchess Codewalk”的 YouTube 视频中,Helen Xu 解释了 Lesierchess 的规则,Lesierchess 是一种由两支球队进行的游戏,目标是射击对方的王或射中你的王。该游戏有两种类型的动作,基本动作和重击动作,以及一个 Ko 规则,如果它撤消了对手最近的动作,则该动作是非法的。 Xu 深入探讨了玩游戏的各个方面,包括 Fisher 时间控制方法、Forsythe-Edwards 符号、云自动测试器和项目组织,使用 Elo 评级、移动生成、静态评估和搜索算法(如 alpha-beta)评估和比较机器人,原理变化、子树修剪和转置表。她还讨论了测试基础架构对于优化程序和 eval.c 文件的重要性,该文件包含根据棋子类型及其颜色评估棋盘上每个方块的试探法。

演讲者还深入探讨了游戏的激光覆盖方面,解释了使用 while-true 语句根据玩家的颜色为一个位置生成所有可能移动的复杂系统。他们还解释了代码正确性的重要性以及如何对其进行测试,建议在性能优化之前转换表示以确保正确性。演讲者还讨论了 Leiserchess UCI 界面提供的极大灵活性,它允许用户根据自己的喜好编辑表格和命令,并向观众保证死代码将被清理,并且应报告任何其他错误以进行修复。

  • 00:00:00 在本节中,Helen 介绍了 Lesierchess 游戏的规则以及如何玩。游戏由橙色和淡紫色两队进行,每队有七个棋子和一个国王。游戏的目标是射击对方球队的国王或让您的国王射门。游戏有两种类型的动作,基本动作和特警动作。该游戏还有一个 Ko 规则,如果它只是通过回到对方所在的位置来撤销对手最近的动作,则该动作是非法的。如果每队已经进行了 50 步而没有棋子被摧毁,相同的位置会重复出现,或者两队同意平局,则会发生平局。

  • 00:05:00 在本节中,演讲者解释了 leiserchess 中使用的 Fisher 时间控制方法。每个玩家从初始时间预算和时间增量开始。在所使用的示例中,每个玩家开始时有 60 秒,当他们结束移动时,他们会获得 0.5 秒的额外时间。但实际时间限制可以是任何时间。然后,演讲者通过要求听众建议 tangerine 在 F5 中消灭棋子同时避免反击的动作来演示如何玩 leiserchess。但是,演讲者警告游戏的微妙之处,例如天真地杀死所有棋子,因为这可能会打开对手。
     
  • 00:10:00 在本节中,Helen Xu 讨论了 Forsythe-Edwards Notation 作为一种使用字符串表示国际象棋位置的工具,这对于调试目的很有用,可以让人们轻松回到特定位置。她还解释了如何阅读国际象棋比赛的日志,她在其中分解了每一步棋及其相应的注释。此外,她还演示了如何使用混战服务器与班级中的其他团队一起测试机器人,以及如何挑战其他团队并查看已进行的比赛。

  • 00:15:00 在本节中,Helen Xu 讨论了 Lesierchess 的云自动测试器和项目组织。虽然混战服务器一次只允许一个游戏,但云自动测试器可以在一个时间控制上运行多个游戏和二进制文件,可以根据每个用户的偏好进行自定义。 repo 中的项目组织包括 doc、auto tester、pgnstates、tests、player 和 webgui 目录。 auto tester是一个Java本地自动测试器,用于测试本地的变化,而在tests目录下,可以指定配置进行自动测试。 Helen Xu 还介绍了通用胸部接口 (UCI),这是 Lesierchess 用于与自动测试仪接口的通信协议。

  • 00:20:00 在本节中,演讲者讨论了如何使用 Elo 评级来衡量和比较机器人,Elo 评级是一种基于零和游戏中相对技能水平的评级系统。 Elo 评级不仅基于时间,还基于使用 UCI 搜索的每秒节点数。然后,演讲者深入探讨了有关玩游戏的更多细节,例如生成移动、棋盘表示和用于存储位置的结构。此外,移动使用 28 位表示,其中包括棋子类型、方向、从方格、中间方格和两个方格。参考植入通过遍历棋盘并从该特定棋子生成所有移动,根据轮到谁来生成所有移动。演讲者提到使用调试功能 perft 来确保修改后的移动生成从每个位置返回相同的移动。

  • 00:25:00 在本节中,演讲者讨论如何通过静态评估确定一步棋是否正确,静态评估会使用启发式方法根据位置生成分数。启发式包括关注国王、兵和距离的启发式,可以帮助确定一个位置是否合适。演讲者还谈到了玩游戏的程序如何使用搜索树来玩游戏并根据评估选择最佳着法。为了减少节点数量,演讲者详细介绍了静态搜索和最小-最大搜索,这可以提高评估的节点数量并带来更好的性能。

  • 00:30:00 在本节中,演讲者讨论了称为 alpha beta 的算法,该算法在从具有窗口 alpha beta 的节点进行搜索时使用。如果搜索的值低于 alpha,则此步不够好,您将继续搜索。 Beta 本质上意味着一方试图最大化,另一方试图最小化。因此,如果该值低于 beta,那么您会生成一个 beta 截止值,因为对手不会让您走那一步,因为这太好了。然后演讲者解释了变体搜索的原理,它假设第一步是最好的,然后你运行侦察搜索,也称为零窗口搜索,对剩余的动作进行验证,以验证它们是否更差。这种搜索方式是 alpha-beta 算法的变体。

  • 00:35:00 在本节中,讨论了极小极大搜索算法中子树修剪的概念。子树修剪背后的想法是删除分数低于目前找到的最佳分数的子树。这减少了评估的节点数量并加快了搜索过程。对手也寻找最佳着法并尽量减少玩家的分数。最大化玩家得分和最小化对手得分之间的平衡是至关重要的,目标是找到对玩家有利且对手允许的动作。

  • 00:40:00 在本节中,Helen Xu 解释了主要变异修剪的概念,其中假设最左边的子树是最佳路径,如果假设为真,则采用早期退出。如果假设为假,则必须搜索整个子树。侦察搜索用于递归地将其应用于较低的子树,并使用初始传递来验证假设。这种方法改进了剪枝并用零窗口搜索处理了大部分博弈树。

  • 00:45:00 在本节中,我们将了解 Leiserchess 程序的搜索优化。最重要的优化之一是使用换位表来存储以前遇到的位置,这通过避免不必要的搜索来节省时间。其他优化包括使用 Zobrist 散列为每个位置生成唯一的散列,杀手移动表存储好的移动,因此它们不必重新计算,以及无用修剪以跳过不会增加 alpha 分数的探索移动。还建议实施更好的开局书,以在游戏开始时存储预先计算的位置并节省搜索时间。

  • 00:50:00 在本节中,演讲者讨论了一些用于优化国际象棋程序的有用工具,包括可以预先计算的开卷和残局数据库。测试并拥有良好的测试基础设施非常重要,这样才能在不调试问题的情况下进行创新和取得进展。在并行编程中使用换位表使得能够将其关闭以用于测试目的很重要,并且建议在测试时进行固定深度搜索。总的来说,拥有良好的测试基础设施对于取得进展和避免重大调试问题至关重要。

  • 00:55:00 在本节中,演讲者讨论了 Leiserchess 项目中的 eval.c 文件,以及由于其大小和复杂性,乍一看似乎让人不知所措。然而,随着用户越来越熟悉它,他们将获得处理一段合适大小的代码的信心。 eval.c 文件包含启发式算法,例如 p between、p central、k face 和 k aggressive,它们根据棋子的类型及其颜色评估棋盘上的每个方块。 p between heuristic 决定兵是否在两个国王之间,而 p central 根据兵离棋盘中心的距离给予奖励。 k face 和 k aggressive heuristics 根据国王的方向及其与对手和棋盘边缘的距离给予奖励。

  • 01:00:00 在本节中,演讲者解释了激光覆盖,这是游戏的一个复杂方面。激光覆盖会根据玩家的颜色生成特定位置的所有可能移动。然后它提供了一个移动列表,演讲者详细说明了这张地图如何使用 while-true 语句执行其功能。激光路径在绘制路径时会从一些棋子上反弹并增加每个方块的距离。生成地图后,重复该过程,距离除以与激光的实际距离。这个复杂的系统优化了寻找棋盘上每一块棋子的评估方法所需的迭代,这会减慢过程,发言人补充说,以不同方式表示棋盘并断言两个转换函数产生相同的结果是一个很好的方法优化决策。

  • 01:05:00 在视频的这一部分,演讲者讨论了保持代码正确性的重要性以及如何对其进行测试。他们解释说,从一种表示形式转换为另一种表示形式会减慢过程,但在优化性能之前确保正确性是必要的。测试正确性的一种方法是跟踪节点计数并确保它们在进行更改后保持不变。他们还演示了如何运行代码并在 Lesierchess.c 中显示 UCI 接口,它允许您为各种事物设置值,而不必每次都重新编译二进制文件。最后,他们查看启发式表格并解释如何为自动测试器指定值。

  • 01:10:00 在本节中,演讲者讨论了 Leiserchess 游戏为通过 UCI 界面编辑表格和命令提供的巨大灵活性。这允许用户访问和实施他们想要的任何命令,包括新的启发式方法,并控制解析和实施。尽管存在一些死代码,教授向观众保证它会被清理,并且应该报告任何其他错误以进行修复。最后,他表示虽然该项目可能不会每一分钟都很有趣,但它总体上提供了很多乐趣。
 

第 20 讲 思辨平行论和 Leiserchess



20. 推测平行论和 Leiserchess

在这个名为“20. Speculative Parallelism & Leiserchess”的 YouTube 视频中,讲师介绍了推测并行性的概念,它本质上是先发制人地猜测某些任务可以并行执行并且可以产生更快的代码。但是,演讲者警告说此代码是不确定的,只应在必要时使用,同时还警告不要在可以使用更好的串行代码的情况下使用它。视频的很大一部分围绕着讨论并行 alpha-beta 搜索,其中涉及修剪游戏树以优化搜索时间,还讨论了搜索算法评估过程中使用的不同数据结构和启发式方法,特别是避免循环和静止搜索。该视频还谈到了迭代加深的好处以及它如何为搜索带来更好的移动排序,同时还讨论了 Zobrist 哈希,这是一种用于搜索算法的优化技术,涉及棋盘上具有相同棋子的每个位置的唯一哈希值。

该视频还讨论了国际象棋引擎的各种优化技术,例如换位表、后期移动减少以及使用位板生成移动。演讲者还谈到了“推测性并行性和 Leiserchess”的主题,他建议程序员评估移动是否影响激光的路径并追求“激光覆盖”。演讲者建议在代码中保留旧的表示,并使用程序来测试更改。他们还开发了一种启发式方法,用于测量激光与 Leiserchess 中的国王有多近。更多优化建议包括寻找更好的方法来评估对手与玩家激光的接近程度以及优化动作排序。最后,讨论了正确重构和测试代码的重要性。

  • 00:00:00 在本节中,讲师介绍了推测并行性的概念,作为优化代码并使其运行速度更快的一种方式。这涉及猜测某些任务可以并行执行,即使它们本质上是串行的,如果猜测结果不正确,这可能会导致一些工作浪费。讲师提供了一个对总和设置阈值的示例,并展示了一个简单的优化方法,即如果部分总和超过阈值则提前退出,尽管这会引入一个可预测的分支,该分支可能仍会减慢代码速度。

  • 00:05:00 在本节中,演讲者讨论了如何缓解在并行操作时向内循环添加过多内容的问题。他们解释说,条带挖掘可用于将 n 次迭代的循环替换为 n 次超过四次迭代的循环和四次迭代的内部循环,同时还检查是否每四次迭代都超过阈值以最小化成本支票。为了并行化一个短路循环,演讲者在代码中添加了一个中止标志,并递归地使用它来检查总和是否大于限制并且在将标志设置为真之前没有被中止。通过在设置之前检查标志,他们避免了确定性竞赛并防止了真正的共享竞赛。

  • 00:10:00 在本节中,视频讨论了推测并行性,即程序预测它将需要执行并行工作并抢先工作。此代码是不确定的,只应在必要时用于性能目的。必须重置中止标志并且不要产生推测性工作,除非几乎没有其他并行机会并且很有可能需要它。该视频警告在可以使用更好的串行代码的情况下不要使用推测并行性,因为这种方法通常会导致更多的工作而没有加速。最后,引用了一个定理,它概述了对于并行性,不必要的工作的机会必须小于使用的处理器数量。

  • 00:15:00 在本节中,讨论围绕并行 alpha-beta 搜索展开,其中涉及修剪博弈树以优化搜索时间。 Burkhardt Manin 和他的学生观察到,在一棵最佳排序的树中,每个节点的度数要么为 1,要么为最大。他们的想法是推测在第一个孩子未能生成 beta 截止值后选择了最佳着法。这使得可以并行搜索剩余的孩子,而不会浪费任何工作。代码中的启发式有助于确保事情按正确的顺序完成,例如使用年轻的兄弟姐妹权重算法来选择最佳移动,即使它是较浅的深度。当证明不必要时,该算法还会中止子计算。

  • 00:20:00 在视频的这一部分,演讲者讨论了定期爬上树以检查并行中止的祖先并避免浪费工作的机制。他们建议使用计数器或巫毒参数来确定检查的频率,因为拔树是有成本的。演讲者还谈到了搜索中使用的数据结构,例如可能导致并行化竞争的转置表。他们建议为每个工作人员复制它或锁定它以避免数据竞争。最后,演讲者建议有一种方法可以关闭代码的推测和其他非确定性部分,以便更轻松地进行调试。

  • 00:25:00 在这一部分,演讲者谈到了一个程序,该程序几乎赢得了 1999 年世界计算机象棋锦标赛的冠军。他们提出了一个大家都同意的规则更改,但后来他们输给了著名的 IBM 计算机深蓝。他们在桑迪亚国家实验室的 1824 节点超级计算机上运行,他们唯一的损失是深蓝。他们决定让他们的程序中的转置表存在竞争,但不包括访问表的锁,因为这会减慢程序的速度。他们计算出,一场比赛影响他们选择的价值并最终影响锦标赛结果的几率很低。

  • 00:30:00 在视频的这一部分,演讲者讨论了三种对国际象棋 AI 决策很重要的数据结构。第一个是“杀手级”启发式算法,它跟踪给定代码深度的最佳移动,并且在本质上倾向于重复。第二个是“最佳着法”表,它根据统计数据对所有顺序较低的着法进行排序。两种数据结构都是共享的,在并行化代码时需要妥善管理。最终的数据结构是开局书,它在游戏开始时预先计算出最佳着法。然而,演讲者表示,悬而未决的结果比开场白要少,而且从统计数据来看,大多数游戏的持续时间都不足以让开场白发挥作用。

  • 00:35:00 在本节中,演讲者讨论了在 Leiserchess 中构建开局书籍的策略,Leiserchess 是一款团队尝试创建最强的国际象棋机器人的游戏。演讲者指出,一些团队通过创建强大的开局书取得了成功,这使他们能够通过出色的开局取胜。他们还建议,为每一方保留单独的开篇书比双方都使用一本更有效。此外,演讲者建议为代码添加轻微的随机性以避免可预测性,但警告说在 beta one 期间没有必要为此进行优化。最后,他们提出了迭代加深的策略,该策略涉及在不同深度执行搜索,直到时间控制到期。演讲者指出,这是一个很好的策略,因为每个深度的工作量呈指数增长,但重要信息是在前几个深度积累的。

  • 00:40:00 在本节中,视频深入探讨了迭代加深的好处,以及它如何为搜索带来更好的移动排序。通过迭代加深,转置表可以存储所有中间位置的最佳走法排序信息,使得剪枝在更深的深度更有效。此外,通过迭代深化,它还提供了一个很好的时间控制机制。该视频还涉及游戏数据库以及为什么创建残局数据库是有益的,同时还讨论了如何通过存储交配距离而不是简单的布尔值来避免在残局数据库中循环。

  • 00:45:00 在本节中,演讲者讨论了搜索算法评估过程中使用的不同启发式方法,特别是在避免循环和静止搜索方面。演讲者通过搜索比当前距离小 1 的获胜距离,提到了保持距离获胜和避免骑车的重要性。另一种使用的启发式方法是移动修剪,静止不动通常不如移动,以及不移动修剪,当位置非常好时,应用空移动来简化搜索,即使什么都不做也会导致获胜.演讲者还讨论了国际象棋中的 Zoogs Wang,以及当只有一个强制移动时如何在国际象棋谎言搜索中使用移动扩展。

  • 00:50:00 在这一节中,演讲者讨论了转置表在搜索算法中的用法,它实际上是一个 dag(有向无环图),因为可以通过转置移动到达相同的位置。换位表存储由搜索深度确定的质量分数,以确定移动的价值,以避免再次搜索相同的位置。重要的是不要使用质量太低的移动,因为它不允许进行全面搜索,并且存储的值可能不太准确。此外,特殊代码用于存储通过从深度中减去一些非常大的数字计算得出的配合位置以获得配合。还讨论了 Zobrist 散列,一种用于搜索算法的优化技术,涉及棋盘上具有相同棋子的每个位置的唯一散列值。

  • 00:55:00 在本节中,Leiserson 教授解释了 Zobrist 散列的概念,该散列用于有效地更新代表棋盘上不同棋子位置的散列函数。哈希函数涉及生成一个随机数表,该表对应于棋子类型、行、列和方向的不同组合。散列函数使用异或运算通过对所有片段的值及其方向进行异或来计算散列。 Zobrist 散列利用 XOR 属性从散列函数中删除片段,方法是对要删除的片段的值进行异或运算以获得剩余片段的哈希值。这允许对任何移动仅使用两个 XOR 操作来廉价且高效地更新哈希函数。

  • 01:00:00 在本节中,演讲者讨论了国际象棋引擎的各种优化技术。他谈到了换位表,它存储着一个移动的 Zobrist 键、分数、质量和绑定类型的记录,并使旧的移动过时。此外,他还提到了后期移动减少的概念,即不太深入地搜索不太有希望的移动以节省时间。演讲者还讨论了棋盘的表示,以及使用位棋盘如何通过使用位技巧有效地执行移位和计数位等操作来加快移动生成和其他国际象棋概念。

  • 01:05:00 在本节中,演讲者讨论了“推测并行性和Leiserchess”的主题。他解释说,在处理激光时可以进行的主要优化之一是评估移动是否会影响激光的路径。此外,演讲者建议采用“激光覆盖”,因为它非常昂贵。他还建议程序员在代码中保留旧的表示形式,并加入断言表明事物是等价的,并使用 Perfectiy 等程序来判断他们是否进行了任何更改。最后,他讨论了该程序在让激光接近国王方面应该如何运作以及位置在游戏中的重要性。

  • 01:10:00 在本节中,演讲者讨论了他们开发的一种新启发式方法,用于测量激光与 Leiserchess 中对手国王的距离。他们通过计算激光与国王的距离来评估每一步,为它移动的每个位置计算一个,如果它从对手身上反弹则计算一个额外的值。他们采用到达每个方格的最小数量,并使用乘数来衡量靠近国王的好坏程度。他们将所有值相加并乘以一个神奇的常量乘数,以将值表示为棋子的分数。由此产生的数字平均可达四左右。

  • 01:15:00 在本节中,演讲者讨论了提高国际象棋引擎效率的各种方法。一个建议是找到一种更好的方法来评估对手与玩家激光的接近程度,因为当前的方法在计算上非常昂贵。另一个优化领域是对动作进行排序,这也可能很昂贵,尤其是在有许多动作需要筛选的情况下。演讲者建议找到一种优化排序的方法,以便只对相关的移动进行排序,避免不必要的排序。演讲者还提到,改变董事会的代表可能是一个痛苦的过程,但有一些替代董事会代表的方法可能更有效。
     
  • 01:20:00 在本节中,视频讨论了重构代码并对其进行适当测试以确保在不破坏代码的情况下正确进行更改的重要性。演讲者建议在修改函数调用之前重构板对函数调用的访问,以便在不进行大量重构的情况下更轻松地更改板表示。适当的测试对于确保更改是正确的并且不会破坏代码也是必不可少的,并且使代码具有确定性以避免不可预测性也很重要。演讲者还提到了 John Bentley 即将举行的讲座,这是一次结识名人和了解该领域更多信息的宝贵机会。