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

 

MIT 6.172 软件系统性能工程,2018 年秋季 - 1. 简介和矩阵乘法



一、简介与矩阵乘法

在这个名为“1. 介绍和矩阵乘法”的 YouTube 视频中,讲师讨论了性能工程的重要性以及它是如何随着时间的推移而演变的。演讲者以矩阵乘法为例,展示了编码技术和机器规格如何极大地影响性能。讨论涵盖循环顺序、高速缓存使用和并行编程等主题。演讲者还探讨了针对不同处理器和算术计算优化代码的方法。总的来说,该视频提供了对性能工程领域及其在现代计算系统中的实际应用的宝贵见解。

  • 00:00:00 在本节中,讲师讨论了性能工程的重要性以及研究它的原因。在软件开发方面,性能并不总是最优先考虑的。然而,它是计算的货币,用于购买其他所需的属性,例如易于编程、安全性和正确性。性能下降会导致软件无法使用,而人们对计算系统的主要抱怨是它们太慢了。因此,虽然性能可能没有内在价值,但它有助于开发人员关心的事情。

  • 00:05:00 在本节中,演讲者讨论了计算性能工程的历史,以及从早期由于机器资源有限而导致的密集性能工程到芯片密度每两年翻一番的摩尔定律时代的转变让硬件赶上比优化软件更有利。然而,演讲者指出,通过降低功耗允许时钟速度变大的 Dennard 缩放在 2004 年结束,因此对性能工程的需求比以往任何时候都更加重要。演讲者引述了计算机科学家 Donald Knuth、Bill Wolfe 和 Michael Jackson 关于可读代码的重要性以及对过早优化的警告的引述。
     
  • 00:10:00 在本节中,演讲者讨论了在 2000 年代初期由于功率密度而达到的时钟速度限制,这导致了多核处理器的发展以扩展性能。然而,这意味着性能不再是免费的,需要并行编程,而这在业界是前所未有的。从那时起,软件必须适应新的硬件配置才能发挥作用,因此,软件开发工作更加关注性能。

  • 00:15:00 在本节中,演讲者首先解释了学习如何编写高效使用现代硬件的软件的实践和理论重要性。然后,他们使用经过充分研究的矩阵乘法问题提供了一个性能工程示例,讨论了他们将使用的机器,包括其规格和功能,例如并行处理、高速缓存大小和内存容量,最后解释了用于矩阵乘法的 Python 基本代码。演讲者强调了机器的强大功能以及利用其功能开展有趣项目的潜力。

  • 00:20:00 在本节中,讲师讨论了 Python、Java 和 C++ 在矩阵乘法方面的性能。讲座演示了 Python 对于大型矩阵乘法来说太慢了,运行时间大约为 21,000 秒,Java 更快,比 Python 快了近 9 倍,而 C++ 最快,运行时间大约为 1,100 秒,比 Java 快 2 倍,比 Python 快 18 倍。

  • 00:25:00 在本节中,演讲者讨论了解释型语言(如 Python)、编译型语言(如 C)以及编译为字节码然后解释的语言(如 Java)之间的性能差异。像 Python 这样的解释器用途广泛但速度很慢,而像 C 这样的编译器则利用硬件和机器指令来优化性能。 JIT 编译器,与 Java 中使用的编译器一样,通过仅编译最常执行的代码片段来恢复一些性能。讲者指出,Python 的性能模型很难理解,这就是为什么他们在课程中会使用 C 来进行性能比较。但是,将有一个客座讲座,他们将讨论使用托管语言(如 Python)进行的性能工程。

  • 00:30:00 在本节中,讨论了循环顺序对于矩阵乘法性能的重要性。循环的顺序影响缓存局部性,进而影响运行时间。矩阵在 C 中按行优先顺序排列在内存中,在 Fortran 中按列优先顺序排列。 ijk 阶的访问模式对 C 具有良好的空间局部性,但对 B 具有较差的空间局部性。工具“cachegrind”可用于确定代码的未命中率,调整编译器标志等简单更改也可以提高性能。

  • 00:35:00 在本节中,演讲者讨论了如何优化代码以获得更好的机器性能。选择一个好的优化标志很重要,例如 -O2 或 -O3,但有时较低的优化标志实际上可以更好地优化,具体取决于代码。此外,多核处理器可以与使用 silk 基础设施的并行循环一起使用,从而在 18 个内核上实现近 18 倍的加速。演讲者强调并行化外部循环比并行化内部循环更有效,后者实际上会减慢程序速度。但是,仍然存在优化机会来源,例如缓存未命中和未有效使用矢量化指令。

  • 00:40:00 在本节中,演讲者讨论了如何通过重组计算以尽可能重用缓存中的数据来更好地管理缓存未命中。通过使用分块方案,矩阵被分解成更小的子矩阵并在块中相乘以减少所需的内存访问次数。演讲者解释说,调整参数对于确定子矩阵的大小是必要的,并建议实验是找到最佳值的最佳方法。通过这种方法,演讲者证明可以显着减少计算相同大小占用空间所需的内存访问次数,从而提高计算效率。

  • 00:45:00 在本节中,演讲者讨论了在执行矩阵乘法时使用分块或分块的好处,例如改进的缓存使用率和更快的运行时间。他们解释了芯片具有的不同级别的缓存,以及程序员如何使用调整参数来优化每个缓存级别的代码。然而,每一级缓存的调优过程变得更加复杂,代码也变得难以阅读和理解。演讲者建议采用分而治之的方法,使用并行编程递归地解决较小的子问题。尽管此方法提高了缓存使用率,但函数调用的开销会减慢计算速度,突出显示需要巧妙的性能工程。

  • 00:50:00 在本节中,演讲者讨论了使用分而治之技术优化矩阵乘法以及调整使用该算法的阈值。当阈值低于某个数字时设置一个基本情况,该算法使用普通矩阵乘法。发现基本情况的最佳值为 32,导致 1.3 秒的运行时间和 12% 的峰值性能。此外,演讲者还介绍了使用向量硬件进行向量化的概念,它以单条指令、多种数据格式处理数据。演讲者概述了优化矢量化的不同方法,包括使用矢量化报告、特定体系结构的标志以及允许编译器重新排序关联的快速数学标志。在使用任何编译器矢量化器时,使用原生架构和快速数学会使性能翻倍。

  • 00:55:00 在视频的这一部分,演讲者讨论了用于算术计算的各种位大小,其中 64 位是线性代数计算中最常见的。他们还提到,较低精度的算法可用于人工智能应用程序以提高性能。演讲者继续谈论使用矢量指令和 AVX 内在函数,它们提供高达 41% 的峰值性能和大约 50,000 的加速。他们警告说,在其他领域可能看不到与矩阵乘法类似的性能改进,但本课程将教学生如何自行实现实质性的性能改进。此外,该课程将侧重于多核计算,而不是 GPU 或其他领域。
 

第 2 讲。Bentley 工作优化规则



2. 优化工作的 Bentley 规则

这段 YouTube 视频讨论了计算机程序的各种优化技术。介绍了用于优化工作的 Bentley 规则,并将优化分为数据结构、循环、逻辑和函数。讨论了编码值、数据结构扩充、预计算、缓存和利用稀疏矩阵等不同技术。演讲者还谈到了在图形程序中使用稀疏矩阵表示的好处、逻辑优化和碰撞检测优化。实施这些优化技术有助于减少程序的运行时间,从而提高它们的效率。

