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

时间:2018-02-27 05:36:26

标签: c++ compilation compiler-optimization undefined-behavior

Aren的现代编译器足够聪明,能够同时生成快速安全的代码吗?

请看下面的代码:

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 的语言中是否可以实现定义的行为,但也将其添加到标签中。

4 个答案:

答案 0 :(得分:2)

  

我的问题是:是否有可能实现几乎相同(也许   没有未定义的语言中的优化级别   行为与具有未定义行为的语言一样。

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

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

  

唯一可以想到未定义行为可以真正帮助的案例   手动内存管理显着提高性能。

这不是上面解释的唯一案例。

  

可能为多线程环境提供已定义的行为   导致绩效成本。

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

答案 1 :(得分:1)

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

答案 2 :(得分: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;
}

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

答案 3 :(得分:0)

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

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

然后它可以默认检查数组边界而不会丢失优化 使用未经检查的访问可以使用。为了允许在许多情况下需要确保程序在执行任何事情之前关闭,并且#34;不好&#34;,但这种关闭的确切时间并不重要,一种语言可能包括一个CHECKED_ASSUME假设,例如

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

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

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