分支预测和UB(未定义行为)

问题描述 投票:0回答:3

我对分支预测了解一点。这发生在 CPU 上,与编译无关。尽管您可能能够告诉编译器一个分支是否比另一个分支更有可能,例如在 C++20 中,通过

[[likely]]
[[unlikely]]
(请参阅 cppreference),这与 CPU 执行的分支预测是分开的(请参阅我可以用我的代码改进分支预测吗?)。

据我所知,当我有例如一个循环(带有退出条件),CPU 将预测退出条件不会满足,并尝试在循环内执行某些操作,即使尚未检查条件。如果 CPU 预测正确,它会节省一些时间,一切都会好起来。然而,如果它无法正确预测会发生什么?我知道这会对性能造成影响,但我不知道一些已经完成的操作是否被丢弃或逆转,或者它如何处理它。

现在我想出了两个简单的例子。第一个(如果我们忽略编译器可能只是在编译时计算总和并且我假设没有发生优化)对于 CPU 来说应该很容易预测。循环条件始终相同,并且循环中的条件仅切换一次。这意味着预测将为我们带来很好的性能提升,即使它失败了几次,添加一个数字也可以轻松逆转。

在第二个示例中,退出条件再次很容易预测。在循环体中,我通过

int
 分配一个新的 
malloc
数组。请注意,我不是故意释放它的,因为我希望分配能够长期成功,以便 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 更感兴趣,但会很高兴获得任何答案。

c++ c undefined-behavior branch-prediction
3个回答
0
投票

未定义行为只是编程语言的一个概念。 CPU 需要执行以汇编代码编写的程序(例如由编译器生成)。然而,预期行为的完整定义根本不清楚。例如,由于推测执行和分支错误预测,CPU 可能会执行汇编代码未表达的操作,这些操作在结果中不可见,但其效果可通过计时观察到。这就是导致诸如 Spectre 之类的漏洞的原因。


0
投票

分支预测与UB没有任何关系。

UB是从实际实现中抽象出来的C或C++语言概念。只能在源代码层面进行分析。如果你的代码中有 UB,那么编译器基本上可以自由地做它想做的事情,因为标准没有指定在这种情况下应该发生什么

如果您的源代码不调用 UB,则编译器必须发出代码,该代码(执行时)将在所有平台上具有相同的可观察行为

在 C++20 中通过 [[likely]] 和 [[unlikely]] (参见首选项)这是 与 CPU 执行的分支预测分开

它早在 C++20 之前就已经作为编译器扩展存在了(例如 GCC

__builtin_expect
),并且只是对编译器的提示,以帮助他更好地理解你的程序流程。在“正常”编程中,这是一个很少使用的功能,您应该仅在非常特定的情况下使用它,当它可以显着提高性能时(例如编写操作系统内核的低级部分或快速设备驱动程序)

我宁愿建议关注语言本身(理解概念)而不是深奥的实现细节。


-1
投票

推测执行的想法是,它对程序员来说是隐藏的。如果您想了解可能的实现方式,您可以例如看看他们如何在 BOOM 中进行推测执行。

访问空指针的 C++ 操作可能会映射到尝试访问机器代码中无效地址处的内存。如果发生这种情况,则会发生 TRAP,但如果是推测性发生,我怀疑推测执行会在发出陷阱之前等待分支得到确认。

Boom 文档对错误推测做了以下说明:

如果分支(或跳转)被错误推测,分支单元必须将 PC 重定向到正确的目标,终止前端和获取缓冲区,并广播错误推测的分支标签,以便所有依赖的、正在运行的 UOP

© www.soinside.com 2019 - 2024. All rights reserved.