视频的第二部分涵盖了几类优化技术,包括循环提升、循环中哨兵的使用、循环展开和融合以及函数内联。演讲者反对过早优化,并强调保持正确性和使用回归测试的重要性。该视频还概述了 Bentley 工作优化规则,这是一个提高生产力和高效实现目标的六步指南。这些规则包括建立明确的目标、分解任务、计划和组织、确定任务的优先级、尽量减少干扰,以及定期审查和调整自己的方法。

  • 00:00:00 在本节中,讲师介绍了优化计算机程序工作的概念,并解释了减少程序工作如何提高其性能。他还谈到了优化工作的 Bentley 规则,该规则以 John Lewis Bentley 的名字命名,他写了一本关于编写高效程序的书。优化分为四类:数据结构、循环、逻辑和函数,讲师计划在整个课程的一系列小型讲座中涵盖所有 22 条规则。虽然减少程序的工作是改善其运行时间的一个很好的启发式方法,但计算机硬件的复杂性意味着它并不总是转化为运行时间的减少,因此讲师还将在稍后的章节中介绍特定于体系结构的优化课程。

  • 00:05:00 在视频的这一部分,演讲者讨论了打包和编码数据结构的概念,以在机器字中存储多个数据值,并将数据值转换为需要更少位的表示形式。通过减少在内存中移动的东西的数量,它成为减少程序运行时间的一个很好的启发式方法。演讲者提供了一个编码日期的示例,以使用更少的位来存储它们,从而更容易获取特定日期的月、年或日。演讲者建议使用 C 中的位域功能来存储结构化数据,使其更易于访问。这种表示需要稍微多一点的位,但在访问结构中的特定数据值时效率更高。

  • 00:10:00 在本节中,视频讨论了三种优化技术。第一种技术是决定是将值编码为连续整数还是将它们解包以便更快地访问。第二种技术是数据结构扩充,向数据结构添加信息可以优化常见操作。一个示例是向单链表添加尾指针,以更有效地附加两个列表。第三种技术是预计算,即提前进行计算,避免在关键时刻进行计算。一个例子是使用 Pascal 的三角形来预先计算二项式系数以加速使用它们的程序。

  • 00:15:00 在本节中,演讲者讨论了如何使用递归公式和 C 代码预先计算帕斯卡三角形。他们解释说递归公式涉及调用 choose 函数,以及如何在运行时使用循环预先计算表。此外,他们还讨论了如何通过将表值放入源代码来在编译时初始化表,从而节省程序执行时间。最后,他们提供了一个可在程序执行期间访问的最多为 10 的二项式系数的示例表。

  • 00:20:00 在本节中,演讲者介绍了元编程的概念,作为一种避免在程序中手动输入大量常量值表的方法。通过编写生成必要代码的程序,可以自动完成繁琐的任务。演讲者提供了一个 Python 代码片段作为示例。还介绍了缓存主题,作为一种通过将最近访问的结果存储在缓存中来避免重复昂贵计算的技术。给出的示例是使用平方根运算符计算直角三角形的斜边,其中缓存预先存储以前的斜边以及 a 和 b 的值。 hypotenuse 函数首先检查输入值是否与缓存中的值匹配,如果匹配,则返回缓存值而无需重新计算平方根。

  • 00:25:00 在本节中,演讲者讨论了缓存概念以优化程序中的工作。通过将常用的计算值存储在缓存中,程序可以节省时间,因为不必重复计算相同的值。虽然硬件也进行缓存,但软件也可以进行缓存,例如大小为 1,000 的缓存存储 1,000 个最近计算的值。如果重复计算相同的值,优化可以使程序加速大约 30%。演讲者随后介绍了利用输入中的稀疏性来避免对该输入的零元素进行计算的想法,从而节省了计算时间。他们通过一个矩阵向量乘法的例子来证明这一点,并引入了可以加速矩阵乘法的压缩稀疏行 (CSR) 数据结构。

  • 00:30:00 在本节中,演讲者讨论了如何使用压缩稀疏行 (CSR) 格式优化稀疏矩阵的存储和计算效率。 CSR 格式将矩阵的非零元素存储在三个数组中:值数组、列数组和行数组。演讲者解释了如何计算行的长度以及如何使用 CSR 格式执行矩阵向量乘法。如果非零元素的数量明显少于 N^2,则 CSR 格式可以比密集矩阵表示更节省空间。然而,对于相对密集的矩阵,密集矩阵表示可能占用更少的空间。演讲者提供了使用 CSR 格式执行矩阵向量乘法的代码示例。

  • 00:35:00 在本节中,讲师讨论了使用稀疏矩阵表示图形的好处,以及如何利用它更有效地运行广度优先搜索和 PageRank 等经典图形算法。稀疏图表示由两个数组组成:偏移量和边,其中每个顶点的度数可以通过取连续偏移量之间的差值来获得。边的权重也可以存储在单独的数组中或与边交错以改善缓存局部性。本节最后简要介绍了逻辑优化,特别是常量折叠和传播,它们在编译期间评估常量表达式以减少运行时间。

  • 00:40:00 在视频的这一部分,演讲者讨论了不同的代码优化技术,包括常量折叠和传播、公共子表达式消除以及利用代数恒等式。通过在编译时定义常量,编译器可以评估它们并在运行时节省时间。公共子表达式消除允许通过存储结果以备将来使用来避免多次计算相同的表达式。利用代数恒等式涉及用更便宜的替代品替换更昂贵的表达式。演讲者强调,虽然编译器通常擅长实现这些优化,但在编译器不自动执行的情况下了解它们仍然很重要。

  • 00:45:00 在视频的这一部分,演讲者讨论了两种优化技术。第一个是通过使用代数恒等式来避免平方根,从而减少计算量大的平方根运算符的使用。第二种优化技术是短路,它涉及在知道结果后立即停止一系列测试,在这种情况下,数组包含非负整数并且需要根据数组检查数组中值的总和限制。优化可以消除查看数组中所有元素的需要并可以加快程序执行速度,但应根据测试何时可以短路的频率明智地使用。

  • 00:50:00 在本节中,视频讨论了短路逻辑运算符的概念及其在优化代码中的用途。双和号和双竖线是短路逻辑运算符,可以通过仅评估参数的一侧来节省时间和资源。此外,该视频还建议根据成功频率和执行成本来安排测试。如果较便宜的测试已经失败,这种方法可以利用短路来跳过昂贵的测试。最后,该视频介绍了通过使用检查在结果已知时提前退出程序来创建快速路径的想法。这方面的一个例子是在检查其他碰撞条件之前检查两个球的边界框是否相交。

  • 00:55:00 在本节中,Bentley 讨论了优化图形程序中碰撞检测测试的方法。他建议进行快速路径测试以确定边界框是否相交,然后再执行更昂贵的碰撞测试。该测试涉及检查每个坐标上的差异的绝对值,并检查它是否大于两个半径的总和。 Bentley 还指出,通过单个 switch 语句甚至表查找组合测试可以大大提高性能。总的来说,这些优化对许多应用程序和图形程序都是有益的。
  • 01:00:00 在本节中,视频介绍了与循环相关的第三类优化。讨论的第一个循环优化是提升,它涉及避免每次通过循环体重新计算循环不变代码。通过一次计算一个表达式并将其存储在一个变量中,而不是在每次迭代中重新计算它,它可以节省运行时间。第二个循环优化是使用哨兵,放置在数据结构中的特殊虚拟值,以简化边界条件的处理和处理循环退出测试的逻辑。通过修改程序以使用数组中的两个额外条目,可以使用哨兵只需要在循环的每次迭代中执行一次检查。

  • 01:05:00 在本节中,演讲者讨论了两种优化代码的技术:边界条件和循环展开。对于边界条件,他展示了如何修改循环以有效处理输入数组的所有元素都为零的特殊情况。通过在数组末尾添加一个虚拟元素并检查溢出,代码可以检测到何时应该停止。对于循环展开,他解释了两种类型:完整和部分。由于循环规模较大,完整循环展开很少见,而部分循环展开通过将多个循环合并为一个来减少循环的迭代次数,这可以通过减少执行的控制流指令的数量来提高性能。
     
  • 01:10:00 在本节中,讲师讨论循环展开和循环融合优化。循环展开涉及将循环的多次迭代合并为一次迭代,从而减少循环控制的开销并实现更多的编译器优化。但是,展开太多会污染指令缓存,从而对性能产生负面影响。另一方面,循环融合将同一索引范围内的多个循环合并为一个循环,从而减少循环控制开销并改善缓存局部性。讲师还讨论了通过修改循环边界以避免在空循环体上执行循环迭代来消除浪费的迭代。

  • 01:15:00 在本节中,Bentley 讨论了函数优化,特别是内联概念以避免函数调用的开销。通过将函数声明为静态内联,编译器将尝试用函数本身的主体替换对该函数的调用,从而消除了单独的函数调用的需要。虽然宏也可以做到这一点,但内联函数更加结构化并评估它们的所有参数,避免了宏在代码中多次粘贴昂贵的表达式的风险。 Bentley 建议不要过早优化,并鼓励开发人员首先确保他们的程序是正确的,并使用回归测试来保持正确性。最后,他指出编译器可以自动进行许多级别的优化,检查汇编代码可以揭示任何此类优化。

  • 01:20:00 在本节中,Bentley 工作优化规则分为六个步骤。这些规则包括建立明确的目标、将任务分解成更小的部分、花时间计划和组织、确定任务的优先级、尽量减少干扰以及定期审查和调整你的方法。通过遵循这些准则,您可以提高工作效率并以更有效的方式实现目标。此外,将这些策略融入您的日常生活可以帮助您保持强烈的职业道德并专注于您的目标。
 

第 3 讲 Bit Hacks



3. 位黑客

这个 YouTube 视频涵盖了各种位操作主题,包括二进制表示、二进制补码、按位运算符、不使用临时变量交换变量以及代码优化。该视频演示了各种位技巧,例如在不使用 if-else 语句的情况下找到两个整数中的最小值,以及如何在不使用临时变量的情况下交换两个整数。演讲者讨论了不可预测的分支,并介绍了可预测分支不可用时的无分支最小位技巧,并展示了位黑客如何通过用简单的按位运算代替除法等代价高昂的运算来优化代码。该视频还讨论了 de Bruijn 序列及其在解决 N Queens 问题等问题中的应用。

第二部分讨论使用位向量解决 N Queens 问题,并有效地计算二进制字中 1 的位数。回溯用于实现 N Queens 问题,位向量用于高效表示棋盘。描述了在 N Queens 问题中安全放置皇后的三个检查,并提出了一种通过递归消除最低有效 1 位来计算一个字中 1 位的数量的方法。此外,还讨论了使用查表和寄存器操作来计算 1 位的数量。视频以分治法的演示结束,该方法计算 1 位,其性能与字长的对数基数成正比。还提供进一步学习的资源。

  • 00:00:00 在本节中,我们将了解单词的二进制表示以及如何从中计算整数值。我们还学习了有符号整数的二进制补码表示以及全零和全一字的特殊情况。此外,我们看到 X 的补码及其与补码表示中 X 的负数的关系的重要恒等式。

  • 00:05:00 在本节中,演示者解释了 Ones Complement 以及如何通过将 Ones Complement 加 1 来获得数字的负数。他还介绍了使用十六进制数字表示大型二进制常量,并给出了一个在十六进制和二进制之间转换的表。然后,该视频介绍了 C++ 中的按位运算符,并展示了如何将它们用于逻辑与、逻辑或、异或以及左移和右移运算的示例。

  • 00:10:00 在本节中,视频讨论了可以在 C 中使用按位运算符实现的各种常见习语。一个示例是通过使用移位后跟 OR 或 NOT 和和,分别。另一个示例是通过在所需位的位置生成一个掩码,在其他地方为零,然后将掩码与 X 进行与运算,并右移提取的位,从而从字 X 中提取位域。该视频还提到,使用这些技巧在处理压缩或编码数据时特别有用。

  • 00:15:00 在本节中,视频讨论了如何通过一些位技巧在不使用临时变量的情况下交换两个整数。该代码涉及设置 X 等于 X XOR Y,然后 Y 等于 X XOR Y,最后 X 等于 X XOR Y。这是有效的,因为 XOR 是它自己的逆,第一行生成一个掩码,其中 X 中的位和 Y 不同,然后用于翻转 Y 中与 X 不同的位。这允许在不使用临时变量的情况下进行交换。该视频还强调了在使用这些技巧时遮蔽以确保安全的重要性。

  • 00:20:00 在本节中,演讲者讨论了两位黑客。第一个 hack 是一种不使用临时变量来交换两个变量的方法。破解涉及使用 XOR 和掩码来翻转两个变量之间不同的位。虽然这个 hack 很酷,但由于指令级并行性差,它并不是交换变量的最有效方法。第二种 hack 是一种在不使用 if-else 语句的情况下找到两个整数中最小值的方法,这可能会由于分支预测错误而导致性能问题。相反,演讲者展示了如何使用按位运算来比较整数并计算最小值而无需分支。

  • 00:25:00 在本节中,演讲者讨论了代码优化以及在合并两个排序数组的子例程中使用“restrict”关键字。该过程通过一个示例进行说明,其中两个绿色阵列合并为一个蓝色阵列。还讨论了代码中每个分支的可预测性,第一个分支是可预测的,而第二个分支是不可预测的。

  • 00:30:00 在本节中,视频讨论了编程中的可预测和不可预测分支,并介绍了无分支最小位技巧作为不可预测分支问题的解决方案。这个技巧涉及使用一个名为“comp”的变量来存储 a 和 b 的第一个元素之间的比较结果,并使用一个位技巧获得 a 和 b 的最小值。然后,最小值放在C中,A或B的值根据“comp”的值递增或递减。该视频指出,虽然该技巧在某些情况下可能不起作用,但它有助于了解编译器在做什么,而且许多针对单词的位黑客攻击可以扩展到向量的位和单词黑客攻击,因此了解这些技巧很有用。

  • 00:35:00 在本节中,视频讨论了位黑客及其在编程中的实用性。给出的示例是模块化加法的一个小技巧,它允许在不使用除法的情况下进行更快的操作。通过将 Z 设置为 X 和 Y 的和,然后检查它是否小于或大于 N,程序可以返回 Z(如果它在范围内)或 Z 减去 N 的结果以将其带回范围内。该程序使用与 minimum 类似的逻辑,并且有一个不可预测的分支,可能导致分支预测错误。给出的另一个示例是一种使用位操作将值四舍五入到最接近的二的幂的方法,方法是递减 N,执行按位或操作,将 N 右移不同的值,然后在最后递增。

  • 00:40:00 在视频的这一部分,演讲者讨论了两个位操作问题。第一个问题涉及找到大于给定值 n 的最小的 2 次幂。演讲者解释了如何通过使用位操作和递减 n(如果它已经是 2 的幂)来实现这一点,以确保设置 n 减去一位的对数。第二个问题涉及计算单词 X 中最低有效位的掩码,演讲者解释了如何通过将结果设置为 X 并使用带负数 X 的按位与运算来实现此目的。演讲者还提供了查找索引的代码通过将 X 乘以一个常数并在表中查找结果来计算该位。该部分以演讲者表演涉及位操作的数学魔术结束。

  • 00:45:00 在这一部分中,一段 YouTube 视频展示了一群人用纸牌和铃铛表演魔术。表演者声称能读懂观众的想法,并要求他们在分发牌前剪掉一副牌。表演者预测每个人牌的花色和号码,似乎成功了。他们将自己的能力归功于穿着“很棒的连体衣”和一顶魔术帽,以及用铃铛净化空气。

  • 00:50:00 在本节中,我们了解 de Bruyne 序列及其与计算 2 的幂的对数底数 2 的相关性。de Bruyne 序列是一个循环位序列,其中长度为 K 的每个可能位串都恰好出现once 作为序列中的子字符串。使用转换表,我们可以将 de Bruyne 序列常数乘以 2 的幂并提取出现在序列开头的子字符串以计算 2 的幂的对数基数 2。通过左移序列然后查找向上转换表中子字符串的起始位置,我们可以确定我们开始的整数的对数基数 2,这解决了之前演示的卡片技巧。

  • 00:55:00 在本节中,演讲者讨论了 de Bruijn 序列,这是一个二进制字母表的循环序列,其中包含每个可能的 n 位字符串恰好一次。该序列可用于解决诸如 N Queens 问题之类的问题,并且可以使用魔术生成。演讲者还解释了位技巧如何确定 de Bruijn 序列左移后子串的位置。这个位技巧的性能受到乘法和查表性能的限制,但现在有一个硬件指令来计算它。

  • 01:00:00 在本节中,演讲者讨论了 N 皇后问题,该问题涉及将 N 个国际象棋皇后放在 NxN 棋盘上,以使两个皇后之间不会相互威胁。该问题通常使用回溯来实现,其中皇后逐行放置,当找不到有效位置时程序回溯。还讨论了棋盘的不同表示,最紧凑的是一组三位向量。下向量存储每列中皇后的存在,左对角向量存储每条左对角线上皇后的存在,右对角向量存储每条右对角线上皇后的存在。使用位向量可以更有效地实现算法。

  • 01:05:00 在本节中,描述了使用位向量检查在 n 皇后问题中放置皇后的位置是否安全的过程。三项检查涉及检查是否已经有与该位置在同一行、同一列、同一对角线上的皇后。这种方法是有效的,并保证如果女王通过所有三项检查就可以安全放置。另一个讨论的问题是人口计数,它涉及计算一个字 X 中 1 位的数量。提出的方法重复消除 X 中的最低有效 1 位直到它变为零,所需的迭代次数与次数成正比X 中的 1 位。

  • 01:10:00 在本节中,演讲者讨论了使用查表来有效计算二进制字中 1 的位数。查表方法涉及创建一个大小为 256 的表,该表存储每个 8 位字中 1 的个数,然后查找二进制字中每个 8 位子串的计数。演讲者指出,此方法的性能受到访问表所需的内存操作的瓶颈。演讲者接着介绍了使用寄存器计算 1 位数的第三种方法,它们在寄存器中创建掩码并执行特定指令,使它们能够在不访问内存的情况下计算二进制字中 1 的个数。演示者通过一个示例来解释此方法的工作原理。

  • 01:15:00 在本节中,演讲者演示了如何使用并行分治法计算输入单词中 1 的数量,其中性能与单词长度 W 的对数基数成正比。它是指出大多数现代机器都有一个内在的弹出计数指令,它比任何可以编码的东西都快,可以通过编译器内部函数访问。但是,这会降低代码在不同机器之间的可移植性。演讲者提供了一些资源来学习更多关于位技巧的信息,包括一个网站、一本教科书、一个国际象棋编程网站和一本名为 Hacker's Delight 的书。
 

