try ai
科普
编辑
分享
反馈
  • 超标量处理器

超标量处理器

SciencePedia玻尔百科
核心要点
  • 超标量处理器利用乱序执行、寄存器重命名和推测执行来发掘指令级并行(ILP),从而在每个时钟周期执行多条指令。
  • 实际性能受限于处理器流水线中最窄的阶段(如指令获取或提交),以及内存延迟等关键瓶颈。
  • 核心调度逻辑的复杂度和功耗呈二次方规模增长,这为构建无限宽度的处理器制造了根本性的物理障碍。
  • 实现高性能需要采用协同设计方法,其中编译器、算法和操作系统需要经过专门设计,以便为硬件暴露并行性并隐藏延迟。

引言

几十年来,对计算速度的不懈追求推动着计算机体系结构的发展。虽然单纯提高时钟频率曾是提升性能的主要途径,但物理极限迫使工程师们寻求一种更深层次的解决方案:并行计算。超标量处理器代表了朝这个方向的巨大飞跃,它通过同时做多件事情而非更快地做一件事情,来加速标准的串行程序。然而,这种方法带来了一个重大挑战:处理器如何能在打乱原始指令顺序执行以寻找并行工作的同时,保证最终结果的绝对正确?

本文将剖析超标量设计这个优雅而复杂的世界,以回答上述问题。首先,在“原理与机制”一节中,我们将探讨指令级并行的核心概念、乱序执行的奥秘,以及限制性能的严峻物理现实和瓶颈。随后,“应用与跨学科联系”一节将揭示超标量硬件的存在如何从根本上重塑了软件,影响了从编译器设计、算法到操作系统架构的方方面面。

原理与机制

要领略超标量处理器的精妙之处,我们必须踏上一段旅程。我们从一个简单而强大的承诺开始,然后发现为实现它而构建的复杂机器。在此过程中,我们将遇到制约我们雄心的严酷物理和逻辑定律,最后,我们将看到要接近处理器真正潜力所需的硬件与软件之间的巧妙协作。

并行性的承诺

计算机程序的核心是一系列指令,一份需要一步步执行的“食谱”。几十年来,让程序运行得更快的主要方法是更快地执行每一步——即提高时钟频率。但这条路有其物理极限。超标量方法提供了一种更深层次的途径:如果我们能同时做很多事,而不是更快地做一件事呢?

这就是​​指令级并行(Instruction-Level Parallelism, ILP)​​的承诺。超标量处理器设计有多个执行流水线,使其拥有一个​​发射宽度​​ WWW。在理想世界中,这意味着它可以在每个时钟周期执行 WWW 条指令,实现 WWW 的​​每周期指令数(Instructions Per Cycle, IPC)​​。理论上,一个 W=4W=4W=4 的处理器可以比 IPC 为 1 的简单标量处理器快四倍。

然而,世界很少是完美的。第一个限制并非来自硬件,而是来自软件本身。程序并非一堆随机指令的集合,而是一个充满依赖关系的网络。一条指令的结果常常是下一条指令的输入。在任何给定时刻,程序中固有的、可并行执行的工作量被称为其指令级并行度,我们可将其表示为 IdI_dId​。这引出了一个支配所有性能的美妙而简单的真理:可实现的 IPC 同时受限于机器能做什么和程序提供什么。因此,性能的上限是这两个值中较小的一个:

IPC≤min⁡(W,Id)IPC \le \min(W, I_d)IPC≤min(W,Id​)

如果你有一台强大的 4 发射宽度机器(W=4W=4W=4),但运行的是一个没有并行性的纯串行程序(Id≈1I_d \approx 1Id​≈1),那么你强大的处理器将表现得像一个简单处理器,IPC 仅能达到 1。相反,如果你的程序富含并行性(Id=50I_d = 50Id​=50),但你的机器一次只能发射两条指令(W=2W=2W=2),那么你将受限于硬件的能力。性能是处理器与程序之间的合作成果。

让我们将其具体化。想象一个程序有三个独立的任务,或者说依赖链。

  • 链 A\mathcal{A}A: A1→A2→A3→…A_1 \rightarrow A_2 \rightarrow A_3 \rightarrow \dotsA1​→A2​→A3​→…
  • 链 B\mathcal{B}B: B1→B2→B3→…B_1 \rightarrow B_2 \rightarrow B_3 \rightarrow \dotsB1​→B2​→B3​→…
  • 链 C\mathcal{C}C: C1→C2→C3→…C_1 \rightarrow C_2 \rightarrow C_3 \rightarrow \dotsC1​→C2​→C3​→…

