【问题标题】:Why don't compilers use asserts to optimize? [closed]为什么编译器不使用断言来优化? [关闭]
【发布时间】:2014-10-16 11:10:37
【问题描述】:

以下伪 C++ 代码:

vector v;
... filling vector here and doing stuff ...
assert(is_sorted(v));
auto x = std::find(v, elementToSearchFor);

find 具有线性运行时间,因为它是在一个可以未排序的向量上调用的。但是在该特定程序的那一行,我们知道:程序不正确(如:如果断言失败,它不会运行到最后)或要搜索的向量已排序,因此允许二进制搜索@987654323 @ 与 O(log n)。将其优化为二进制搜索应该由一个好的编译器完成。

这只是迄今为止我发现的最简单的最坏情况行为(更复杂的断言可能允许更多优化)。

有些编译器会这样做吗?如果是,是哪些?如果没有,他们为什么不呢?

附录:一些高级语言可能很容易做到这一点(尤其是在 FP 的情况下),所以这更多是关于 C/C++/Java/类似语言

【问题讨论】:

  • 没有v.find(),所以不清楚你在问什么。
  • 你的代码假设它会在那个时候被排序,那么为什么不只做一个二进制搜索呢?有很多库可以做到这一点,为什么要依赖编译器?
  • 没有编译器会用std::binary_search 替换对std::find 的调用。首先是因为它不能直接互换(返回迭代器与返回布尔值),其次是因为它只能在编译时存在知识的情况下才能完成,而且这种情况很少见。在这种情况下,期望用户使用正确的功能。
  • @Nephtys 我很确定什么时候(不是如果,而是什么时候)编译器变得如此聪明以至于他们可以像这样进行代码分析,那么就没有工作了。
  • 这个例子太复杂了。也许从assert(x>3); if(x>0)... 开始,这很简单,有希望。

标签: java c++ optimization compiler-construction


【解决方案1】:

赖斯定理基本上表明,代码的非平凡属性通常无法计算。

is_sorted 为真和运行更快的搜索而不是线性搜索之间的关系是断言is_sorted 后程序的一个重要属性。

您可以安排is_sorted 之间的显式连接以及使用各种更快算法的能力。在 C++ 中将此信息传达给编译器的方式是通过类型系统。也许是这样的:

template<typename C>
struct container_is_sorted {
  C c;
  // forward a bunch of methods to `c`.
};

然后,您将调用基于容器的算法,该算法将在大多数容器上使用线性搜索,或对包装在 container_is_sorted 中的容器进行排序搜索。

这在 C++ 中有点尴尬。在一个系统中,变量可以在同一代码流中的不同点(在操作下发生变异的类型)携带不同的编译器已知的类型信息,这会更容易。

也就是说,假设 C++ 中的类型有一系列标签,例如 int{positive, even},您可以附加到它们上,并且可以更改标签:

int x;
make_positive(x);

对没有主动保留标签的类型的操作会自动丢弃它。

然后assert( {is sorted}, foo ) 可以将标签{is sorted} 附加到foo。然后,以后的代码可以使用foo 并拥有该知识。如果你在foo 中插入了一些东西,它会丢失标签。

这样的标签可能是运行时(有成本,但在 C++ 中不太可能)或编译时(在这种情况下,给定变量的标签状态必须在代码中的给定位置静态确定) .

在 C++ 中,由于这些东西的笨拙,我们习惯性地在 cmets 中简单地记录它和/或使用完整的类型系统来标记事物(右值与左值引用是一个被折叠到适当语言中的示例) .

因此,程序员应该知道它已排序,并在知道它已排序的情况下调用适当的算法。