第 4 讲 汇编语言与计算机体系结构



第 4 讲 汇编语言与计算机体系结构

该视频全面概述了汇编语言和计算机体系结构。汇编语言是优化代码性能的重要接口,了解计算机体系结构对于掌握汇编语言至关重要。演讲者讲解了x86 64 架构的历史和发展,它的关键寄存器、数据类型、内存寻址模式和指令集架构,包括堆栈、整数和二进制逻辑、布尔逻辑和子程序。他们还讨论了零和符号扩展等扩展以及汇编语言中的各种寻址模式。此外,该视频还讨论了浮点类型、向量和向量单元、传统指令和 SSE 指令,以及超标量处理、乱序执行和分支预测等计算机体系结构设计功能。

该视频还涵盖了与汇编语言和计算机体系结构相关的几个主题。其中一个中心主题是指令级并行性 (ILP) 和流水线停顿,它们是由数据依赖性等危害引起的。演讲者讨论了真实、反和输出数据相关性,以及超标量处理器如何利用硬件中的更多并行性来一次执行多条指令。然而,为了避免危险,架构师已经实施了重命名和重新排序等策略,以及推测执行来猜测分支的结果并预先执行它。演讲者鼓励听众了解这些方法,以更好地理解软件优化。

  • 00:00:00 在本节中,讲师谈论汇编语言和计算机体系结构,现代软件课程通常不涉及这些内容。理解这些概念对于优化代码性能是必要的,而汇编语言是这样做的最佳接口。编译代码的过程涉及几个阶段,包括预处理、编译、汇编和链接。汇编语言是机器代码的助记结构,使其更易于阅读,最终的可执行文件是通过使用 LD 命令的链接阶段生成的。

  • 00:05:00 在本节中,演讲者解释说汇编和机器代码在结构上非常相似,汇编中的 OP 代码对应于机器代码中的特定位模式。演讲者介绍了程序 ABS,它可以生成机器代码的反汇编,揭示助记的、人类可读的汇编代码。演讲者然后讨论了为什么有人可能想要查看汇编代码的几个原因,包括揭示编译器做了什么和没有做什么、调试编译器错误以及在无法访问其源代码的情况下对程序进行逆向工程。

  • 00:10:00 在本节中,演讲者解释了对课程的期望,包括理解编译器如何使用 x86 指令实现语言结构,能够阅读 x86 汇编语言,以及理解常见汇编模式的性能影响。如有必要,学生还应该能够从头开始编写代码,并掌握到可行的水平。演讲者随后深入探讨了 x86 64 位指令集架构,包括寄存器、指令、数据类型和内存寻址模式。关键寄存器包括通用寄存器和标志寄存器,它跟踪算术运算和进位。指令指针通过汇编语言程序中的指令序列引导硬件。

  • 00:15:00 在本节中,演讲者讨论了 x86 64 架构的历史及其从具有有限内存的 16 位字机到随着可用内存的增加而用于索引的更宽字的发展。演讲者还解释了矢量寄存器的添加,例如用于矢量化的 xmm 和 AVX 寄存器,以及它们的使用方法。他们还谈到了通用寄存器的主题,以及它们的功能用途如何随着时间的推移发生了重大变化,但由于历史原因它们的名字一直沿用至今。此外,演讲者还解释了某些寄存器如何为短字、32 位或 64 位字的下半部分和上半部分别名相同。
     
  • 00:20:00 在本节中,演讲者解释了 x86 64 汇编语言的基础知识及其工作原理。根据访问寄存器的哪一部分,寄存器有不同的名称,有些有特定用途,如 RSP 被用作堆栈指针。 x86 64 指令代码的格式是有一个操作码和一个操作数列表,通常有 0-3 个操作数,可以是源或目标。演讲者解释了 AT&T 语法和 Intel 语法在引用操作方面的区别,并提供了常见操作代码的列表,如“移动”和“条件移动”。此外,演讲者解释了在从 32 位值寄存器移动到 64 位寄存器时扩展符号的重要性。

  • 00:25:00 在本节中,演讲者讨论了汇编语言中的各种指令,包括堆栈指令、整数算术、二进制逻辑、布尔逻辑、跳转和子例程。可以使用描述操作数据类型或条件代码的后缀来扩充操作码。例如,带有“cue”的移动表示被移动的数据是一个四字,由 8 个字节或 64 位组成。还讨论了 x86 64 数据类型及其汇编后缀,并给出了示例来说明符号扩展的工作原理。汇编语言中某些操作码和助记符的存在与否可能会造成混淆,但编译器需要理解这些指令才能正确执行程序。

  • 00:30:00 在本节中,演讲者讨论了将 32 位操作移动到 64 位操作时发生的零扩展和符号扩展等扩展。他们还提到英特尔指令集如何随着新指令的添加而变得更加复杂。演讲者接着解释了条件跳跃和条件移动中使用的不同条件代码以及与它们一起使用的标志,例如零标志和溢出标志。还讨论了某些条件代码检查零标志的原因。

  • 00:35:00 在本节中,演讲者讨论了汇编语言中不同的直接和间接寻址模式。直接寻址方式包括立即数、直接内存和使用寄存器。寄存器间接寻址和寄存器索引寻址模式允许通过在寄存器中提供地址或偏移地址来访问内存。演讲者指出,访问内存可能很慢且代价高昂,因此尽可能将值存储在寄存器中以加快该过程非常重要。他们还提到缓存在优化内存访问方面的重要性。

  • 00:40:00 在本节中,视频讨论了指针相对索引的使用,其中指令指针用于索引数据而不是通用寄存器。还介绍了基本索引标度位移寻址方法,这是一种复杂的指令,支持对基本指针进行温和索引。该视频概述了一些汇编语言惯用语,包括用于将寄存器归零的 XOR 操作码,以及计算两个值的按位和并保留寄存器标志的测试操作码。最后,视频涉及跳转指令和标签,它们允许在代码中控制流程。

  • 00:45:00 在本节中,视频讨论了汇编语言和计算机体系结构,包括各种指令集和浮点数的历史。 x86 指令集(包括 x87 和 SSE)支持单精度和双精度标量浮点运算和向量指令。 SSE 指令通常比 x87 更受编译器青睐,因为它们在编译和优化方面非常简单。还解释了在汇编中使用无操作 (nop) 指令的目的,例如对齐优化。

  • 00:50:00 在本节中,演讲者讨论了计算机体系结构中使用的浮点类型、向量和向量单元。演讲者解释说,向量单元的工作方式是处理器向所有向量单元发出指令,每个向量单元称为一个通道。这些通道包含整数或浮点运算,并且都以锁步方式运行,这意味着它们都做同样的事情。它们可以一次执行多个操作,所有这些都可以通过一条指令完成。演讲者解释说,一些架构需要对齐向量操作数,而其他架构可以在内存中移动向量,这导致两者之间存在性能差异。此外,还有支持跨通道操作的架构,例如排列、混洗和分散-聚集。

  • 00:55:00 在本节中,演讲者讨论了传统指令和 SSE 指令之间的区别以及它们的使用方法。他们还提到了别名技巧,其中 ymm 注册别名 xmm 寄存器并使用 avx-512 将它们扩展到 512 位。然后他们开始讨论计算机体系结构,特别是五级流水线与具有 14 到 19 级流水线级的现代处理器之间的区别。他们讨论的设计特性包括矢量或硬件 I、超标量处理、乱序执行和分支预测,但他们提到由于时间限制,他们不会深入研究乱序执行。

  • 01:00:00 在视频的这一部分,讲师讨论了指令级并行性 (ILP) 和流水线停顿。 ILP 涉及寻找机会同时执行多条指令以提高吞吐量。然而,当指令由于诸如结构冒险、数据冒险或控制冒险之类的冒险而无法执行时,可能会发生流水线停顿,其中数据冒险是最常见的。当一条指令在写依赖之后读取时发生真正的依赖,而当一条指令想要写入一个位置但必须等到前一条指令读取该值时发生反依赖。编译器试图通过优化代码来减少流水线停顿以避免危险。

  • 01:05:00 本节讨论汇编语言中数据依赖的概念。数据依赖分为三种:真、反、输出。算术运算,尤其是浮点运算等复杂运算,具有较长的延迟,并且需要处理器中的独立功能单元,有时需要独立的寄存器。为了提高指令级并行性,架构师实施了诸如每个周期发出多条指令的技术,以利用硬件中的更多并行性。

  • 01:10:00 在本节中,视频以 Haswell 将指令分解为更简单的操作(称为微操作)并每个周期最多发出四个微操作为例,说明了超标量处理器如何一次执行多条指令。然后,该视频详细介绍了使处理器免于按顺序执行指令的策略,包括绕过和寄存器重命名,这允许不按顺序执行非相关指令,从而加快处理时间。这些策略需要跟踪依赖关系并以避免数据依赖性等危险的方式执行代码。

  • 01:15:00 在本节中,演讲者讨论重命名和重新排序,这是现代处理器中用于避免数据危害的两种重要方法。演讲者还谈到了推测执行,它用于分支预测以猜测分支的结果并预先执行它,以避免停顿。但是,如果猜测错误,则需要花费大约 15 到 20 个周期来撤消计算。演讲者简要提到了分支预测器的工作原理,但指出它很复杂,需要进一步研究。尽管结束很仓促,但演讲者鼓励听众了解各种方法,这将有助于理解为什么某些软件优化有效和无效。
 