一个老式的简单处理器会先执行 A1A_1A1​,然后是 A2A_2A2​,在完成整个链 A\mathcal{A}A 之后才会开始执行 B\mathcal{B}B。一个发射宽度为 W=3W=3W=3 的超标量处理器查看代码,发现 A1A_1A1​、B1B_1B1​ 和 C1C_1C1​ 互不依赖。因此,在第一个周期,它同时执行这三条指令!在第二个周期,它执行 A2A_2A2​、B2B_2B2​ 和 C2C_2C2​。它同时在这些并行的轨道上飞驰,充分利用其宽度。总时间不再是所有链长度的总和,而仅由最长的链(​​关键路径​​)决定。这就是超标量处理器发掘代码中固有并行性的方式。

并行引擎:乱序执行

处理器是如何施展这种“魔法”,找到并执行那些在原始程序文本中可能相隔数百行的独立指令的呢?答案在于计算机科学中最优雅的概念之一:​​乱序执行(out-of-order execution)​​。

为了实现这一点,处理器采用了一套非凡的硬件结构。可以把它想象成一个效率极高的餐厅厨房。

  1. ​​重排序缓冲区(Reorder Buffer, ROB):​​ 当指令进入处理器时,它们会得到一个编号,就像在熟食店取号一样。这就是它们的原始程序顺序。它们可能以完全不同的顺序被执行(谁的菜先好就先上),但它们必须按原始的编号顺序提交(commit)——也就是将其结果永久化并对外部世界可见。ROB 就是强制执行这一规则的硬件。它确保了即使内部执行是并行带来的混乱狂潮,最终结果也与简单的顺序执行完全相同。

  2. ​​寄存器重命名与物理寄存器堆(Physical Register File, PRF):​​ 程序使用少量架构寄存器(如 R1,R2,…R_1, R_2, \dotsR1​,R2​,…)。如果两条不相关的指令恰好都想写入 R5R_5R5​,它们通常必须互相等待。这是一种“伪”依赖。寄存器重命名通过使用一个庞大的、隐藏的物理寄存器池来解决这个问题。当指令 I10I_{10}I10​ 想写入 R5R_5R5​ 时,它会被分配一个全新的物理寄存器,比如 P38P_{38}P38​。当后面一条独立的指令 I20I_{20}I20​ 也想写入 R5R_5R5​ 时,它会被分配另一个物理寄存器,比如 P42P_{42}P42​。现在它们可以并行执行而互不干扰。处理器维护一个映射表,以知晓哪个物理寄存器当前代表了 R5R_5R5​ 的“最新”版本。

有了这些机制,处理器可以变得非常激进。它可以在指令流中向前看很远,越过依赖关系和慢速操作,去寻找可做的工作。它甚至可以进行​​推测(speculate)​​,最典型的就是猜测分支(if-then-else 语句)的方向。

想象一下处理器遇到一条分支指令 I3I_3I3​。它预测条件为假,并立即开始执行预测路径上的指令 I4I_4I4​ 和 I5I_5I5​。这些推测执行指令的结果被计算出来,但它们被写入物理寄存器的推测世界和一个称为​​存储缓冲区(store buffer)​​的临时区域。它们不触及“真实”的架构状态。稍后,当 I3I_3I3​ 最终执行时,处理器发现它的预测是错误的!现在发生的事情就是这个魔法的关键所在。处理器只需将错误路径上的所有工作声明为无效。I4I_4I4​ 和 I5I_5I5​ 的 ROB 条目被清空。寄存器 R5R_5R5​ 的推测值被丢弃,映射关系恢复到推测前的状态。来自 I5I_5I5​ 的计划内存存储操作从存储缓冲区中被清除。这一切就好像从未发生过。没有造成任何损害。然后处理器从正确路径开始取指。这种探索、犯错并完美恢复的能力,使得乱序执行机器既能快得惊人,又能保证结果完全正确。

现实中的瓶颈

实现 IPC 为 WWW 的梦想是一个强大的驱动力,但现实是一系列的瓶颈。处理器是一条流水线,就像任何装配线一样,其总吞吐量受限于其最慢或最窄的阶段。

