【问题标题】:Example of compiler optimizations that can be 'easily' done on C++ code but not C code可以在 C++ 代码而非 C 代码上“轻松”完成的编译器优化示例
【发布时间】:2012-04-09 21:19:30
【问题描述】:

这个问题谈到了在 C 中无法轻易实现的排序功能的优化: Performance of qsort vs std::sort?

与 C++ 相比,是否有更多在 C 中不可能或至少难以实现的编译器优化示例?

【问题讨论】:

  • 这与优化无关。它是关于可以轻松表达的抽象同时仍然发出最佳代码。
  • 没有什么 (AFAIK) 阻止编译器编写者通过函数指针内联(假设目标在同一个翻译单元中),他们只是选择不这样做,无论出于何种原因。
  • @Oli 仅当他们能够证明函数指针从未更改时,这在编译时可能通常不容易做到(并且对于 qsort 几乎是不可能的)。但可以肯定他们可以——当内联虚拟 valls 时,JVM 基本上做同样的事情,它们只是在那里有一些优势。
  • @Oli: 至少 gcc 和 clang do 通过函数指针内联 - 你只需要注意如何分解代码

标签: c++ c optimization compiler-optimization


【解决方案1】:

正如@sehe 在评论中提到的那样。它与抽象有关。换句话说,如果该语言允许编码人员更好地表达意图,那么它可以发出以更优化的方式实现该意图的代码。

一个简单的例子是std::fill。当然对于基本类型,您可以使用 memset,但是,假设它是一个 32 位 unsigned longs 的数组。 std::fill 知道数组大小是 32 位的倍数。根据编译器的不同,它甚至可以假设数组在 32 位边界上正确对齐。

所有这些组合可能允许编译器发出一次将值设置为 32 位的代码,而无需运行时检查以确保这样做是有效的。如果幸运的话,编译器会识别到这一点,并用一个特别有效的架构特定版本的代码替换它。

(实际上 gcc 和可能其他主流编译器实际上对任何可能被认为等同于 memset 的东西都这样做了,包括 std::fill)。

通常,memset 的实现方式会对这些类型的事物进行运行时检查,以便选择最佳代码路径。虽然这种差异可能可以忽略不计,但我们的想法是我们更好地表达了用特定值“填充”数组的意图,因此编译器能够做出稍微更好的选择。

其他更复杂的语言特性很好地利用意图的表达来获得更大的收益,但这是最简单的例子。

需要明确的是,我的意思不是std::fillmemset“更好”,而是这是一个示例,说明 c++ 如何向编译器更好地表达意图,允许它在编译时获得更多信息,从而使一些优化更容易实现。

【讨论】:

  • 同样可以通过在string.h 中的memset 的内联定义来完成...
  • 有没有编译器实际上优化std::fill比memset更好?
  • @R..:你当然是正确的,我并不是暗示这不能在 C 中完成。我的目的是表明它更容易给出关于类型和意图的编译器表达信息。这反过来又使编译器更容易发出好的代码。 std::fill 说明了这一点,而不是“c++ 可以做到,但 c 不能”。
【解决方案2】:

这在一定程度上取决于您认为这里的优化是什么。如果您纯粹将其视为“std::sort vs. qsort”,那么还有数以千计的其他类似优化。在以下情况下,使用 C++ 模板可以支持内联,其中 C 中唯一合理的选择是使用指向函数的指针,并且几乎没有已知的编译器会内联被调用的代码。根据您的观点,这可以是单个优化,也可以是整个(开放式)系列。

另一种可能性是使用模板元编程将某些内容转换为编译时常量,该常量通常必须在运行时使用 C 进行计算。理论上,您可以通常这样做嵌入一​​个幻数。这可以通过 #define 进入 C,但可能会失去上下文、灵活性或两者兼而有之(例如,在 C++ 中,您可以在编译时定义一个常量,从该输入执行任意计算,并生成一个编译时使用的常量通过其余代码。鉴于您可以在 #define 中执行的计算更加有限,因此几乎不可能经常这样做。

另一种可能性是函数重载和模板特化。这些是分开的,但给出了相同的基本结果:使用专用于特定类型的代码。在 C 中,为了使您处理的函数数量保持合理,您经常最终编写代码(例如)将所有整数转换为 long,然后对其进行数学运算。模板、模板特化和重载使得使用保持较小类型的原始大小的代码变得相对容易,这可以显着提高速度(尤其是当它可以启用数学向量化时)。