第 5 讲 C 到汇编



第 5 讲 C 到汇编

在这部分视频中,讨论了理解 C 语言对汇编语言的重要性,以及如何使用编译器以汇编语言实现 C 代码。重点特别在于 LLVM IR 如何在 Linux x86 64 调用约定中转换为程序集。演示者解释了 LLVM IR 的基本组件以及 C 编程语言中的结构如何转换为 LLVM IR。该视频还涵盖了虚拟内存的布局、多个函数之间协调函数调用的问题,以及 Linux x86 64 调用约定使用两个指针(BP 和 SP)来管理所有堆栈帧。

该视频还解释了在 C 语言到汇编编程中维护寄存器状态的策略,例如将寄存器保存为调用者保存或被调用者保存,以及 x86 调用约定如何避免浪费工作。它涵盖了函数调用在 C 和汇编中的工作方式,讨论了将参数和局部变量保存到堆栈的过程,以及使用堆栈指针而不是基指针的常见优化。该视频还展示了将 LV miR 编译为汇编代码的过程,讨论了函数序言、保存寄存器、处理条件以及使用控制流图将 C 代码转换为汇编代码。最后,它讨论了用于在返回结果之前恢复寄存器的函数结语。

  • 00:00:00 在这部分视频中,TB Shuttle 讨论了理解 C 语言对汇编语言的重要性。他指出,汇编语言提供了比 C 代码更高的精度,并且可以揭示 C 代码中不明显的程序细节。查看汇编还可以让开发人员确定编译器在优化程序时做了什么或没有做什么。此外,可能会出现错误,这些错误只会在特定级别优化代码时产生意外行为,从而难以发现这些错误。最后,了解汇编可以让开发人员手动修改汇编代码或对代码进行逆向工程。

  • 00:05:00 在本节中,视频讨论了如何通过使用编译器以汇编语言实现 C 代码,编译器必须就使用哪些汇编指令、如何实现 C 条件和循环以及如何协调函数调用。为了解翻译过程,该视频介绍了 LVM IR,这是编译器用来推理程序的简化程序集。然后,该视频解释了 C 编程语言中的结构(例如函数、条件和循环)如何转换为 LVM IR。本节最后简要介绍了 LVM IR 属性。

  • 00:10:00 在本节中,重点是 LLVM ir 如何转换为程序集的过程,特别是在 Linux x86 64 调用约定中。演示者简要介绍了 LLVM ir,解释了其函数、指令和寄存器的基本组成部分,类似于更简单版本的汇编。 LLVM ir寄存器类似于c变量,以名称区分,每个函数都有自己的本地寄存器名称。演示者在示例代码片段中突出显示了寄存器,并指出在引用 LLVM 中的不同基本块时,寄存器的语法被劫持了。演讲最后以一个案例研究结束,该案例研究介绍了此过程如何使用简单代码来计算斐波那契数列。

  • 00:15:00 在本节中,演讲者解释了 LV Mir 指令的语法,其中包括寄存器名称、等号、操作码和操作数列表。虽然一些指令返回一个存储在本地寄存器中的值,但其他指令则不会。 LV Mir 指令集比 x86 的指令集小,因为它只包含少量用于数据移动、算术和逻辑运算以及控制流的指令。在 LV Mir 中,一切都作为类型公开,包括整数(具有定义的位数)、浮点类型、指针类型、向量类型和基本块的标签类型。演讲者还提到 LV Mir 向量运算看起来不像 SSE 或 AVX,而更像是对向量类型起作用的普通运算。

  • 00:20:00 在本节中,视频讨论了如何将 C 代码中的操作序列转换为 LLVM IR,以及在 IR 中解释代码的经验法则。该摘录还解释了 LLVM IR 如何处理基本类型和聚合类型,例如数组和结构,并展示了访问数组中的元素如何涉及计算内存中地址的示例。此外,该视频还解释了如何将 C 函数转换为 LLVM IR,以及两种语言的相应返回语句。

  • 00:25:00 在本节中,视频解释了 C 中的函数和参数如何转换为 LV Mir。 LV Mir 中的函数类似于 C 中的函数,但 C 参数在 LV Mir 中变成了参数列表。然而,读取 LV Mir 可能具有挑战性,因为寄存器名称不明确,因此难以破译。该视频还讨论了 LV Mir 中的基本块,这些块是根据控制流指令划分的代码块。基本块之间的连接基于分支指令引起的边。最后,该视频介绍了 LV Mir 中的条件语句,其中 if-then-else 语句成为条件分支指令,可引发基本块和控制流边缘。

  • 00:30:00 在本节中,视频解释了条件操作和分支在 LLVM IR 中的工作原理。该过程从字面值与存储在寄存器中的值之间的比较开始,这将创建一位整数或布尔结果。然后将此结果连同两个基本块标签一起传递给条件分支操作,这两个标签指示如果谓词为真或假则去哪里。如果存在带一个操作数的无条件分支,则结果是直接跳转到特定的基本块。该视频还展示了如何在存在条件语句时在控制流图中创建菱形,并提供了一个用 C 代码编写的循环示例。

  • 00:35:00 在本节中,视频讨论了 C 循环的组件,其中包括循环体和循环控制。循环体在每次迭代时执行,而循环控制管理循环的所有迭代。 AC 循环在控制流图中产生循环模式,进而在 LLVM ir 中创建循环结构。然后视频分析了循环控制的LLVM ir代码,其中有一个奇怪的fie指令,在处理循环时经常出现。 fie 指令试图解决代码的 LLVM 表示的问题,其中 I 由一大堆不同的寄存器表示,使我不清楚我实际去了哪里。

  • 00:40:00 在本节中,视频讨论了将循环中的归纳变量映射到 LLVM 中的不同位置,这是由于变量在循环迭代中不断变化的值。这就导致了LLVM中的“静态单赋值”不变量,即每条指令只定义一个寄存器的值一次,这给归纳变量带来了问题。 “phi”指令通过定义一个寄存器值来解决这个问题,该寄存器值取决于控制流如何在循环的入口点合并。该视频还讨论了 LLVM 中的属性以及它们如何为 LLVM 构造提供额外信息,例如附加到“添加”指令的 NSW 属性。

  • 00:45:00 在视频的这一部分,重点是 LLVM IR,它类似于汇编,但在很多方面更简单,因为所有计算值都存储在寄存器中,就像普通的 C 变量一样。 LLVM IR 使用静态单一赋值范式,其中每个变量最多由一条指令写入。该视频介绍了如何将 C 代码转换为 LLVM IR,然后再转换为汇编,过程中还有一些额外的复杂性。编译器选择 LLVM IR 操作所需的实际汇编指令,决定使用哪些通用寄存器,并协调不同源文件和二进制库之间的函数调用。然后讨论转向 Linux x86 64 调用约定。

  • 00:50:00 在本节中,讨论了程序的虚拟内存布局。虚拟内存被分成段,例如堆栈段和堆段,这些段是使用汇编代码中的汇编指令来组织的。此外,还讨论了不同类型的存储指令,例如分配内存和存储值的空间指令和长段。然后将注意力转向存储数据以管理函数调用和返回的堆栈段,其中包括存储局部变量、函数参数、返回地址和可能的中间结果。

  • 00:55:00 在视频的这一部分,演示者讨论了跨多个函数协调函数调用的问题,其中一些函数可能来自不同的文件或库。为了确保所有函数都能很好地协同工作,已经建立了所有函数都必须遵守的调用约定。 Linux x86 64 调用约定使用两个指针——BP 和 SP——来管理每个函数实例化的所有堆栈帧。执行call指令时,将IP的当前值作为返回地址压栈,call指令跳转到程序内存中某个函数的地址。返回指令撤消调用指令的操作,并将返回地址弹出堆栈以返回到调用者的执行。为了处理多个函数想要使用相同寄存器的问题,调用约定详细说明了每个函数可以使用哪些寄存器的规则,以及它们必须如何通过函数调用保留这些寄存器。

  • 01:00:00 在本节中,视频讨论了在 C 到汇编编程中使用不同函数操作时维护寄存器状态的策略。它提到了可以使用的两种方法,一种是调用者在调用之前保存寄存器状态,另一种是被调用者保存所有寄存器状态。 x86 调用约定涉及两者,将一些寄存器指定为 callee-save,将其他寄存器指定为 caller-save 以避免浪费工作。该视频还探讨了堆栈内存的组织方式以及堆栈向下增长的方式。最后,它讨论了协调函数将如何使用堆栈内存的重叠部分。

  • 01:05:00 在本节中,视频讨论了函数调用在 C 语言和汇编语言中的工作原理。在调用函数 C 之前,函数 B 将函数 C 的非寄存器参数放置到其局部变量下方其自己的堆栈内存中的保留链接块中。它将访问那些具有负偏移量的参数。当函数 C 启动时,它执行一个函数序言,它保存 B 的堆栈帧的基指针并将 BP 设置为 SP,因为它正在进入一个新帧。然后它在堆栈上为它自己的局部变量分配空间,以及 B 将用于它为其调用者或它所调用的事物创建的任何链接块的空间。该视频还提到了一个常见的优化,其中编译器可能会摆脱 BP 并根据堆栈指针 RSP 进行所有索引以获得更多的通用寄存器。

  • 01:10:00 在本节中,讲师将带领观众完成将 LV miR 编译为汇编代码的过程。第一步涉及使用一些汇编程序指令(例如全局 fib 指令和对齐)将函数“fib”定义为全局可访问函数。然后,函数序言保存了一个 var BP 的推送队列和一个 RSP rbp 的 mob 队列。在将参数移至函数 RTI 之前,汇编代码还将几个寄存器压入堆栈,它们是 Kali 保存寄存器,并将其松鼠到 RBX 寄存器中。最后,评估一个比较指令以检查 N 的值是否小于二,如果是,则返回输入值。否则,它会通过一些直线代码来计算 n 减一的 fib 和 n 减二的 fib,将它们相加并返回结果。

  • 01:15:00 在本节中,视频讨论了条件跳转以及代码中接下来根据比较结果发生的不同行为。 JGE指令在n大于等于2时跳转到LLVM分支操作的假边的标签,而当n小于2时对应操作的真边。LEA指令用于地址计算,而移动操作将函数调用的结果保存到R14中。下一组指令计算 n-2 的值,将其存储到 RDI 中,然后在 n-2 上调用 fib,将结果返回到我们的 AX 中。最后,该操作将 R14 添加到我们的 AX。

  • 01:20:00 在本节中,视频讨论了从 C 代码到汇编代码的转换。演讲者解释说,该过程使用控制流图来表示代码,控制流图由通过控制流边连接的基本块组成。他们还提到处理汇编代码中的调用约定的复杂性。函数 epilogue 用于在返回结果之前恢复堆栈帧中任何内容之前的寄存器。视频最后总结了整个部分涵盖的主要主题。
 