我们已经看到了第一个瓶颈:IPC≤min⁡(W,Id)IPC \le \min(W, I_d)IPC≤min(W,Id​)。但机器的宽度 WWW 本身是一个简化。流水线有多个阶段:指令​​获取​​(fetch, FFF)、解码(decode)、发射(issue, WWW)和提交(commit, bbb)。真正的 IPC 受限于所有这些阶段中的最小值。

  • 如果你的前端每个周期只能​​获取​​(fetch) F=4F=4F=4 条指令,那么即使你的执行核心可以发射 W=8W=8W=8 条指令也无济于事。核心会因为没有足够的工作而“挨饿”。IPC 的上限是 4。这就像试图用一条四车道的匝道来填满一条八车道的高速公路。
  • 同样,如果你的核心可以发射 W=8W=8W=8 条指令,但​​提交​​(commit)阶段每周期只能退役(retire) b=3b=3b=3 条指令,那么重排序缓冲区中就会形成“交通堵塞”。最终,ROB 会被填满,导致整个引擎停滞,直到提交阶段赶上来。IPC 的上限是 3。

即使硬件完美平衡,也并非每个执行槽都能被填满。无数微小的冒险(hazard)和资源冲突都可能在流水线中产生“气泡”。我们可以用一个简单的概率 qqq 来对此建模,表示一个给定的发射槽被阻塞的概率。对于一台 2 发射宽度的机器,平均 IPC 不是 2,而是 2(1−q)2(1-q)2(1−q)。这意味着如果一个槽有 50% 的几率被阻塞(q=0.5q=0.5q=0.5),你强大的超标量机器就会退化到 IPC 为 1,不比简单的标量处理器好。

然而,最强大的瓶颈是内存。一条在缓存中未命中(miss)而必须从主存(DRAM)获取数据的加载指令,可能需要数百个周期。这会产生一个严重的问题,称为​​队头阻塞(Head-of-Line Blocking)​​。还记得我们用熟食店柜台来类比重排序缓冲区吗?想象一下,指令 #27 位于 ROB 的头部,准备提交。但它是一条仍在等待内存数据的加载指令。在它身后,指令 #28, #29, #30...#50 都已完成工作,准备“回家”。但它们不能。它们都卡在 #27 后面排队。整个提交阶段都停滞了,不是因为缺少工作,而是因为队头有一条慢速指令。这种情况发生的可能性取决于缓存未命中率(rMr_MrM​)、内存速度(我们称之为 λ\lambdaλ)和 ROB 的大小(NNN)。一个更大的 ROB 提供了更大的缓冲区,让慢速内存操作有更多时间在到达队头并阻塞其他所有指令之前完成。

隐藏延迟的艺术

既然停顿,尤其是内存造成的停顿,是不可避免的,我们能对此做些什么聪明的事情吗?答案是肯定的。这正是处理器乱序能力真正大放异彩的地方,通常与智能编译器协同工作。目标是​​隐藏延迟​​:当我们在等待一个长时间操作完成时,我们应该找到其他独立的工作来做。

考虑一个循环,在每次迭代中,它从内存加载一个值,然后在计算中使用它。如果加载到使用(load-to-use)的延迟是 4 个周期,处理器的算术单元将在这 4 个周期内闲置,等待数据到达。这将严重削弱 IPC。但如果我们能填补那些空闲的周期呢?

一个巧妙的调度可以交错执行来自不同循环迭代的操作。在周期 ttt,处理器可能会发射:

  1. 当前迭代的​​加载​​(load)指令 LtL_tLt​。这开始了漫长的访存之旅。
  2. 来自当前迭代的一条​​独立​​指令 AtA_tAt​,它不需要加载的数据。
  3. 来自一个更早迭代的​​依赖加法​​指令 Ut−4U_{t-4}Ut−4​,它所需的数据来自 4 个周期前发射的加载指令 Lt−4L_{t-4}Lt−4​,而这个数据刚刚到达。

在这场优美的编排中,每个周期都是富有成效的。处理器从不空闲。它同时启动新的内存请求,使用旧请求的结果,并执行一些其他不相关的工作。这 4 个周期的内存延迟被完美地隐藏起来,使得处理器能够维持其峰值吞吐量。这就是超标量性能的艺术。

