未定义的行为是否真的有助于现代编译器优化生成的代码?

问题描述 投票:2回答:4

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

看下面的代码:

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 ++标准是否允许这样的优化,因为它排除了内存分配/解除分配步骤,但让我们想一想C ++ - 就像允许这种优化的语言一样。)

而且,好的,这种优化不如下一个代码快:

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

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

请注意,我的问题与这个问题没什么不同:Is undefined behavior worth it?问题是:性能改进的优势是否超过未定义行为的缺点。它假定未定义的行为确实有助于优化代码。我的问题是:是否有可能在没有未定义行为的语言中实现几乎相同(可能少一点)的优化级别,就像在具有未定义行为的语言中一样。

我能想到的唯一一种不确定行为可以真正帮助提高性能的案例是手动内存管理。您永远不知道指针指向的地址是否未被释放。有人可以拥有指针的副本,而不是调用它上面的free。您的指针仍指向同一地址。要避免这种未定义的行为,您必须使用垃圾收集器(它有自己的缺点)或者必须维护指向该地址的所有指针的列表,并且当释放地址时,您必须使所有这些指针无效(和在访问之前检查它们是否有null)。

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

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

c++ compilation compiler-optimization undefined-behavior
4个回答
2
投票

我的问题是:是否有可能在没有未定义行为的语言中实现几乎相同(可能少一点)的优化级别,就像在具有未定义行为的语言中一样。

是的,使用类型安全的语言。诸如C和C ++之类的语言需要精确定义未定义行为的概念,因为它们不是类型安全的(这基本上意味着任何指针都可以随时随地指向),因此在很多情况下,编译器无法静态证明没有违反语言规范可以在程序的任何执行中发生,即使实际情况也是如此。这是因为指针分析的硬性限制。如果没有未定义的行为,编译器必须插入太多动态检查,其中大部分都不是真正需要的,但是编译器无法解决这个问题。

例如,考虑安全的C#代码,其中函数接受指向某种类型的对象(数组)的指针。由于语言和底层虚拟机的设计方式,保证指针指向期望类型的对象。这是静态确保的。在某些情况下,C#发出的代码仍然需要边界和类型动态检查,但与C / C ++相比,实现完全定义的行为所需的动态检查数量很少,而且通常是可以承受的。许多C#程序可以达到与相应C ++程序相同或稍低的性能。虽然这在很大程度上取决于如何编译。

我能想到的唯一一种不确定行为可以真正帮助提高性能的案例是手动内存管理。

这不是上面解释的唯一情况。

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

不确定你的意思。该语言指定的内存模型定义了多线程程序的行为。这些模型的范围可以从非常宽松到非常严格(例如,参见C ++内存模型)。


1
投票

对于第一个例子,它不会明显超出范围,编译器; at()函数是一个黑盒子,在尝试访问向量数组之前可能会向我添加200。那将是愚蠢的,但有时程序员是愚蠢的。它看起来很明显,因为你知道模板不会这样做。如果at()被声明为内联,则稍后的窥孔优化阶段可以执行那种边界检查跳过,但这是因为该函数在该点处是打开的框,因此它可以访问向量边界并且循环仅涉及常量。


0
投票

你的例子就是一个例子。在您的示例中,使用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;
}

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


0
投票

在许多情况下,最佳代码生成将需要一些构造,程序员可以通过这些构造邀请编译器来承担某些事情,如果事实证明它们不真实则会产生不可预测的后果。此外,在某些情况下,执行任务的最有效方式是不可验证的。但是,如果所有数组都标有长度,那么就没有必要让语言处理越界数组访问调用UB [而不是陷阱]如果语言有一个构造,例如

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

然后它可以默认检查数组边界,而不会丢失使用未经检查的访问可用的优化。为了允许在许多情况下需要确保程序在执行任何“坏”之前关闭,但这种关闭的确切时间无关紧要,语言可能包括CHECKED_ASSUME假设,如此给定例如

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

一个编译器将被允许在任何时候致命陷阱它可以确定代码将使用x>Arr.length调用或首先击中一些其他致命陷阱。如果上面的代码出现在循环中,则使用CHECKED_ASSUME而不是ASSERT将邀请编译器将检查移出循环。

虽然C编译器的维护者坚持认为无约束UB是优化所必需的,但在某些狭窄环境之外的设计良好的语言中则不然。

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