第 6 讲 多核编程



第 6 讲 多核编程

本视频讲座讨论了多核编程以及由于摩尔定律和时钟频率缩放结束而出现的多核处理器。演讲者解释了处理器面临的功率密度问题,以及它如何导致在芯片中添加多个内核以跟上摩尔定律。本讲座还涵盖了共享内存硬件和并发平台(如 Pthreads、TBB、OpenMP 和 Silk 等为并行编程提供抽象的缓存一致性协议)的基础知识。每个平台的优点和缺点都通过实施斐波那契程序的示例进行了讨论和演示。该视频全面概述了多核编程以及程序员面临的挑战和解决方案。

该视频还涵盖了 Silk 的各个方面,Silk 是一种用于处理并行处理的抽象工具。演讲者讨论的主题包括嵌套 silk for 循环、生成汇编代码、使用缩减程序进行缩减、调度程序以及性能优化。他们还概述了 Silk 生态系统和相关工具,例如 silk sanitizer 和 silk scale,分别用于调试和分析可扩展性。主要收获是为多核处理器编写并行程序可能具有挑战性,但 Silk 通过高效处理复杂任务简化了流程,让程序员更好地控制代码的执行。

  • 00:00:00 在本节中,讲师讨论了多核编程以及多核处理器出现的原因。随着摩尔定律的出现,即芯片上可容纳的晶体管数量每两年翻一番,以及时钟频率缩放在 2004 年至 2005 年左右结束,半导体供应商开始生产具有多个处理器内核的芯片。这是因为不再可能增加芯片上单核的频率,因此有必要切换到允许并行处理的设计模型。讲师还阐明了芯片上的晶体管数量与处理器最大频率之间的关系。

  • 00:05:00 在本节中,演讲者讨论了处理器面临的功率密度问题,其中增加时钟频率会导致功率密度呈指数级增长。这导致将多个内核添加到芯片中以跟上摩尔定律并且不超过功率密度限制。演讲者随后介绍了被称为芯片多处理器的抽象多核架构,并概述了使用不同并发平台(如 P 线程、winAPI 线程、Intel 线程)在多核机器上编写并行程序的硬件挑战和软件解决方案的讲座building blocks, openmp, 等等加。

  • 00:10:00 在本节中,演讲者解释了缓存在多核编程中的工作原理。当一个处理器从主内存加载一个值到它的缓存中时,它会将该值保存在缓存中,以防将来需要再次访问它。如果另一个处理器想要加载相同的值,它也可以通过第一个处理器的缓存获取它,而不是一路进入主内存。但是,当一个处理器更新其自己的缓存中的值时,就会出现问题,从而使另一个处理器的缓存中的值过时。 MSI 协议是这个问题的基本解决方案,它用三种可能的状态(M、S 和 I)标记缓存行,以确保缓存之间的一致性。

  • 00:15:00 在本节中,讲座讨论了共享内存硬件中缓存一致性协议的基础知识,特别是对一个处理器缓存中缓存行的修改如何使同一行的其他缓存副本无效。本讲座介绍了一个简单的协议,并解释了在实践中如何存在其他更复杂的协议。当多个处理器修改同一缓存行时,硬件负责管理冲突,但这会导致“无效风暴”并降低性能。该讲座还指出,并发平台可以抽象出这些复杂性,并有助于并行编程中的同步和通信。

  • 00:20:00 在本节中,演讲者使用斐波那契数列示例讨论了不同的并发平台。 Fibonacci 程序是使用递归算法实现的,该算法多次计算 Fibonacci 数,使其成为一个糟糕的算法。这两个递归调用可以并行化,因为它们是完全独立的计算。 Pthreads 是线程的标准 API,可用于实现这种并行化。

  • 00:25:00 在本节中,演讲者讨论了 Pthreads,这是一个支持编程并发的函数库。 Pthreads 提供了一个自己动手的并发平台,因为它允许您使用具有特殊语义的函数库在代码中指定并发。 Pthreads 中的每个线程都实现了处理器的抽象,并被多路复用到实际的机器资源上。 Pthreads 还提供了简化内部线程协调中涉及的协议的掩码。 Pthreads 库具有一些关键函数,例如 pthread_create 和 pthread_join,前者存储和标识 pthread 创建的新线程,后者等待线程完成后再继续执行代码。演讲者演示了如何使用 Pthreads 实现斐波那契数列。

  • 00:30:00 在本节中,主持人讨论了用于并行执行斐波那契数列的 Pthreads 代码的实现。他们解释说,如果输入大小足够小,由于并行创建线程的成本,最好按顺序执行程序。代码将输入参数编组到线程并将其存储在参数结构中。演示者讨论了 Pthread 创建、Pthread 连接及其一些缺点,例如如果代码需要递归创建线程会变得更加复杂。他们还提到这段代码只创建一个线程,所以如果在四个处理器上运行,最大可能的加速是两个。

  • 00:35:00 在视频的这一部分中,讨论了 Pthread 的问题和斐波那契数列的代码。创建线程的高开销导致粗粒度并发,可扩展性问题是两个内核没有做相同数量的工作。该代码还缺乏模块化,因为斐波那契逻辑没有很好地封装在一个函数中,因此难以维护和更改。由于参数编组,代码也变得复杂,这类似于必须在汇编中编写代码。视频随后介绍了线程构建模块 (TBB),这是英特尔开发的一个库,旨在为这些问题提供解决方案。

  • 00:40:00 在本节中,视频讨论了英特尔线程构建模块 (TBB) 库的使用,这是一个 C++ 库,旨在允许程序员使用本机线程和工作窃取算法,而无需直接处理线程。通过指定任务,TBB 可以跨线程对它们进行负载平衡,从而使代码更易于编写并且性能比使用 POSIX 线程更好。该视频展示了使用 TBB 实施斐波那契程序的示例,展示了它如何递归地创建子任务,从而允许跨多个处理器实现更多的并行性和可扩展性。 TBB 还提供用于循环并行、数据聚合和软件流水线的模板,以及用于安全并发访问共享数据的并发容器类。

  • 00:45:00 在本节中,演讲者介绍了解决并发问题的两种语言解决方案:OpenMP 和 Silk。 OpenMP 是一种规范,它为 C 和 C++ 以及 Fortran 提供语言扩展,使用编译器编译指示来指定哪些代码片段应该并行运行。它支持循环并行、任务并行和流水线并行,并且有许多调度和数据共享指令,以及同步结构。演讲者提供了一个在 OpenMP 中实现 Fibonacci 的示例,它比使用 Pthreads 或 TBB 更简单并且性能更好。 OpenMP 是编写并行程序的流行解决方案,因为它提供了许多功能并简化了代码。

  • 00:50:00 在本节中,演讲者讨论了 silk 编程平台,该平台支持联合和向量并行性,并且包括一个可证明有效的工作窃取调度程序。该程序还有一个超级对象库,用于将代码与全局变量并行化,并附带编程工具,例如丝印竞争检测器和称为 silk view 的可扩展性分析器。演讲者还指出,虽然他们不会在课堂上使用 silk plus,但他们将使用一种更好的编译器,称为 taper LLVM,与 silk 的所有其他实现相比,它相对于其基本编译器生成更好的代码。

  • 00:55:00 在本节中,将通过示例讨论使用 silk spawn 和 silk sync 语句来启用并行执行。第一个例子是 Fibonacci 的 salt coat,它包括 silk spawn 和 silk sync 语句,表明 n-1 的 fib 可以与调用它的函数并行执行,而 n-2 的 fib 正在执行。然而,运行时系统决定是否并行运行这些任务。讨论的另一个例子是循环并行性,其中 silk for 语句用于并行执行 for 循环,编译器递归地将迭代空间分成两半,并使用 silk spawn 和 silk sync 语句,直到范围变得太小而无法并行执行。重要的是要保证迭代对于正确的结果是独立的,并且使用 silk 会增加一些额外的开销。

  • 01:00:00 在本节中,视频讨论了使用嵌套 silk for 循环以及使用 clang 编译器中的标志生成汇编代码的详细信息。当多个处理器写入同一内存位置时,使用 silk for 循环的值求和的示例代码会引发确定性竞争问题。 Silk 通过使用 reducer 解决了这个问题,reducer 是处理给定变量的加法函数的超级对象,同时注册和取消注册 reducer 宏。这是一种处理归约问题的方法,可以在多核编程中使用,还有许多其他归约运算符可供使用。

  • 01:05:00 在本节中,演讲者讨论了 Silk 中的缩减器 - 具有关联二元运算和身份元素的代数结构。 Silk 有几个预定义的 reducer,包括加法、乘法、min、max 和 XOR,您可以定义自己的 reducer。 Silk 的好处之一是程序始终有一个有效的串行解释,使调试更容易,并且它有一个运行时调度程序,可以监视程序并将其映射到可用的处理器内核,使用工作窃取调度算法来平衡任务均匀地。与其他并发平台不同,Silk 的调度器在理论上是高效的。

  • 01:10:00 在本节中,演讲者对用于多核编程的 Silk 生态系统进行了高级概述,包括如何编译 Silk 源代码、基准并行和串行性能以及使用 silk sanitizer 和 silk 等工具调试问题规模。他们还强调了优化串行程序性能的重要性,以及 Silk 的调度程序如何在程序执行期间处理不同的任务。此外,演讲者还解释了 silk scale 如何确定代码可以利用的最大处理器数量,使其成为分析可扩展性的有用工具。

  • 01:15:00 在本节中,演讲者总结了多核编程讲座的要点。他们解释说,大多数现代处理器都有多个内核,因此有必要编写并行程序以获得高性能。然而,编写这样的程序可能很困难,这就是 Silk 的用武之地。这个工具从程序员那里抽象出处理器核心,并处理同步、通信协议和负载平衡。演讲者还提到了一个未来的项目,学生将在该项目中使用 Silk 实现他们自己的并行屏幕保护程序。
 