复杂性的物理代价

如果乱序执行如此强大,为什么不建造一个宽度为 W=1000W=1000W=1000 的处理器呢?答案在于物理学的严酷现实和二次方规模增长的“暴政”。乱序执行引擎的“大脑”是保留站(reservation stations)中的​​唤醒-选择逻辑(wakeup-select logic)​​,其复杂性令人咋舌。

  • ​​唤醒(Wakeup):​​ 当一条指令完成时,其结果标签通过​​公共数据总线(Common Data Bus, CDB)​​广播给保留站中每一条等待的指令(共 NNN 条)。每条等待的指令必须将广播的标签与自己缺失的操作数标签进行比较。这是一场大规模的匹配游戏。如果你的机器是 WWW 发射宽度,你最多可以同时广播 WWW 个结果,而 NNN 个条目中的每一个都必须监听所有这些结果。这样做的能量和布线成本与 N×WN \times WN×W 成正比。

  • ​​选择(Select):​​ 在下一瞬间,可能有多条指令同时“唤醒”,意识到它们现在已经拥有了所有操作数。一个中央仲裁器现在必须从这些就绪的指令中选择最多 WWW 条来发射,通常是选择最旧的那些。要在一个周期内完成此操作,每个就绪的候选指令基本上都必须与其他所有就绪的候选指令进行比较以确定其优先级。这种两两比较导致逻辑复杂度呈二次方增长,即 O(N2)\mathcal{O}(N^2)O(N2)。

这种二次方规模的增长是致命的。将保留站条目数(NNN)加倍,选择逻辑的复杂度不是加倍,而是变为四倍。该逻辑在物理上变得更大、更慢,并且功耗急剧增加。这就是为什么你无法建造一个无限宽的超标量处理器的根本原因。那个实现并行性的机制本身,最终变成了瓶颈。

此外,推测引擎尽管强大,却是浪费的。每当分支预测器出错,错误路径上所做的所有工作都被丢弃。这不仅是浪费时间,也是浪费能量和宝贵资源(如物理寄存器)的分配。在实际场景中,这部分被浪费的工作比例可能相当可观,常常超过 20%,这凸显了高精度分支预测的至关重要性。

因此,超标量处理器的故事是一个关于平衡的故事。它是在对无限并行性的渴望与硅、功耗和散热等无情物理约束之间做出的巧妙权衡。它的原理和机制代表了工程学的巅峰,是一场复杂而优美的舞蹈,旨在从我们编写的代码中榨取每一滴性能。

应用与跨学科联系

在前面的讨论中,我们打开了盒子,窥见了超标量处理器奇妙的内部构造。我们看到了它如何同时处理多条指令,乱序执行它们以保持其众多功能单元的繁忙。这是一台为单一目的而生的机器:发掘指令级并行(ILP)。但这不仅仅是一项局限于硅芯片的工程壮举;它是计算本质的一次根本性转变。这种并行性的存在改变了一切。它迫使我们重新思考如何编写软件、如何设计算法,甚至如何构建管理全局的操作系统。让我们踏上征程,看看超标量设计的涟漪如何广泛传播,将看似无关的领域在硬件与软件之间优美、统一的协作中连接起来。

编译器的艺术:编排指令

想象一位杰出的编舞家,负责指导一个杂技团队。这些杂技演员就是处理器的功能单元——加法器、乘法器和内存端口。编舞就是指令流。一个简单的线性步骤序列会让大多数杂技演员无所事事地站着。编舞家的天才之处在于找到可以同时表演的动作。这正是现代编译器的任务。

最基本的任务是在一小段直线型代码内调度指令。编译器查看指令序列及其依赖关系——谁需要谁的结果——并试图找到一个有效的调度,以最快速度完成工作。它必须是资源管理的专家。假设处理器有两个算术单元、一个加载单元和一个分支单元。编译器不能在一个周期内调度三个算术操作,即使它们都是独立的。它必须尊重这些被称为“端口约束”的硬件限制。有时,关键瓶颈并非硬件,而是数据本身。一条长长的依赖链,其中一个计算必须等待前一个计算完成,无论有多少执行单元可用,都可能使整个过程串行化。编译器的调度是代码中理想的并行性与硬件资源和数据依赖的严酷现实之间的微妙折中。

