分支预测错误是否会刷新整个管道,即使是非常短的 if 语句体?
Does a branch misprediction flush the entire pipeline, even for very short if-statement body?
我读过的所有内容似乎都表明分支预测错误总是导致整个管道被刷新,这意味着很多周期被浪费了。我从来没有听过任何人提到任何简短 if 条件的例外情况。
这在某些情况下似乎真的很浪费。例如,假设您有一个单独的 if 语句,其主体非常简单,被编译为 1 CPU 条指令。 if 子句将通过一条指令编译成条件向前跳转。如果 CPU 预测分支不会被执行,那么它将开始执行 if-body 指令,并且可以立即开始执行后面的指令。现在,一旦对 if 条件的评估到达了管道的末端,比如说,12 个周期之后,CPU 现在知道它的预测是对还是错。如果它预测错误,并且实际上采用了分支,那么 CPU 实际上只需要从管道中丢弃 1 条指令(if-body 中的指令)。但是,如果它刷新了整个管道,那么在接下来的指令中所做的所有工作也被浪费了,并且将不得不无缘无故地重复。在深度流水线架构上浪费了很多周期。
现代 CPU 是否有任何机制可以仅丢弃短 if 主体中的少数指令?或者它真的冲洗了整个管道吗?如果是后者,那么我想使用条件移动指令会获得更好的性能。顺便说一句,有谁知道现代编译器是否擅长将短 if 语句转换为 cmov 指令?
至少对于大多数处理器来说,错误预测的分支确实会刷新整个管道。
这就是为什么许多(大多数?)当前处理器还提供谓词指令的主要原因。
在 ARM 上,大多数指令都是断言的,这意味着指令本身可以包含一个条件,本质上就是 "do X, but only if the following condition is true."
同样,x86/x64 的最近迭代包括一些谓词指令,例如 "CMOV"(条件移动),其工作方式相同——仅在满足条件时执行指令。
这些不会刷新流水线——指令本身总是流过流水线。如果条件不满足,指令基本上就没有任何效果。缺点是指令需要执行时间,即使它们没有效果。
因此,在您所讨论的情况下(if
语句主体很小)只能用几条指令来实现,您可以将它们作为谓词指令来实现。
如果正文接受了足够多的指令(大约是指令流水线的大小,乘以某个常数因子),那么使用条件跳转就更有意义了。
"如果它预测错误,并且分支确实被采用,那么 CPU
实际上只需要从管道中丢弃 1 条指令(if-body 中的指令)。"
这并不像您说的那么容易。指令修改其他指令所依赖的体系结构中的各种不同状态(寄存器结果、条件标志、内存等)。当您意识到自己预测错误时,您可能已经在管道中拥有大量指令,这些指令已根据该指令和管道中所有后续指令更改的状态开始执行......更不用说可以引发的指令faults/exceptions.
一个简单的例子:
b = 0
f (a == 0) {
b = 1;
}
c = b * 10;
if (b == 0)
printf("\nc = %d.",c);
foo(b);
etc..
撤消 "one simple instruction" 需要大量工作。
对于可预测性差的简单分支,首选predication/cmovs/etc。
大多数通用处理器会在分支预测错误时刷新管道。除了对分支预测(以及其他技术)。 (Mark Smotherman's page on eager execution 提供了一些细节和参考资料。我会添加 Hyesoon Kim 等人的 "Wish Branches: Combining Conditional Branching and Predication for Adaptive Predicated Execution",2005 年作为重要论文。)
IBM 的 POWER7 似乎是第一个实现比预取替代路径(即急切获取)更复杂的东西的主流处理器,它只处理单指令情况。 (POWER7 使用分支预测置信度估计来选择是谓词还是使用预测。)
Eager Execution 存在明显的资源使用爆炸问题。即使具有基于分支预测置信度、推测深度和资源可用性(前端可用信息)的 selective eagerness,在单个路径下更深入地推测也很容易更有效。发现多条路径的连接点并避免过多的冗余计算也会增加复杂性。 (理想情况下,与控制无关的操作只会执行一次,并且会优化连接和数据流,但这种优化会增加复杂性。)
对于深度流水线有序处理器,预测短前向分支未被采用并且仅在流水线中向后刷新到实际采用分支时采用的分支所针对的指令似乎很有吸引力。如果管道中一次只允许一个这样的分支(其他分支使用预测),则向每条指令添加一位可以控制它是转换为 nop 还是执行。 (如果只处理单个指令被分支的情况,那么在流水线中允许多个分支可能不会特别复杂。)
这类似于 annul-if-taken 分支延迟槽。 MIPS 有 "Branch Likely" 指令,如果 不 被采用,这些指令将被取消,并且这些指令在修订版 2.62 中被标记为过时。虽然这样做的一些理由可能是将实现与接口分开以及恢复指令编码的愿望 space,但这一决定也暗示该概念存在一些问题。
如果对所有短前向分支都这样做,当分支被正确预测为采用时,它会丢弃指令。 (请注意,如果采取的分支总是在获取重定向中遇到延迟,这种惩罚可能会更小,这更有可能在深度流水线处理器中进行多周期指令缓存访问。在这种情况下,就好像没有分支一样获取具有与正确预测的采纳分支相同的性能。但是,有人可能会争辩说处理器的特殊情况是这种短采纳分支以最小化此类提取气泡。)
作为一个例子,考虑一个标量流水线(每个周期的非分支指令等于 1.0),在第八阶段结束时进行分支解析,并且对正确预测的分支没有提取重定向惩罚,处理单指令分支-结束。假设此类短前向分支(2% 的指令,30% 的时间)的分支预测器准确度为 75%(不受方向影响),其他分支的准确度为 93%(18% 的指令)。对于被错误预测为已执行的短分支(此类分支的 17.5%;0.35% 的指令),将节省 8 个周期;如果被错误预测为未执行,则可节省 7 个周期(7.2%;0.144%);正确时将丢失 1 个周期预测为已采取 (22.5%; 0.45%)。每条指令总共可以节省 0.03358 个周期。如果没有这种优化,每条指令的周期数将为 1.2758。
(虽然以上数字仅供参考,但除了用于非分支指令的 1.0 IPC 之外,它们可能与实际情况相差不远。提供小型循环缓存将减少误预测惩罚(并在短循环中节省功率) 因为指令缓存访问可能是八个周期中的三个。添加缓存未命中的影响将进一步降低此分支优化的百分比改进。避免预测 "strongly taken" 短分支的开销 可能 值得。)
顺序设计倾向于使用较窄和较浅的管道,并且更喜欢简单性(为了降低设计、功耗和面积成本)。由于指令集很可能支持许多短分支情况下的无分支代码,进一步降低了优化这方面的动机。
对于乱序实现,必须预测可能分支的指令,因为处理器希望能够执行后面的非相关指令。预测引入了额外的数据依赖性,必须检查调度。指令调度程序通常只为每条指令提供两个比较器并拆分一个条件移动(一条只有三个数据流操作数的简单指令:旧值、替代值和条件;谓词寄存器-寄存器加法将具有四个操作数。(有其他方法可以解决这个问题,但这个答案已经很长了。)
当分支条件不可用时,乱序实现也不会停止。这是控制依赖性和数据依赖性之间的权衡。通过准确的分支预测,控制依赖性非常便宜,但数据依赖性可以阻止等待数据操作数的前进进程。 (当然,对于布尔数据依赖,价值预测变得更具吸引力。在某些情况下使用谓词预测可能是可取的,并且比使用动态成本和置信度估计的简单预测更有优势。)
(这或许表明 ARM 选择放弃 64 位 AArch64 中的广泛谓词。虽然其中很大一部分用于指令编码,但谓词对高性能实现的好处可能相对较低。)
编译器问题
无分支代码与分支代码的性能取决于分支的可预测性和其他因素(如果采用,包括重定向提取的任何惩罚),但编译器很难确定分支的可预测性。即使是配置文件数据通常也只提供分支频率,这可以给出可预测性的悲观观点,因为这不考虑使用局部或全局历史的分支预测器。编译器也不能完全了解数据可用性和其他动态方面的时间。如果条件晚于用于计算的操作数可用,则用数据依赖(预测)替换控制依赖(分支预测)可能会降低性能。无分支代码还可能引入更多实时值,可能会增加寄存器溢出和填充开销。
更复杂的是,大多数仅提供条件移动或 select 指令的指令集不提供条件存储。虽然这可以通过使用条件移动到 select 一个安全的、被忽略的存储位置来解决,但这似乎是一个没有吸引力的并发症。此外,条件移动指令通常比简单的算术指令更昂贵;加法和条件移动可能需要三个周期,其中正确预测的分支和加法将采用零(如果加法分支)或一个周期。
更复杂的是分支预测器通常会忽略预测操作。如果后来保留的分支与删除分支的条件相关,则该分支的分支预测错误率可能会增加。 (谓词预测可用于保留此类已删除分支的预测器效果。)
随着对向量化的日益重视,无分支代码的使用变得更加重要,因为基于分支的代码限制了对整个向量使用操作的能力。
Modern high-performance out-of-order CPUs 通常不会在错误预测时刷新整个管道0,但实际上并非如此取决于分支的距离或按照您的建议工作。
他们通常使用类似于刷新分支指令和所有较新指令的策略。 front-end 被刷新,这将在错误预测的路径上充满指令,但超出 front-end 现代内核可能有超过 100 条指令 in-flight一次,只有其中一些可能比分支更年轻。
这意味着分支的成本至少部分与周围的指令有关:如果分支条件可以检查早 mis-prediction的影响可以限制甚至为零1。另一方面,如果分支条件处理得晚,在错误路径上花费了大量资源之后,成本可能会很大(例如,大于 12-20 周期 "published" 分支错误预测惩罚,你会经常看到)。
0 确切的术语在这里有待讨论:冲洗管道 的含义对于 [=39 并不完全清楚=] 处理器。这里我的意思是 CPU 不会刷新所有 in-flight 但可能未执行的指令。
1 特别是,某些指令序列的限制因素可能是依赖链,其当前执行远远落后于指令的前沿 window错误预测不会刷新任何这些指令,也不会减慢代码速度。
我读过的所有内容似乎都表明分支预测错误总是导致整个管道被刷新,这意味着很多周期被浪费了。我从来没有听过任何人提到任何简短 if 条件的例外情况。
这在某些情况下似乎真的很浪费。例如,假设您有一个单独的 if 语句,其主体非常简单,被编译为 1 CPU 条指令。 if 子句将通过一条指令编译成条件向前跳转。如果 CPU 预测分支不会被执行,那么它将开始执行 if-body 指令,并且可以立即开始执行后面的指令。现在,一旦对 if 条件的评估到达了管道的末端,比如说,12 个周期之后,CPU 现在知道它的预测是对还是错。如果它预测错误,并且实际上采用了分支,那么 CPU 实际上只需要从管道中丢弃 1 条指令(if-body 中的指令)。但是,如果它刷新了整个管道,那么在接下来的指令中所做的所有工作也被浪费了,并且将不得不无缘无故地重复。在深度流水线架构上浪费了很多周期。
现代 CPU 是否有任何机制可以仅丢弃短 if 主体中的少数指令?或者它真的冲洗了整个管道吗?如果是后者,那么我想使用条件移动指令会获得更好的性能。顺便说一句,有谁知道现代编译器是否擅长将短 if 语句转换为 cmov 指令?
至少对于大多数处理器来说,错误预测的分支确实会刷新整个管道。
这就是为什么许多(大多数?)当前处理器还提供谓词指令的主要原因。
在 ARM 上,大多数指令都是断言的,这意味着指令本身可以包含一个条件,本质上就是 "do X, but only if the following condition is true."
同样,x86/x64 的最近迭代包括一些谓词指令,例如 "CMOV"(条件移动),其工作方式相同——仅在满足条件时执行指令。
这些不会刷新流水线——指令本身总是流过流水线。如果条件不满足,指令基本上就没有任何效果。缺点是指令需要执行时间,即使它们没有效果。
因此,在您所讨论的情况下(if
语句主体很小)只能用几条指令来实现,您可以将它们作为谓词指令来实现。
如果正文接受了足够多的指令(大约是指令流水线的大小,乘以某个常数因子),那么使用条件跳转就更有意义了。
"如果它预测错误,并且分支确实被采用,那么 CPU 实际上只需要从管道中丢弃 1 条指令(if-body 中的指令)。"
这并不像您说的那么容易。指令修改其他指令所依赖的体系结构中的各种不同状态(寄存器结果、条件标志、内存等)。当您意识到自己预测错误时,您可能已经在管道中拥有大量指令,这些指令已根据该指令和管道中所有后续指令更改的状态开始执行......更不用说可以引发的指令faults/exceptions.
一个简单的例子:
b = 0
f (a == 0) {
b = 1;
}
c = b * 10;
if (b == 0)
printf("\nc = %d.",c);
foo(b);
etc..
撤消 "one simple instruction" 需要大量工作。
对于可预测性差的简单分支,首选predication/cmovs/etc。
大多数通用处理器会在分支预测错误时刷新管道。除了对分支预测(以及其他技术)。 (Mark Smotherman's page on eager execution 提供了一些细节和参考资料。我会添加 Hyesoon Kim 等人的 "Wish Branches: Combining Conditional Branching and Predication for Adaptive Predicated Execution",2005 年作为重要论文。)
IBM 的 POWER7 似乎是第一个实现比预取替代路径(即急切获取)更复杂的东西的主流处理器,它只处理单指令情况。 (POWER7 使用分支预测置信度估计来选择是谓词还是使用预测。)
Eager Execution 存在明显的资源使用爆炸问题。即使具有基于分支预测置信度、推测深度和资源可用性(前端可用信息)的 selective eagerness,在单个路径下更深入地推测也很容易更有效。发现多条路径的连接点并避免过多的冗余计算也会增加复杂性。 (理想情况下,与控制无关的操作只会执行一次,并且会优化连接和数据流,但这种优化会增加复杂性。)
对于深度流水线有序处理器,预测短前向分支未被采用并且仅在流水线中向后刷新到实际采用分支时采用的分支所针对的指令似乎很有吸引力。如果管道中一次只允许一个这样的分支(其他分支使用预测),则向每条指令添加一位可以控制它是转换为 nop 还是执行。 (如果只处理单个指令被分支的情况,那么在流水线中允许多个分支可能不会特别复杂。)
这类似于 annul-if-taken 分支延迟槽。 MIPS 有 "Branch Likely" 指令,如果 不 被采用,这些指令将被取消,并且这些指令在修订版 2.62 中被标记为过时。虽然这样做的一些理由可能是将实现与接口分开以及恢复指令编码的愿望 space,但这一决定也暗示该概念存在一些问题。
如果对所有短前向分支都这样做,当分支被正确预测为采用时,它会丢弃指令。 (请注意,如果采取的分支总是在获取重定向中遇到延迟,这种惩罚可能会更小,这更有可能在深度流水线处理器中进行多周期指令缓存访问。在这种情况下,就好像没有分支一样获取具有与正确预测的采纳分支相同的性能。但是,有人可能会争辩说处理器的特殊情况是这种短采纳分支以最小化此类提取气泡。)
作为一个例子,考虑一个标量流水线(每个周期的非分支指令等于 1.0),在第八阶段结束时进行分支解析,并且对正确预测的分支没有提取重定向惩罚,处理单指令分支-结束。假设此类短前向分支(2% 的指令,30% 的时间)的分支预测器准确度为 75%(不受方向影响),其他分支的准确度为 93%(18% 的指令)。对于被错误预测为已执行的短分支(此类分支的 17.5%;0.35% 的指令),将节省 8 个周期;如果被错误预测为未执行,则可节省 7 个周期(7.2%;0.144%);正确时将丢失 1 个周期预测为已采取 (22.5%; 0.45%)。每条指令总共可以节省 0.03358 个周期。如果没有这种优化,每条指令的周期数将为 1.2758。
(虽然以上数字仅供参考,但除了用于非分支指令的 1.0 IPC 之外,它们可能与实际情况相差不远。提供小型循环缓存将减少误预测惩罚(并在短循环中节省功率) 因为指令缓存访问可能是八个周期中的三个。添加缓存未命中的影响将进一步降低此分支优化的百分比改进。避免预测 "strongly taken" 短分支的开销 可能 值得。)
顺序设计倾向于使用较窄和较浅的管道,并且更喜欢简单性(为了降低设计、功耗和面积成本)。由于指令集很可能支持许多短分支情况下的无分支代码,进一步降低了优化这方面的动机。
对于乱序实现,必须预测可能分支的指令,因为处理器希望能够执行后面的非相关指令。预测引入了额外的数据依赖性,必须检查调度。指令调度程序通常只为每条指令提供两个比较器并拆分一个条件移动(一条只有三个数据流操作数的简单指令:旧值、替代值和条件;谓词寄存器-寄存器加法将具有四个操作数。(有其他方法可以解决这个问题,但这个答案已经很长了。)
当分支条件不可用时,乱序实现也不会停止。这是控制依赖性和数据依赖性之间的权衡。通过准确的分支预测,控制依赖性非常便宜,但数据依赖性可以阻止等待数据操作数的前进进程。 (当然,对于布尔数据依赖,价值预测变得更具吸引力。在某些情况下使用谓词预测可能是可取的,并且比使用动态成本和置信度估计的简单预测更有优势。)
(这或许表明 ARM 选择放弃 64 位 AArch64 中的广泛谓词。虽然其中很大一部分用于指令编码,但谓词对高性能实现的好处可能相对较低。)
编译器问题
无分支代码与分支代码的性能取决于分支的可预测性和其他因素(如果采用,包括重定向提取的任何惩罚),但编译器很难确定分支的可预测性。即使是配置文件数据通常也只提供分支频率,这可以给出可预测性的悲观观点,因为这不考虑使用局部或全局历史的分支预测器。编译器也不能完全了解数据可用性和其他动态方面的时间。如果条件晚于用于计算的操作数可用,则用数据依赖(预测)替换控制依赖(分支预测)可能会降低性能。无分支代码还可能引入更多实时值,可能会增加寄存器溢出和填充开销。
更复杂的是,大多数仅提供条件移动或 select 指令的指令集不提供条件存储。虽然这可以通过使用条件移动到 select 一个安全的、被忽略的存储位置来解决,但这似乎是一个没有吸引力的并发症。此外,条件移动指令通常比简单的算术指令更昂贵;加法和条件移动可能需要三个周期,其中正确预测的分支和加法将采用零(如果加法分支)或一个周期。
更复杂的是分支预测器通常会忽略预测操作。如果后来保留的分支与删除分支的条件相关,则该分支的分支预测错误率可能会增加。 (谓词预测可用于保留此类已删除分支的预测器效果。)
随着对向量化的日益重视,无分支代码的使用变得更加重要,因为基于分支的代码限制了对整个向量使用操作的能力。
Modern high-performance out-of-order CPUs 通常不会在错误预测时刷新整个管道0,但实际上并非如此取决于分支的距离或按照您的建议工作。
他们通常使用类似于刷新分支指令和所有较新指令的策略。 front-end 被刷新,这将在错误预测的路径上充满指令,但超出 front-end 现代内核可能有超过 100 条指令 in-flight一次,只有其中一些可能比分支更年轻。
这意味着分支的成本至少部分与周围的指令有关:如果分支条件可以检查早 mis-prediction的影响可以限制甚至为零1。另一方面,如果分支条件处理得晚,在错误路径上花费了大量资源之后,成本可能会很大(例如,大于 12-20 周期 "published" 分支错误预测惩罚,你会经常看到)。
0 确切的术语在这里有待讨论:冲洗管道 的含义对于 [=39 并不完全清楚=] 处理器。这里我的意思是 CPU 不会刷新所有 in-flight 但可能未执行的指令。
1 特别是,某些指令序列的限制因素可能是依赖链,其当前执行远远落后于指令的前沿 window错误预测不会刷新任何这些指令,也不会减慢代码速度。