【讨论】:

  • 我喜欢这个答案。您准确地描述了使用这些额外的元数据可以做什么,并且在讨论支持 JIT 运行时编译语言时,这也是一个完美的论据。例如,.NET(以及更多的语言运行时)允许这样的标签,然后可以使用这些标签在这方面进行优化(即使大多数 JIT 编译器没有在这种规模上这样做)。
  • @Nephtys:这根本不是使用 JIT 的理由。还需要向 JIT 教授“排序”和其他所有内容。使用 JIT 所能做的,而静态编译无法做到的,只是基于运行时已知但编译时不知道的东西进行优化,这与您的示例无关。
  • @tmyklebu 纠正我自己:它允许实现这样的“标签”作为添加到对象的运行时数据,而不是在编译时基于静态类型(这将是 C++ 中的#1 解决方案,将排序后的向量转换为某种sorted_vector,甚至使用模板作为标签)。这是相同的想法,但在运行时更容易计算。
  • @Nephtys:您似乎在寻找某种依赖类型系统。 JIT 与否与这里的任何事情无关。
  • @Nephtys JIT 与允许标记的类型系统正交。即使在具有数据动态标签的系统中没有 JIT,您也可以拥有动态标签,并让算法的运行时调度读取所述标签。但这基本上是添加一个表示“已排序”的布尔值并在向量上清除它,并让查找代码检查它。自动排列这些东西(生产和消费标签)基本上是不可能的,ala Rice 定理。必须有人手动编码关系。
【解决方案2】:

嗯,答案分为两部分。

首先,我们来看看断言:

7.2 诊断&lt;assert.h&gt;

1 标头定义了 assert 和 static_assert 宏以及 引用另一个宏,

NDEBUG

这不是由&lt;assert.h&gt; 定义的。如果NDEBUG 被定义为源文件中包含&lt;assert.h&gt; 的位置的宏名称,则断言宏被简单地定义为

#define assert(ignore) ((void)0)

每次包含&lt;assert.h&gt;时,都会根据NDEBUG的当前状态重新定义断言宏。
2 assert 宏应实现为宏,而不是实际功能。如果为了访问实际函数而抑制宏定义,则行为未定义。

因此,在发布模式下没有任何东西可以给编译器任何可以假定某些条件成立的提示。
尽管如此,没有什么能阻止您自己在发布模式下使用实现定义的__assume 重新定义assert(看看clang / gcc 中的__builtin_unreachable())。

假设您已经这样做了。现在,测试的条件可能非常复杂且昂贵。因此,您真的想对其进行注释,这样它就不会导致任何运行时工作。不知道该怎么做。

让我们承认你的编译器甚至允许任意表达式。


下一个障碍是识别表达式实际测试的内容,以及它与编写的代码之间的关系,以及任何可能更快但在给定假设下等效的代码。

这最后一步导致编译器复杂性的巨大爆炸,要么必须创建所有这些模式的显式列表以进行测试,要么构建一个非常复杂的自动分析器。
这不好玩,而且和构建 SkyNET 一样复杂。

此外,您真的不想在数据集上使用渐近更快的算法,因为数据集太小以至于渐近时间无关紧要。那将是一种悲观情绪,而您只需要预先知道就可以避免这种情况。

【讨论】:

  • 我会把钱花在 GCC 和 Clang 上,而忽略了 if (!x) __builtin_unreachable(); 中对 x 的评估。这显然是死代码,在更简单的情况下会被淘汰。所以条件测试,只要没有副作用,应该不会导致运行时工作。
  • @tmyklebu:他们传播假设支持执行流程,是的。但这可能不足以消除对x 的评估,例如,如果它调用了一些编译器不可用的函数,因此既不能内联也不能分析。另外,它可能太复杂了,所以优化并没有全部内爆。
【解决方案3】:
  1. 断言(通常)在最终代码中编译出来。这意味着,如果不满足断言,代码可能会(静默地)由于这种优化而失败(通过检索错误的值)。
  2. 如果程序员(将断言放在那里)知道向量已排序,他为什么不使用不同的搜索算法?让编译器以这种方式猜测程序员有什么意义?
  3. 鉴于它们都是库例程,而不是语言语义的一部分,编译器如何知道要替换哪个搜索算法?

【讨论】:

  • “断言(通常)在最终代码中编译出来。” - 通常,是的。但这是幸运的吗?我不这么认为。但是,您的第二点是成立的。
【解决方案4】:

你说的是“编译器”。但是编译器不是为了为您编写更好的算法而存在的。他们在那里编译你写的东西。

您可能会问是否应该实现库函数std::find 以潜在地寻找它是否可以执行除使用线性搜索之外的算法。实际上,如果用户传入了std::set 迭代器甚至std::unordered_set,并且STL 实现者知道这些迭代器的详细信息并且可以使用它,但一般情况下不是这样,vector 也不是。

assert 本身只适用于调试模式,发布模式通常需要优化。此外,失败的assert 会导致中断而不是库切换。

本质上,提供了一些集合以加快查找速度,由程序员来选择它,而不是库编写者尝试猜测程序员真正想要做什么。 (而且在我看来,编译器更不这样做)。

【讨论】:

    【解决方案5】:

    从狭义上讲,答案是如果可以的话,他们会这样做,但大多数情况下他们不能,因为语言不是为此而设计的,并且断言表达式太复杂。

    如果 assert() 被实现为宏(就像在 C++ 中一样),并且它没有被禁用(通过在 C++ 中设置 NDEBUG)并且可以在编译时评估表达式(或可以跟踪数据)然后编译器将应用其通常的优化。这种情况并不经常发生。

    在大多数情况下(当然在您给出的示例中),assert() 和所需优化之间的关系远远超出了编译器在没有语言帮助的情况下所能做到的。鉴于 C++(和 Java)的元编程能力水平非常低,这样做的能力非常有限。

    从更广泛的意义上说,我认为您真正需要的是一种语言,程序员可以在其中断言代码的意图,编译器可以从中选择不同的翻译(和算法)。已经有实验性语言尝试这样做,而 Eiffel 在这方面有一些特性,但我现在知道任何主流编译语言都可以做到这一点。

    【讨论】:

      【解决方案6】:

      将其优化为二进制搜索应该由一个好的编译器来完成。

      不!线性搜索会产生更可预测的分支。如果数组足够短,线性搜索是正确的做法。

      除此之外,即使编译器想要,它必须知道的想法和概念的列表将是巨大的,并且它必须对它们进行非平凡的逻辑。这会变得非常缓慢。编译器经过精心设计,可以快速运行并生成不错的代码。

      您可能会花一些时间使用形式验证工具,这些工具的工作是找出他们所能输入的代码的所有信息,哪些断言可能会出错,等等。它们通常是在没有编译器相同的速度要求的情况下构建的,因此它们在弄清楚程序方面要好得多。您可能会发现,对代码进行严格推理比乍看之下更难。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-12-13
        • 1970-01-01
        • 1970-01-01
        • 2015-09-15
        • 2015-05-13
        • 1970-01-01
        • 2010-09-21
        • 1970-01-01
        相关资源
        最近更新 更多