但是,当一个简单的代码块中的并行性被耗尽时,编译器从哪里能找到更多的并行性呢?它寻找模式,最常见的模式是循环。一个处理一百万个数据元素的循环包含一百万个并行机会,但它们常常被循环迭代之间的依赖关系所隐藏。考虑一个重复更新某个值的循环,比如计算一个累加和。第 i+1i+1i+1 次迭代的计算直接依赖于第 iii 次迭代的结果。这是一种“循环携带依赖(loop-carried dependence)”,它形成了一条长长的链条,束缚了处理器的手脚。

一个聪明的编译器可以使用一种称为​​循环展开(loop unrolling)​​的技术来打破这条链。它不是每次迭代运行一次循环体,而是将其“展开”,比如说,三次。现在,调度器可以同时看到三个原始迭代的指令。它无法打破对累加和的依赖,但它现在有了一个更大的、来自三个展开循环体的其他独立指令池,可以在等待关键依赖解决时执行。通过仔细选择展开因子,编译器可以提供恰到好处的独立工作来“隐藏”循环携带依赖的延迟,让处理器最终得以施展拳脚,接近其最大发射率。

编译器的任务并不仅限于选择和排序指令。代码在内存中的放置方式本身就能产生深远影响。超标量处理器的前端是一个贪婪的野兽,每个周期从内存中获取多条指令。但它通常有一个简单的限制:它只能从一个对齐的内存块(比如一个 8 字节的块)中获取指令。如果一个关键的循环或函数恰好从跨越两个这样块的地址开始,获取单元在第一个周期可能只能抓取一条指令,而不是两条或四条。这个看似微小的细节,一个“对齐错误”,会产生一个在整个流水线中逐周期传播的气泡,从而严重影响性能。一个了解这些架构特性的智能编译器或链接器,可以策略性地插入几个字节的填充——即什么都不做的 nop 指令——来确保重要的代码块从有利的对齐边界开始。这种整理代码布局的简单行为,可以显著提升处理器的实际吞吐量,即每周期指令数(IPC)。

机器的心智:对平衡的无尽追求

