我对分支预测略知一二。这发生在CPU上,与编译无关。尽管你可以告诉编译器一个分支是否比另一个更有可能,例如。在C++20中,通过[[likely]]
和[[unlikely]]
(参见cppreference),这与CPU执行的分支预测(参见Can I improve branch prediction with my code?)是分开的。
据我所知,当我有,例如。一个循环(有退出条件),CPU会预测退出条件不会被满足,并尝试在循环内执行一些操作,即使条件还没有被检查。如果CPU预测正确,它会节省一些时间,一切都很好。但是,如果它不能正确预测,会发生什么?我知道这将是一个性能打击,但我不知道如果一些已经完成的操作被丢弃或逆转,或者只是它如何处理它。
我举两个简单的例子。第一个(如果我们忽略编译器可能只是在编译时计算总和,我认为没有优化发生)应该很容易预测CPU。循环条件在整个时间内都是相同的,并且循环中的条件仅切换一次。这意味着预测将为我们带来很好的性能提升,即使它失败了几次,添加一个数字也可以很容易地逆转。
在第二个例子中,退出条件也很容易预测。在循环体中,我通过malloc
分配了一个新的int
数组。请注意,我不是故意释放它的,因为我希望分配成功很长一段时间,所以CPU预测这一成功。有时,当我用完内存(我没有计算总的内存消耗,并假设内存不会被移动到磁盘)或发生其他错误时,分配会失败。这意味着ptr
将是NULL
,解引用它是UB。它没有定义发生了什么,它可能只是一个无操作,崩溃我的程序或导致我的电脑飞走。因此,我得出结论,CPU不能只是撤消,我想知道会发生什么。
#include <stdlib.h>
#define VERSION 1
#if VERSION == 1
int main() {
size_t sum = 0ull;
for (size_t i = 0ull, max = 1'000ull; i < max; ++i) {
if (i < (max / 2)) {
sum += 2 * i;
}
else {
sum += i;
}
}
return 0;
}
#else
int main() {
int* ptr = NULL;
for (size_t i = 0ull, max = 1'000'000ull; i < max; ++i) {
ptr = (int*)malloc((sizeof * ptr) * 1'000ull);
if (ptr) {
*ptr = 1234;
}
// free(ptr)
}
return 0;
}
#endif
分支预测是CPU的任务,UB显然存在于C和C中,所以我认为这个问题的答案不需要一种特定的语言,我的代码应该可以在两种语言中工作。然而,如果选择的语言有所不同,我对C比C更感兴趣,但会很高兴得到任何答案。
4条答案
按热度按时间sqyvllje1#
分支预测与UB无关。
UB是从实际实现中抽象出来的C或C++语言概念。它只能在源代码级别上进行分析。如果你的代码中有一个UB,那么编译器基本上可以自由地做它想做的事情,因为标准没有指定在这种情况下应该发生什么。
如果你的源代码没有调用UB,那么编译器必须发出在所有平台上都具有相同可观察行为的代码。
在C20中,通过likely和likely(参见preference),这与CPU执行的分支预测是分开的
它早在C20之前就存在了,作为编译器扩展(例如GCC
__builtin_expect
),只是编译器的一个 * 提示 *,以帮助它更好地理解你的程序流。在“正常”编程中,它是一个很少使用的特性,你应该只在非常特定的情况下使用它,当它可以显着提高性能(例如编写操作系统内核或快速设备驱动程序的低级部分)我更愿意建议关注语言本身(理解概念),而不是深奥的实现细节。
jecbmhm32#
但是,如果它不能正确预测,会发生什么?
它浪费时间和精力去做那些必须扔掉的工作。
这意味着ptr将为NULL,并且解引用它是UB。
不,语言不是这样的。编译器必须荣誉该解引用周围的保护(if语句)。
编译器必须荣誉 C++ ,句号!如果编译器生成一个空指针的推测性加载(可能在某些ISA上,如Itanium),这必须是有条件的和可验证的,因为程序明确表示不这样做。
同时,硬件必须荣誉伊萨,句号!如果硬件生成空指针的推测加载,那也必须是有条件的和可验证的,因为机器代码程序明确表示不这样做。
通过可能和不太可能...这与CPU执行的分支预测是分开的
C++ 中的代码路径提示不一定转换为机器代码中的分支预测提示。
许多ISA没有(或其实现不使用)机器码分支方向提示。这是因为硬件分支预测已经变得如此之好,以至于它很早就完成了,并且不需要提示。为了使用提示,必须对指令进行解码,这比我们希望在处理器阶段进行预测的时间要晚。
C++编译器可以对这些提示做的是重新排列机器代码,使可能的路径是直的和连续的,而不可能的路径被重新定位在其他地方,不妨碍直路径。
vsikbqxv3#
推测执行的思想是,它对作为程序员的您是隐藏的。如果你想了解可能的实现方式,你可以看看他们如何在BOOM中进行投机执行。
访问空指针的C++操作可能会Map到类似于试图访问机器码中无效地址的内存。如果发生这种情况,则会发生TRAP,但如果是推测性地发生,我怀疑推测性执行将在发出陷阱之前等待分支被确认。
Boom文档对错误推测说了以下几点:
如果分支(或跳转)被误推测,则分支单元必须将PC重定向到正确的目标,终止前端和提取缓冲区,并广播误推测的分支标记,以便可以终止所有相关的、飞行中的UOP <微操作(UOP)。PC重定向信号立即发出,以减少误预测惩罚。然而,由于关键路径的原因,取消信号被延迟一个周期。
ws51t4hk4#
未定义的行为只是编程语言中的一个概念。CPU需要执行以汇编代码编写的程序(例如,由编译器生成)。然而,预期行为的完整定义根本不清楚。例如,由于推测性执行和分支误预测,CPU可能会做汇编代码没有表达的事情,这些事情在结果中不可见,但其影响可以通过计时观察到。这就是导致Spectre等漏洞的原因。