最后一个明显的可能性源于简单地提供相当多的预先构建的数据结构和算法,并允许将这些东西打包以便相对容易、有效地重用。我怀疑我什至可以用我知道的相对低效的数据结构和/或算法来计算我用 C 编写代码的次数,仅仅是因为不值得花时间去寻找(或适应)一个更有效的手头的任务。是的,如果它真的变成了一个主要瓶颈,我会不厌其烦地寻找或写出更好的东西——但是做一些比较,看到速度翻倍的情况仍然很常见C++。

然而,我应该补充一点,至少在理论上,所有这些对于 C 来说无疑是可能的。如果您从语言复杂性理论和计算理论模型(例如图灵机)之类的角度来处理这个问题,那么毫无疑问 C 和 C++ 是等价的。只要有足够的工作编写每个函数的专用版本,理论上你可以/可以用 C 做所有这些事情,就像用 C++ 做的一样。

从您可以计划在实际项目中真正编写的代码的角度来看,故事变化非常快——您可以做的限制主要取决于您可以合理管理的内容,而不是理论模型语言所代表的计算。在 C 中几乎完全是理论上的优化级别不仅实用,而且在 C++ 中非常常规。

【讨论】:

  • 几乎没有已知的编译器会内联被调用的代码 具有误导性——gcc 做到了,clang 做到了,如果其他高性能编译器我真的会感到惊讶做不到;当然,这种优化只有在代码驻留在同一个翻译单元中时才有可能,但对于模板也是如此(事实上,这是它们的大部分性能优势的来源)
  • @Christoph:理论上是正确的。然而,实际上,qsort(或bsearch,例如)不会将它们的实现放在一个标题中,它会在与比较函数相同的 TU 中看到。相比之下,(如您所述)std::sortstd::lower_bound 等,始终位于标头中,因此代码在目标 TU 中直接可见 - 以及几乎任何其他内容你用模板做做同样的事情。相比之下,C 不是这样写的。除了在极其本地化的基础上,这只是理论上的。
  • 从包含电池的角度来看,STL 优于 libc,但这是一个单独的问题;虽然 GNU libc 确实没有为 qsort()bsearch() 提供内联定义,但这可以说是库缺陷,因为 C99 内联语义明确支持多个定义的用例,因此您可以在共享库并从标头提供的内联定义中创建优化的库;它实际上是一个相当优雅的解决方案,它避免了模板使用中固有的代码重复......
  • @Christoph:是的,正如我之前所说,理论上这是绝对正确的。现实情况是,即使 C 在 13 多年前添加了 inline,但要做到这一点,您仍然必须编写自己的标准库。
  • @Christoph 随着 LTO 的出现,作为优化要求,驻留在同一个 TU 中几乎已被移除。 (库代码显然仍然是“防火墙”)
【解决方案3】:

即使 qsortstd::sort 的示例也是无效的。如果需要 C 实现,它可以将 qsort 的内联版本放在 stdlib.h 中,并且任何体面的 C 编译器都可以处理内联比较函数。通常不这样做的原因是它非常臃肿并且性能优势令人怀疑——C++ 人往往不关心的问题......

【讨论】:

  • 那么,我如何使用 qsort 和使用我自己的函数/类型(以及字符串、长整数等)的 std::sort 实现相同的性能 - 因为我的编译器没有做任何你想到的优化)?与标准 qsort 相比,std::sort 仅提供“可疑的性能优势”的说法是微不足道的 - 正如链接的基准示例中实际显示的那样(除非慢约 10 倍属于“可疑的改进”)
  • @Voo,R.. 说这不是 C 与 C++ 的主要内容,而是 C 库的提供者选择不提供 qsort 的可内联版本。如果您想自己做这样的事情,那么提取qsort 的一些公共领域实现的代码并将其作为内联版本提供应该是相对简单的。
  • @Jens 我想我误解了你,但你的意思是我们应该写一个qsortStringqsortStringIgnoreCases 等函数吗?因为那不是真的有用。或者有没有办法让c编译器内联对函数指针的实际调用? (我只是不明白该怎么做?)
  • @Voo,如果函数定义在调用点可见并且函数指针正确consted,则调用不能内联的主要原因没有。特别是编译器不会丢失类型信息,否则这些信息会因转换为void* 而丢失。我记得看过这个,我认为是 gcc。
  • 您的陈述“非常臃肿且性能优势令人怀疑——C++ 人员往往不关心的问题......” 是一个大而不准确的概括。在很多情况下,C++ 开发人员会担心占用空间和性能,尤其是在大量使用 C++ 的嵌入式领域。
猜你喜欢
  • 2011-04-30
  • 1970-01-01
  • 2013-09-11
  • 2013-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-08
  • 2011-04-13
相关资源
最近更新 更多