如果说编译器是编舞家,那么处理器架构师就是设计舞台并雇佣杂技演员的人。架构师的世界是一场持续的权衡和平衡游戏,其指导原则是每个科学家都熟知的:链条的强度取决于其最薄弱的一环。这是微架构版本的阿姆达尔定律(Amdahl's Law)。

假设一个处理器正在执行一个包含 50% 算术(ALU)指令的程序。如果机器的发射宽度为 W=4W=4W=4 但只有一个 ALU 单元,那么它的 IPC 永远不可能超过 2,因为平均而言,它每周期需要执行 T⋅0.5T \cdot 0.5T⋅0.5 条 ALU 指令,如果 TTT 是 4,它就需要 2 个 ALU。单个 ALU 成为了瓶颈。在这种情况下,将发射宽度增加到 W=5W=5W=5 对性能毫无帮助。你得到的只是一个更宽、更空的管道。然而,增加第二个 ALU 单元会立即缓解瓶颈,使 IPC 得以上升,直到达到下一个限制——可能是发射宽度或另一个功能单元。设计一个平衡的处理器意味着要仔细地将资源(功能单元、内存端口)与典型程序的预期指令构成相匹配。

架构师还采用巧妙的技巧来使机器更高效。考虑一个常见的指令模式:比较两个数,然后根据结果进行分支(CMP;BR)。在简单的设计中,这是两个操作。CMP 将其结果写入一个特殊的“条件码”寄存器,而 BR 读取它。这产生了一个微小的依赖,并占用了流水线中的两个槽位。许多现代处理器使用​​微操作融合(micro-op fusion)​​。解码器识别出这个指令对,并将它们融合成一个单一的、特殊的微操作。这个融合后的操作在内部执行比较并一次性确定分支方向。它只需要一个发射槽而不是两个,并且消除了跟踪条件码寄存器的需要,从而减轻了寄存器重命名硬件的压力。这个单一而优雅的优化可以通过提高前端效率来显著提升处理器的 IPC。

对并行性的渴求延伸到了内存系统。如果一个处理器每周期只能从内存中读取一个操作数,那么即使它每周期能执行两个算术操作也用处不大。为了解决这个问题,缓存通常是​​分体的(banked)​​。一个缓存可能被分成,比如说,8 个独立的体(bank),每个体都有自己的端口。这使得处理器在一个周期内最多可以处理 8 个内存请求,前提是它们都访问不同的体。但这产生了一种新的冒险:体冲突(bank conflict)。如果两条同时发生的加载指令碰巧需要来自同一个体的数据,其中一条就必须等待。处理器的乱序调度器现在必须玩另一个平衡游戏,不仅要在加载指令的依赖就绪时发射它们,还要在其目标体空闲时发射。对于某些内存访问模式,比如跨步访问数组,体冲突可能成为主要的性能瓶颈,无论处理器有多少个 ALU 或发射槽。

最后,现代架构师受制于一种比硅更根本的力量:物理学。每个操作都会消耗能量并产生热量。将处理器推向其理论上的最大 ILP 可能会产生不可持续的功耗。如今,性能几乎总是受限于​​功耗上限(power cap)​​。当处理器过热时,电源管理系统会介入并对其进行节流。它可能会通过在“占空比”中短暂禁用某些发射端口来实现这一点。如果一台 6 端口的机器被限制在一个仅允许大约 3 个端口能耗的功率水平上,系统可能会有效地将其变成一台 3 端口的机器。并行性仍然存在于代码中,但硬件为了避免熔化而被迫忽略它。性能与功耗之间的这种权衡是现代处理器设计的核心挑战。

超越核心:在算法和系统中的回响

超标量设计的原理具有深远的影响,其范围远远超出了芯片本身的限制,影响着我们软件的根本结构。

思考一下​​算法​​的世界。几十年来,算法效率是通过计算操作数来衡量的。一个需要 10n10n10n 步的算法被认为优于一个需要 2n22n^22n2 步的算法。在超标量机器上,这不再是故事的全部。计算的结构及其固有的并行性,与指令数量同等重要。

以计算多项式的简单问题为例。Horner 方法提供了一种用最少乘法次数完成此任务的优雅方式。它创建了一个串行依赖链:每一步都依赖于前一步的结果。在超标量处理器上,这是一场灾难。机器一次只能执行一步,其庞大的并行资源处于闲置状态。另一种方法,如 Estrin 方案,将计算重组为一个平衡树。它的总工作量稍多,但树的独立分支可以并行求值。对于一个 15 次的多项式,Estrin 方案在超标量机器上的速度可以比 Horner 方法快几倍,仅仅因为它提供了硬件渴望的并行性。对于像选择这样的基本算法也是如此。经典的 Quickselect 算法平均速度很快,但其核心分区步骤包含一个隐藏的串行依赖,将其 ILP 限制在一个很小的常数。相比之下,中位数的中位数算法虽然更复杂,但其寻找主元的阶段是高度并行的。它的并行性随输入规模的增长而增长,使其更适合宽的超标量架构。“最佳”算法不再是普适的真理;它取决于运行它的机器。

这种涟漪效应一直延伸到​​操作系统​​。操作系统使用抢占来制造多个程序同时运行的假象。一个时钟中断会停止一个进程并启动另一个进程——即“上下文切换”。在简单的处理器上,这种切换的成本只是保存和恢复一些寄存器的时间。在超标量处理器上,上下文切换是一场微架构的灾难。流水线被突然清空,所有正在处理的工作都被丢弃。精心“预热”过的分支预测器,已经学习了程序的跳转行为,现在变“冷”了,会对新进程频繁地错误预测,导致大规模停顿。充满旧进程数据和地址转换的缓存和 TLB 现在变得无用,必须通过缓慢的强制性未命中来重新填充。单次上下文切换的总成本不是几十个周期,而往往是数千个。这种深层次的隐藏成本展示了操作系统设计与硬件性能之间的强大联系,表明在最高层软件抽象中所做的决策,会对机器核心深处产生实实在在的后果。

从一条 add 指令到操作系统的宏大设计,对并行性的追求是将所有事物联系在一起的线索。超标量处理器不仅仅是更快地执行代码;它为我们的软件举起了一面镜子,揭示其隐藏的结构,并迫使我们去寻找其中潜藏的并行性。它告诉我们,计算不仅仅是一系列步骤,而是一幅由依赖关系和机遇构成的丰富、多层次的织锦,等待以最高效、最美丽的方式被编织在一起。