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

xeufq47z  于 2023-10-16  发布在  其他
关注(0)|答案(4)|浏览(233)

我对分支预测略知一二。这发生在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更感兴趣,但会很高兴得到任何答案。

sqyvllje

sqyvllje1#

分支预测与UB无关。

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

如果你的源代码没有调用UB,那么编译器必须发出在所有平台上都具有相同可观察行为的代码。
在C20中,通过likelylikely(参见preference),这与CPU执行的分支预测是分开的
它早在C
20之前就存在了,作为编译器扩展(例如GCC __builtin_expect),只是编译器的一个 * 提示 *,以帮助它更好地理解你的程序流。在“正常”编程中,它是一个很少使用的特性,你应该只在非常特定的情况下使用它,当它可以显着提高性能(例如编写操作系统内核或快速设备驱动程序的低级部分)
我更愿意建议关注语言本身(理解概念),而不是深奥的实现细节。

jecbmhm3

jecbmhm32#

但是,如果它不能正确预测,会发生什么?
它浪费时间和精力去做那些必须扔掉的工作。
这意味着ptr将为NULL,并且解引用它是UB。
不,语言不是这样的。编译器必须荣誉该解引用周围的保护(if语句)。
编译器必须荣誉 C++ ,句号!如果编译器生成一个空指针的推测性加载(可能在某些ISA上,如Itanium),这必须是有条件的和可验证的,因为程序明确表示不这样做。
同时,硬件必须荣誉伊萨,句号!如果硬件生成空指针的推测加载,那也必须是有条件的和可验证的,因为机器代码程序明确表示不这样做。
通过可能不太可能...这与CPU执行的分支预测是分开的
C++ 中的代码路径提示不一定转换为机器代码中的分支预测提示。
许多ISA没有(或其实现不使用)机器码分支方向提示。这是因为硬件分支预测已经变得如此之好,以至于它很早就完成了,并且不需要提示。为了使用提示,必须对指令进行解码,这比我们希望在处理器阶段进行预测的时间要晚。
C++编译器可以对这些提示做的是重新排列机器代码,使可能的路径是直的和连续的,而不可能的路径被重新定位在其他地方,不妨碍直路径。

vsikbqxv

vsikbqxv3#

推测执行的思想是,它对作为程序员的您是隐藏的。如果你想了解可能的实现方式,你可以看看他们如何在BOOM中进行投机执行。
访问空指针的C++操作可能会Map到类似于试图访问机器码中无效地址的内存。如果发生这种情况,则会发生TRAP,但如果是推测性地发生,我怀疑推测性执行将在发出陷阱之前等待分支被确认。
Boom文档对错误推测说了以下几点:
如果分支(或跳转)被误推测,则分支单元必须将PC重定向到正确的目标,终止前端和提取缓冲区,并广播误推测的分支标记,以便可以终止所有相关的、飞行中的UOP <微操作(UOP)。PC重定向信号立即发出,以减少误预测惩罚。然而,由于关键路径的原因,取消信号被延迟一个周期。

ws51t4hk

ws51t4hk4#

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

相关问题