第 7 讲 种族和并行性



第 7 讲 种族和并行性

该视频涵盖了与 Silk 编程中的竞争、并行性和计算障碍相关的一系列主题。涵盖的一些关键概念包括用于并发执行的 spawn 和 sync 语句、避免竞争条件的重要性以及使用 Silk 的竞争检测器来识别它们。该视频还介绍了阿姆达尔定律、工作定律和跨度定律,作为量化程序中并行度的方法,以及分析工作和计算跨度的方法。还讨论了并行排序算法的潜在加速和并行性以及调度理论的概念,重点是贪婪调度定理。总体而言,该视频为了解和优化 Silk 编程中的程序性能提供了宝贵的见解。

该视频解释了贪婪调度器边界的推论,它本质上表明只要 T1/Tinfinity 大于或等于 P^2,任何贪婪调度器都能实现近乎完美的线性加速。 silk scheduler,采用work stealing scheduler,只要处理器数量远少于T1/Tinfinity,就可以实现近乎完美的线性加速。 Silk 运行时系统通过维护准备好的链的工作平台来工作,并像堆栈一样操纵平台的底部。该视频还讨论了 Cactus Stack,它允许并行查看堆栈的多个视图,并使并行函数调用成为可能。 P 处理器执行所需的堆栈空间上限通常比实际需要的数量要宽松得多,因为每个处理器可能不需要在每次窃取工作时都沿着计算图一路走下去。

  • 00:00:00 在本节中,讲师介绍了 Silk 中的种族和并行性主题,这对即将到来的家庭作业和项目很重要。审查了 Silk 的 spawn 和 sync 语句的基础知识,它们允许并发执行父函数和子函数。讲师还强调了与麻省理工学院政策成员会面和避免代码竞争条件的重要性,这可能会导致灾难性后果,如过去的例子所示。最难发现的比赛错误是那些导致灾难性事件的错误,而传统测试通常是无效的。 Silk 确实提供了一种工具来帮助识别代码中潜在的竞争错误。

  • 00:05:00 在本节中,视频解释了争用如何成为开发人员发现的最具挑战性的错误之一,因为它可能只发生在逻辑并行代码访问相同内存位置且至少有一个写入寄存器的罕见事件中给它。例如,该视频演示了一个简单的代码,该代码利用依赖关系图来说明两条并行路径之间共享的竞争错误并不总是会发生。当两个存储都写入相同的内存位置时,就会发生竞争,这可能会导致不同的输出,具体取决于哪个执行路径首先执行。

  • 00:10:00 在本节中,演讲者解释了确定性竞争的概念,当两条指令访问同一内存位置并且至少一条指令是写操作时会发生这种情况,从而导致程序中出现非确定性行为。演讲者给出了如何避免竞争的提示,例如确保 Silk for 循环的迭代是独立的,并确保 Silk spawn 语句和相应的 Silk sync 语句之间的代码也是独立的。演讲者还指出,机器字长很重要,在读取和写入使用非标准类型的打包数据结构时应格外小心。

  • 00:15:00 在本节中,视频讨论了 Silk 平台的“竞争检测器”,它可以帮助识别程序中潜在的竞争条件。通过使用“-f sanitized equal to silk”标志生成检测程序,Silk 可以报告和本地化违规竞争,这有助于调试代码。该视频还解释了并行性的概念,以及 Silk 执行模型如何使用计算图来说明执行期间计算的展开。在尝试优化和提高程序性能时,理解这些概念很重要。

  • 00:20:00 计算 dag 中的顶点类型:初始链、延续链、函数调用链和最终链。 initial strand 包含要执行的第一条指令,final strand 包含程序中执行的最后一条指令。延续链代表生成操作的延续,而函数调用链代表函数调用的执行。重要的是要注意计算 dag 在执行期间动态展开并且不受处理器影响,这意味着运行时系统会弄清楚如何在运行时将任务映射到处理器。总之,计算 dag 是理解程序的并行性和并发性的强大工具。

  • 00:25:00 在本节中,我们将了解计算 dag 以及如何使用它们来分析程序中的并行性。计算 dag 表示程序不同部分之间的依赖关系,具有不同类型的边,包括生成边、调用边、返回边和继续边。可以使用计算 dags 来分析程序中有多少并行度,我们使用 Amdahl 定律来量化并行度。在提供的示例中,需要顺序执行的节点少于七个,表明计算中存在一定程度的并行性。

  • 00:30:00 在本节中,介绍了阿姆达尔定律的概念,作为确定并行处理中可能的最大加速比的一种方法。确定程序的串行部分为 3/18,导致最大加速比为 6。虽然 Amdahl 定律提供了并行度的上限,但在实际情况下往往过于宽松。引入了工作法则和跨度法则作为并行性的替代定义,其中工作法则表明 P 个处理器上的执行时间大于或等于程序的工作除以 P,而跨度法则表明执行时间P 个处理器上的执行时间至少是无限数量的处理器上的执行时间。在许多情况下,这些法则提供了比阿姆达尔法则更好的最大加速上限。

  • 00:35:00 在本节中,演讲者讨论了如何组合不同计算的工作量和跨度量。他们解释说,当执行两个并行计算时,工作仍然是单个计算的工作总和,跨度是单个计算跨度的最大值。演讲者还定义了 P 处理器的加速,并讨论了次线性、线性和超线性加速。他们指出,最大可能的加速等于计算的并行度,即计算中每步的平均工作量。总的来说,本节提供了对计算的组成以及如何衡量它们的并行性的深入了解。

  • 00:40:00 在本节中,演讲者使用计算 DAG 和并行快速排序算法等示例讨论如何分析计算的工作量和跨度以计算最大并行度。演讲者介绍了 Silk Scale 可扩展性分析器,它使用编译器工具来分析程序的串行执行并推导出程序并行加速的上限。演讲者还提到,手动分析大型计算的并行性可能很乏味,但 Silk Scale 可以帮助解决这个问题。

  • 00:45:00 在本节中,视频讨论了运行并行计算并生成显示观察到的加速的图表的结果,以及跨度和工作定律的界限。该图显示最大加速比约为 5,表明程序中的并行度较低。分析表明,并行的瓶颈是按顺序执行分区函数,并行版本的快速排序的预期工作量和跨度为 n log n 量级。能达到的最大并行度是log log n的量级,不是很高。为了增加并行度,分区函数必须并行化。

  • 00:50:00 在本节中,演讲者讨论了在分区和合并排序算法中实现并行性以实现显着加速的重要性。这些算法的并行版本具有 log 平方 n 的总跨度和 n 超过 log n 的高并行度。此外,还有许多其他实用的并行算法,它们具有高并行性和渐近等于相应顺序算法的工作界限。演讲者还介绍了调度理论的概念,解释了集中式调度器可以在运行时将计算 DAG 映射到可用的处理器上,而分布式调度器用于 Silk 编程。一个贪婪的调度器,它在计算的每一步都做尽可能多的事情,作为一个例子。

  • 00:55:00 在本节中,引入了贪婪调度器的概念,它在当前时间步执行尽可能多的工作,而不考虑未来。定义了一个完整的步骤,其中至少 P 条链准备就绪,以及一个不完整的步骤,其中少于 P 条链准备就绪。 Ron Graham 于 1968 年首次展示的著名定理指出,贪婪调度程序实现的时间界限小于或等于 T1/P + T 无穷大,其中 T1 是工作,T 无穷大是跨度。提供了该定理的证明,并表明任何贪婪调度程序都可以在最佳运行时间的两倍内实现。

  • 01:00:00 在本节中,视频解释了贪婪调度器边界的推论,它在最优调度器的两倍内实现。推论指出,当 T1/Tinfinity 大于或等于 P^2 时,任何贪心调度程序都能实现近乎完美的线性加速,其中 T1/P 乘以 T 无穷大衡量计算中的并行度与处理器数量的比值可用的。 silk scheduler使用work stealing调度器,期望运行时间TP等于T1/P加上阶T无穷大,在T无穷大前有一个Big O来承担调度的开销,可以实现近-只要处理器数量远少于 T1/Tinfinity,就可以实现完美的线性加速。 Silk 运行时系统通过维护准备好的链的工作平台来工作,并像堆栈一样操纵平台的底部。 Silk Scale 中的仪器允许测量工作和跨度项以确定程序中的并行性。

  • 01:05:00 在本节中,演讲者讨论了处理器中的生成和并行性的概念。他们解释说,当处理器调用一个函数时,它会将函数的帧放在堆栈的底部,并可以在堆栈底部生成帧。多个进程可以并行发生,并且可以从生成和调用中返回。当工人没有工作可做时,他们会从另一个处理器的甲板上偷东西。著名的定理指出,工人很少偷窃,从而提供近乎线性的加速。演讲者还指出,Silk 支持 C 的指针规则,其中指向堆栈空间的指针可以从父级传递给子级,但不能从子级传递给父级。

  • 01:10:00 在视频的这一部分,演讲者讨论了 Cactus 堆栈,这是 Silk 程序的祖先计算图中的任务可以看到的堆栈。这个栈允许并行的栈的多个视图,这使得并行函数调用成为可能。演讲者解释说,Silk 程序所需的堆栈空间可以通过将程序的串行执行所需的堆栈空间 (S sub 1) 乘以将使用的处理器数 (P) (S子 P ≤ P x S 子 1)。演讲者提供了这一概念的高级证明,并指出 P 处理器执行所需的堆栈空间上限通常比实际需要的数量要宽松得多,因为每个处理器可能不需要每次都沿着计算图向下移动它窃取工作的时间。
 

第 8 讲 多线程算法分析



第 8 讲 多线程算法分析

本视频讨论了分析分而治之递归的主要方法,并将其应用于多线程算法,方法是将 n 的对数基数 B 与 A 的对数基数 B 与 N 的 F 进行比较,以确定它们在 n 中的增长和所需的工作量。该视频展示了一个包含基本多线程算法解决方案的备忘单,涵盖了工作和跨度分析、工作效率以及通过粒度优化克服间接成本等主题。强调了在交流技术主题时同理心的重要性,并引入了 DAG 的概念作为平衡工作和跨度以增加并行性和减少运行时间的手段。

