未定义的行为确实有助于现代编译器优化生成的代码

Does undefined behavior really help modern compilers to optimize generated code?

本文关键字:优化 编译器 代码 有助于 未定义      更新时间:2023-10-16

现代编译器不够聪明,无法同时生成快速安全的代码?

查看下面的代码:

std::vector<int> a(100);
for (int i = 0; i < 50; i++)
    { a.at(i) = i; }
...

很明显,范围错误在这里永远不会发生,并且智能编译器可以生成下一个代码:

std::vector<int> a(100);
for (int i = 0; i < 50; i++)
    { a[i] = i; } // operator[] doesn't check for out of range
...

现在让我们检查此代码:

std::vector<int> a(unknown_function());
for (int i = 0; i < 50; i++)
    { a.at(i) = i; }
...

它可以更改为如此等效的:

std::vector<int> a(unknown_function());
size_t __loop_limit = std::min(a.size(), 50);
for (int i = 0; i < __loop_limit; i++)
    { a[i] = i; }
if (50 > a.size())
    { throw std::out_of_range("oor"); }
...

另外,我们知道int类型在其破坏者和分配操作员中没有副作用。因此,我们可以将代码转换为下一个等价:

size_t __tmp = unknown_function();
if (50 > __tmp)
    { throw std::out_of_range("oor"); }
std::vector<int> a(__tmp);
for (int i = 0; i < 50; i++)
    { a[i] = i; }
...

(我不确定C 标准允许使用此类优化,因为它不包括内存分配/DealLocation步骤,但让我们想一想C - 喜欢允许这种优化的语言。(

,好吧,此优化不如下一个代码快:

std::vector<int> a(unknown_function());
for (int i = 0; i < 50; i++)
    { a[i] = i; }

因为还有一个额外的检查if (50 > __tmp),如果您确定unknown_function永远不会返回小于50的值,您确实不需要,但是在这种情况下,性能改进不是很高。

请注意,我的问题与这个问题没有什么不同:不确定的行为值得吗?这个问题是:绩效改善的优势是否超过了不确定的行为的缺点。它假设未定义的行为确实有助于优化代码。我的问题是:是否有可能在没有不确定行为的语言中实现一种几乎相同(也许少(的语言优化水平。

我唯一能想到的不确定的行为可以真正有助于显着提高性能的地方是手动记忆管理。您永远不会知道指针指向的地址是否没有释放。有人可以拥有指针的副本,而不是在上面拨打free。您的指针仍然指向相同的地址。为了避免这种不确定的行为,您要么必须使用垃圾收集器(具有自己的缺点(,要么必须维护所有指向地址的指示的列表,并且当释放地址时,您必须取消所有这些指示器(and(在访问它们之前,请检查它们是否有null(。

为多线程环境提供定义的行为可能也可能导致性能成本。

ps我不确定在 c 的语言中是否可以实现定义的行为,但也将其添加到标签中。

在第一个示例中,它并不明显,它会超出编译器;AT((函数是一个黑匣子,在尝试访问向量数组之前,很可能会向I添加200个i。那会很愚蠢,但有时程序员很愚蠢。看起来很明显,因为您知道模板没有这样做。如果at((被列为内联声明,则稍后的窥视孔优化阶段可能会执行此类范围跳过,但这是因为该函数是当时打开的框,因此它可以访问向量范围,并且该循环仅涉及常数。

在许多情况下,最佳代码生成将需要一些通过程序员可以邀请编译器承担某些事情的构造,如果他们没有结果,则会产生不可预测的后果是真实的。此外,在某些情况下,执行任务的最有效方法将无法验证。但是,如果所有数组均以长度标记,则无需使用语言处理范围内的数组来访问undoke ub [而不是捕获语言,而如果语言具有构造,例如。

UNCHECKED_ASSUME(x < Arr.length);
Arr[x] += 23;

然后,它可以默认情况下检查数组界限而不会丢失优化使用未选中的访问可以使用。为了允许许多情况必须确保程序在做任何"不好"之前会被关闭,但是这种关闭的确切时机并不重要,一种语言可以包括checked_assume假设,以便给定例如

CHECKED_ASSUME(x < Arr.length);
Arr[x] += 23;

可以随时允许编译器致命陷阱,以确定该代码将被x>Arr.length调用或首先击中其他一些致命的陷阱。如果以上代码要出现在循环中,则使用checked_assume而不是断言会邀请编译器将签出移出循环。

虽然C编译器的维护者坚持认为不受约束的UB是优化所必需的,但在某些狭窄情况之外,这在精心设计的语言中是不正确的。

您的示例只是一个示例。在您的示例中,使用operator[]代替at的性能增益可能很小,但是在许多其他情况下,不确定的行为带来的性能增益可能很大。

例如,只需考虑以下代码

std::vector<int> a(100);
std::vector<int>::size_type index;
for (int i = 0; i != 100; ++i) {
    std::cin >> index;
    a.at(index) = i;
}

对于此代码,编译器必须检查每次迭代中的界限,这可能是相当大的成本。