该视频还讨论了多线程算法的分析,重点关注工作和跨度,以及如何在最小化跨度的同时最大化并行度以实现近乎完美的线性加速。演讲者建议对多线程算法采用分而治之的方法,其中分配给线程的任务数量足够大,并警告不要因调度开销而产生大量小任务。提供的示例代码包括一个有效的代码和一个糟糕的代码。演讲者还讨论了如何在二维编码和索引中表示子矩阵,并强调在分治矩阵乘法代码中使用“限制”来提高性能。

  • 00:00:00 在本节中,教师首先提醒学生分而治之的递归以及解决这些问题的一般方法,称为主方法。该方法处理递归及其形式 T(n) = a*T(n/B) + F(n),其中 a 是常数,B 是拆分因子,F(n) 是工作总量将问题拆分为大小为 n/B 的子问题。提醒学生递归树,以及每个级别如何拆分为大小为 n/B 的子问题,他们将跨行的运行时间相加,计算树的高度并将其乘以每个级别的子问题数(a ^k).最后同学们计算出A的n^log底B就是算法的总运行时间。

  • 00:05:00 在本节中,演讲者解释了在评估多线程算法时如何确定 n 的增长和所需的工作量。关键是将 n 的对数 B 与 A 的对数 B 和 N 的 F 进行比较。演讲者涵盖了三种可能的情况,第一种是当对数 B n 远大于 n 的 F 时。在这种情况下,权重的常数部分在叶子中,导致答案为 n 到 A 的对数底数 B。第二种情况是当对数底数 B n 近似等于 n 的 F 时。为此,您添加了一个额外的日志,答案是 n 到日志的日志基数 B 到 n 的 k 加 1。最后,第三种情况是当对数基数 B 远小于 N 的 F 时,这意味着 F 必须满足正则性条件,他们将要查看的所有函数(如多项式和对数)都满足该条件。

  • 00:10:00 在本节中,演讲者展示了一张备忘单,其中包含计算机科学中常用的基本多线程算法解决方案。第一个算法,T(n) = T(n/2) + n,有一个 Θ(n^2) 的解,因为它属于情况一。第二种算法 T(n) = T(n/2) + n^2logn 的解为 Θ(n^2logn),因为它属于 k = 0 的情况 2。对于第三种算法,T(n) = T(n/2) + n^2,解决方案是 Θ(n^2),因为它属于第一种情况。第四种算法 T(n) = 2T(n/2) - n 是一个棘手的算法,因为它不属于主方法的任何情况,并且具有 Θ(n^2loglogn) 的解。

  • 00:15:00 在本节中,演讲者讨论了 Aqua Bazi 方法以及它如何比 master 方法更复杂,这足以分析大多数程序。然后,他们提供了一个并行循环示例,其中包含外部循环并行化的就地矩阵转置代码。演讲者强调了正确定义迭代空间和负载平衡对于高效并行处理的重要性。他们还解释了 open silk 编译器和运行时系统如何实现循环。

  • 00:20:00 在本节中,演讲者描述了一个循环的递归程序,该循环利用分治法将循环分成两部分,并在每一侧递归调用自身,直到达到单元素迭代。这个计算的工作量是按照工作量和跨度来分析的,叶子的工作量是 n 次方的平方,上面的部分是 theta n,因为从叶子到根的每一层只有 n 个节点。 open silk 运行时系统没有循环原语,因此循环有效地转化为这种并行的分而治之方法。

  • 00:25:00 在本节中,演讲者讨论了多线程算法的工作和跨度分析。他解释说,总功是 Θ(n²),因为对完整二叉树的 n 个叶子中的每一个都进行了恒定的工作。循环控制的跨度是log(n),这意味着无限多的处理器可以在对数时间内完成任务。但是,由于计算 (n) 中存在线性分量,因此总跨度为 Θ(n)。因此,并行度为 Θ(n),这对于大多数实际系统来说是一个很好的数量。然后演讲者探讨了内循环也被并行化的场景,并讨论了由此产生的工作量。

  • 00:30:00 在这部分视频中,教授讨论了并行算法中工作效率的概念。并行化算法不会改变工作,但它应该有望减少计算的跨度以实现大并行。然而,有时并行化会导致增加更多的工作,这可能会带来问题,因为与原始算法相比,它会减慢程序的速度。工作高效的并行算法是教授感兴趣的教学内容,因为它们能够在不增加更多工作的情况下减少跨度以实现大并行。教授强调在学习过程中提出问题和犯错误的重要性,因为它可以帮助其他可能有相同问题但不敢提问的人。他鼓励学生参与和参与学习过程,即使他们不在前 10%。

  • 00:35:00 在本节中,强调了在涉及技术主题时同理心在交流中的重要性。教授鼓励学生对课堂上可能不熟悉所涵盖材料的其他人要有耐心。继续对多线程算法进行分析,提出了向量加法的分而治之算法,发现并行度为 n over log n。讨论了并行度和处理器数量之间的关系,教授强调虽然更多的并行度看起来更好,但它只在特定阈值内有益。

  • 00:40:00 在本节中,演讲者讨论了如何优化多线程算法中的一些开销,特别是子程序调用开销。他们引入了“粒度”的概念,这向编译器建议在分而治之过程的叶子上每个块最多有 G 个元素。这允许子例程开销在 G 迭代中分摊,而不是仅仅分摊一次,从而获得更好的性能。如果未指定粒度,系统会做出最佳猜测以最小化开销。演讲者分解变量 I、G 和 s 来分析此上下文中的工作,并将其与没有并行控制内容的原始代码中的工作进行比较。

  • 00:45:00 在本节中,演讲者讨论了多线程算法分析以及循环控制在现代乱序处理器上的成本如何。他们查看跨度,即使用无限数量的处理器执行算法所需的时间,并讨论开销成本与迭代成本。演讲者根据叶子的数量和需要在每个叶子上执行的彩色操作(称为“s”)来分解跨度。他们还回答了有关完整二叉树中内部节点数量以及计算跨度所采用的路径的问题。

  • 00:50:00 在本节中,演讲者讨论了多线程算法的分析,特别是 DAG(有向无环图)的概念以及它如何影响算法的路径。他们强调 DAG 的不同子树之间需要独立,以允许并行处理。目标是平衡算法的工作量和跨度,因为这两个因素的作用方向相反。关键是让 G(并行量)比 I(算法的顺序部分)大得多,这样可以减少开销并加快处理速度。演讲者还提到了这个概念的实现,但指出它将在讲座系列的后面讨论。

  • 00:55:00 在本节中,演讲者介绍了使用 for 循环将两个向量相加的多线程算法的实现。该循环使用一个称为 V add 的运算符来执行串行向量加法,并且该循环使用大小为 G 的向量在 G 组中产生加法。假设 G 等于 1,则循环的工作是 n 阶,跨度也是订单并行度是通过工作与跨度的比率来衡量的,即 n 除以 n,简化为 1。演讲者强调分析多线程算法的目标是增加并行度并减少跨度以最小化运行时间,并强调了技术称为条带挖掘,其中长度为 N 的循环被双嵌套循环取代以并行计算,作为改进多线程算法的一种方法。

  • 01:00:00 在本节中,演讲者从工作和跨度方面分析了多线程算法的性能。他们讨论了如何最小化跨度以最大化并行性,因为跨度在分母中。如果想要近乎完美的线性加速,关键是要产生比处理器多十倍的并行度。演讲者还建议在必要时权衡一些并行性以减少工作开销。只要保留足够的平行度,他们鼓励摆弄粒度来这样做。

  • 01:05:00 在本节中,演讲者讨论了在多线程算法中使用分而治之策略,建议不要一个接一个地产生大量小任务,除非它们被分成几个任务,在这种情况下,开销很好。通常,应该采用分而治之的方法,其中分配给线程的任务数量足够大。演讲者警告要注意调度开销,并建议并行化外循环而不是内循环,后者有更多的开销。此处提供的示例代码显示了一个高效的代码,其中调度只发生一次,而一个糟糕的代码,其中开销发生 n 次。矩阵乘法用作多线程算法的示例,该算法使用分而治之策略将 n 乘以 n 矩阵,方法是对 n 乘以 2 x n 超过 2 个矩阵进行 8 次乘法,然后将两个 n 乘 n 矩阵相加。

  • 01:10:00 在本节中,演讲者讨论了如何在二维编码和索引中表示子矩阵,尤其是 C 和 Fortran 等语言的行主顺序和列主顺序。其思想是,每个二维矩阵都可以作为一维矩阵进行索引,在处理子矩阵时,需要加上长矩阵的行数和长度,才能知道IJ元素在哪里。此外,在实施分而治之时,了解四个子矩阵中每一个的起始角是至关重要的。最后,演讲者强调在分治矩阵乘法代码中使用“restrict”来告诉编译器哪些元素可以或不可以别名。

  • 01:15:00 在本节中,演讲者讨论了矩阵乘法的多线程算法。该算法基于将矩阵递归细分为更小的子矩阵,每个子矩阵递归相乘以获得最终结果。演讲者强调了一个快速确定 n 是否为 2 的幂的小技巧,并使用宏来简化矩阵的索引。该算法表现出高并行性,可以减少并行性以提高性能。还提到了其他类似的算法。
 

第 9 课 编译器能做什么和不能做什么



第 9 课 编译器能做什么和不能做什么

该视频讨论了编译器如何使用各种示例来优化代码。它解释了编译器如何消除不必要的存储和内存引用,以及它们如何优化循环以提高性能。

该视频还解释了编译器优化过程的概念,以及如何使用它们来提高代码性能。它还讨论了编译器的局限性,特别是在矢量化方面。如果编译器可以确定代码中的指针不会别名,则它们只能对代码进行矢量化。如果指针做别名,编译器将无法向量化代码。作为一名程序员,您可以通过注释您的指针来帮助编译器提供有关它们使用的更多信息。

  • 00:00:00 在本次讲座中,Tao Schardl 讨论了编译器可以做什么和不能做什么。他解释说,研究编译器优化可以帮助开发人员了解如何编写利用优化的代码,以及如何避免手动优化编译器已经可以优化的代码。

  • 00:05:00 编译器是一个很大的软件,当编译器本身有bug时,它绝对可以帮助调试。它有助于了解编译器可能在哪里犯了错误,或者编译器只是没有做您认为它应该能够做的事情。

  • 00:10:00 编译器可以做很多优化,但有些事情它做不到。例如,它不能总是创建快速路径或优化循环。

  • 00:15:00 编译器可以做很多事情来优化代码,但有些事情它做不好。一个例子是数据结构——编译器对它们的处理方式与对函数的处理方式不同。另一个是循环——编译器可以对它们做一些优化,但是有限制。

  • 00:20:00 这个 YouTube 视频解释了编译器如何使用简单的操作来替换更复杂的操作。例如,要替换除法运算,编译器可以乘以一个幻数,然后移动结果。该视频还介绍了地址计算器的工作原理。

  • 00:25:00 该视频以简单的“vex scale”函数为例,讨论了编译器如何优化代码。代码首先显示没有优化,然后显示应用了各种优化。显示的优化包括减少指令数量、使用矢量指令和使用幻数。

  • 00:30:00 该视频讨论了编译器如何通过消除不必要的内存操作来优化代码。特别是,它展示了编译器如何通过消除将值存储在内存中的需要来优化将两个标量值相乘的函数。最后,它展示了编译器如何通过消除将结构存储在内存中的需要来优化对结构进行操作的函数。

  • 00:35:00 在此 YouTube 视频中,演讲者解释了编译器如何通过消除不必要的存储和内存引用来优化代码。他使用具有多个字段的结构示例,并通过仔细分析地址计算中涉及的数学来演示编译器如何优化代码。通过了解每条指令的作用,编译器可以优化代码并删除不必要的操作。

  • 00:40:00 编译器努力转换数据结构和标量值,以尽可能多地存储在寄存器中,并尽可能避免使用任何本地存储。对于函数调用,编译器会看到调用指令和被调用函数的代码。然后它会尝试使上面代码中的函数运行得更快。

  • 00:45:00 编译器可以内联代码以避免函数调用开销,还可以通过删除不必要的操作来优化代码。

  • 00:50:00 在此 YouTube 视频中,演讲者解释了编译器可能无法内联函数的三个主要原因:如果函数是递归的,如果函数在不同的编译单元中,或者如果函数可以增加代码大小并损害性能。演讲者还提供了一些在您自己的程序中控制函数内联的技巧。

  • 00:55:00 该视频介绍了编译器如何优化循环以使程序运行得更快。代码提升或循环不变代码移动是一种可用于提高性能的优化。

  • 01:00:00 编译器可以通过执行一系列转换过程来优化代码。这些遍旨在查找相同地址计算等属性,并用更高效的代码替换它们。

  • 01:05:00 编译器能够向量化此循环,尽管它不知道“x”和“y”的地址是否重叠。这是因为编译代码的结构比作为输入给出的两行函数更复杂。

  • 01:10:00 这个 YouTube 视频解释了编译器可以做什么和不能做什么。如果代码不包含任何循环,编译器可以对代码进行矢量化。但是,如果代码确实包含循环,则编译器只能在循环充满向量操作的情况下对代码进行向量化。如果循环中没有充满向量操作,编译器将无法对代码进行向量化。

  • 01:15:00 编译器是否可以向量化代码是一个难题,因为它取决于许多因素。特别是,编译器需要能够确定两个指针是否会产生别名,这可能是一项艰巨的任务。作为一名程序员,您可以通过注释您的指针来帮助编译器提供有关它们使用的更多信息。
 

第 10 讲 测量和定时



第 10 讲 测量和定时

在此视频中,演讲者讨论了计算中测量和计时的各个方面,包括可能影响准确性的外部因素。他们强调需要模型和对数据的清晰理解,减少方差以补偿错误,并使用适当的定时调用来确保可靠性。他们还讨论了准确测量软件性能的挑战,例如幂律和非确定性因素,同时重点介绍了性能建模、定时调用、硬件计数器和模拟器等工具。最后,他们告诫不要依赖单一工具,建议将三角测量和适应作为确保准确结果的技术。

演讲者解释了可靠的性能测量在性能软件工程中的重要性。他建议使用统计数据来准确衡量绩效,并讨论了各种类型的汇总统计数据,例如均值,它们可以在不同情况下提供有用的绩效衡量标准。他指出,取比率的算术平均值不是一种有效的方法,并建议改用几何平均值。演讲者强调了在比较程序时选择正确的数据聚合方式的重要性,并建议从多次运行中取低阶统计量,例如最小值或 10%,以更准确地比较性能。演讲者还讨论了衡量和比较性能的不同方法,包括直接比较和拟合模型以得出统计数据,但警告了建模中的过度拟合问题。总的来说,该视频强调了可靠的性能测量对于提高程序性能的重要性。

  • 00:00:00 在本节中,演讲者讨论了他以前的一位学生所做的关于排序代码的研究。该代码使用 time.h 头文件来访问用于计时测量的时钟获取时间例程。排序例程是定时的,经过的时间被计算并打印出来。测得的运行时间图表显示,排序例程主要遵循 N log N 增长,但即使对于最大的数组,测得的时间也很慢。

  • 00:05:00 在本节中,一位教授讨论了为观察到的数据建立模型的重要性。他举了一个例子,其中测量数据显着偏离预期,并要求学生针对这种偏差提出可能的假设。虽然有些人认为这可能是缓存或时间精度的问题,但教授指出,机器上可能运行着不相关的进程,导致了偏差。在这种情况下,问题在于多租户,其中一台机器同时被多个进程使用。教授强调需要进行质量测量并清楚地了解数据的含义。

  • 00:10:00 在本节中,演讲者讨论了计算中的测量和计时,以及外部因素如何影响测量的准确性。他们特别关注时钟频率如何因系统温度和节能技术而发生变化,这会显着影响测量结果。演讲者介绍了动态频率和电压缩放的概念,这是一种通过调整晶体管的时钟频率和电压来降低功耗的技术。他们强调,在进行测量时考虑外部因素以避免不准确的结果至关重要。

  • 00:15:00 在本节中,演讲者讨论了由于电气工程师使用幂律而测量软件性能的挑战。幂律表明功率等于 CV 的平方 F,其中 C 是动态电容,V 是电源电压,F 是时钟频率。为了减少功率和热量,可以降低频率和电压,从而导致功率的立方减少。这对可靠地测量软件性能提出了挑战,演讲者建议通过消除一些噪声来停止系统以进行准确的测量。他们还讨论了用于测量软件性能的工具,例如性能建模。

  • 00:20:00 在本节中,演讲者讨论了在测量和计时方面减少方差的重要性,尤其是在性能工程的背景下。通过减少可变性,可以补偿系统和随机测量误差,并且需要更少的试验来比较程序。演讲者还指出了计算机系统中可变性的各种来源,包括后台作业、中断和线程放置等。最后,他强调了在尝试对系统进行更改之前首先关注减少方差的重要性,因为在高方差期间进行测量可能会导致不可靠的结果。

  • 00:25:00 在本节中,演讲者讨论了处理器中超线程的概念以及它如何导致云系统中的测量不准确。超线程通过一个功能单元运行两个指令流,看起来像两个处理器,但实际上只提供了 20% 的加速,而不是两个独立的处理器。演讲者还讨论了其他技术,例如涡轮增压和静止系统,这些技术会显着影响测量结果。通过关闭超线程和 Turbo Boost 并停止所有恶魔,演讲者和他的团队能够实现非常可靠的测量,比最小运行时间慢不到 1%。

  • 00:30:00 在本节中,演讲者讨论了可能影响在现代硬件上运行的程序的确定性的各种因素。虽然可以采取某些措施使程序更具确定性,但不可能完全消除非确定性因素。在程序执行期间可能出现的大多数非确定性因素,例如缓存、中断、线程和分支预测,都是确定性过程。但是,硬件中的随机性不在用户的控制范围内,可能会导致内存错误。演讲者建议代码对齐可以影响程序的确定性,代码中的任何更改都可能导致缓存对齐发生变化,从而影响程序的确定性。

  • 00:35:00 在本节中,演讲者讨论了微小的变化如何对程序的性能产生巨大的影响。例如,插入一个字节可能会导致它之后的所有内容受到线性影响,并且更改点 o 文件在链接器命令行上出现的顺序可能比在 -OH- 和 -O3 之间移动产生更大的影响。为了解决这些问题,编译器正在识别问题并进行更多调整。 LLVM 有开关可以对齐一个函数中的所有函数和块,但是对齐会减慢程序的速度。此外,程序的名称会影响其速度,因为可执行文件的名称最终出现在最终出现在调用堆栈上的环境变量中。

  • 00:40:00 在本节中,演讲者讨论了用于测量软件性能的各种工具和方法,包括使用 time 命令从外部测量程序,使用 clock get time 或其他方法将计时调用放入程序中,使用 gdb 中断程序确定哪个例程花费的时间最多,利用硬件和操作系统支持(例如 perf 工具集中使用的支持)或模拟程序以获得更深入的理解,但速度较慢。演讲者解释了使用 time 命令时经过的时间、用户时间和系统时间之间的差异,以及由于处理器分配的原因,它们加起来可能不等于总时间。

  • 00:45:00 在本节中,演讲者讨论了不同类型的计时和测量,包括挂钟时间、处理器在用户模式下花费的时间以及在内核中花费的时间。推荐使用的计时调用是带有时钟单调选项的时钟获取时间,它保证永远不会向后运行。虽然它可能需要不确定的时间来运行,但它比 RTIDTSC 等其他计时器更可靠,后者可能会在不同的内核上给出不同的答案,并且可能会向后运行。演讲者警告不要使用 get time of day。

  • 00:50:00 在本节中,演讲者讨论了在编程中测量和计时事件的各种方法,包括使用 clock_gettime 和使用 time 命令进行测量。他们警告说,随着时间的推移,技术可能会发生变化,工程师可能需要适应。演讲者还建议将随机中断代码作为一种简单的分析技术,并提到 Facebook 等大公司使用这种方法。此外,演讲者介绍了硬件计数器和库 livepFM4 的概念,它允许在每个进程的基础上访问这些计数器。他们强调,工程师可能并不总是需要外科手术般精确的工具,有时一个简单的工具就足以完成这项工作。

  • 00:55:00 在本节中,演讲者讨论了收集测量值的不同方法,包括硬件计数器和模拟器。他们告诫说,许多硬件计数器的文档记录很差,而且如果使用超过四个或五个计数器,一些工具会分时共享带宽。模拟器因其可重复性而受到称赞,但可能无法对缓存中的所有内容进行建模。演讲者建议三角测量并至少使用两次测量以确保准确性。他们还强调了使用性能模型来指导数据解释的重要性。

  • 01:00:00 在本节中,演讲者解释了性能软件工程的过程,该过程涉及采用程序、对其进行更改以及测量其性能以生成更快的程序。然而,可靠的性能测量对于做出累加的小变化和准确得出结论至关重要。因此,演讲者建议使用统计数据来准确了解正在发生的事情。他还提出了一个难题,涉及测量确定性程序的原始性能,并得出结论,使用最小值是拒绝噪声和确定程序底层硬件性能的最佳方法。

  • 01:05:00 在本节中,讨论了各种类型的汇总统计数据,包括均值,它可以在不同情况下提供有用的绩效衡量标准。在评估 Web 服务器时,可以使用算术平均值来衡量 CPU 利用率和总任务完成时间,而挂钟时间和百分位行为可能与优化请求满意度更相关。然而,在总结比率时,例如比较程序 A 和 B 的性能,采用算术平均值并不是一种有效的方法,尽管它经常被误用。这是因为比率的平均值与比率本身不同,如视频中给出的示例所示。

  • 01:10:00 在本节中,演讲者讨论了衡量绩效时比较比率和均值的问题。他们证明,取比率的算术平均值可能会导致可疑结果,最好使用几何平均值。此外,他们指出,在比较利率时,调和平均数通常很有用。强调了在比较程序时选择正确的数据聚合方式的重要性,演讲者建议从多次运行中获取低阶统计量,例如最小值或 10%,以更准确地比较性能。

  • 01:15:00 在本节中,演讲者讨论了衡量和比较绩效的不同方法。一种方法是比较 10% 最便宜的选项并进行降噪以比较均值,尽管它可能无法提供确凿的证据。或者,可以在 A 和 B 之间进行面对面比较,看看哪一方获胜的频率更高。通过这个,可以检验原假设,并可以计算p值来判断偏差是否显着。这种方法常用于社会科学,在嘈杂的环境中也很有用。最后,演讲者简要介绍了拟合模型以得出统计数据的想法,并讨论了最小二乘近似的使用。

  • 01:20:00 在本节中,演讲者讨论了建模中的过度拟合问题,以及添加更多基函数如何改善数据拟合但也会导致过度拟合。解决方案是删除基函数并检查它是否影响模型的质量。演讲者还提到了被称为测量大师的开尔文勋爵,他说测量就是知道,如果一个人不能测量,就不能改进。视频的结尾是演讲者祝愿周二的测验好运。