【问题标题】:Optimising compilers mindset for a Python programmer为 Python 程序员优化编译器心态
【发布时间】:2020-09-18 12:59:46
【问题描述】:

主要来自 Python 背景,我现在正在学习 C 和 x86-64 汇编。我以前通过 Cython 间接使用过 C,但现在除了汇编之外,我还在学习 C。

我的基本问题是,在优化编译器时,我应该把自己放在什么样的心态上。我应该让编译器完成它的工作,但是一旦我足够精通汇编,就开始检查并确认汇编输出吗?这就是想要编写高性能代码的负责任的 C 程序员所做的吗?

问题被触发是因为我想检查gcc 7.5.0 将优化下面的代码。特别是,我运行了objdump 来了解如何在不同级别上优化在同一索引处访问一个数组两次。

  • -O3 上有一些我还没有学过的指令,例如movaps XMMWORD PTR [rsp+0x10],xmm0
  • -O2-O1 的关卡比较清晰,但我还是没有完全理解
  • -O0 级别我相信我可以看到代码的相当简单的翻译,我认为messages[idx] 确实被访问了两次

我的问题不是什么时候应该使用这些级别。我只是问更有经验的程序员,如果这就是你所做的,运行高度优化的代码并检查汇编输出以确保一切都符合预期?对于想要真正了解编译器生成什么机器代码的人来说,这是自然的工作流程吗?

我知道下面的示例是一种微不足道的优化机会,但您是否刚刚了解到某些优化肯定会发生并且您不再考虑它们?关于可以进行什么样的转换和优化的信息并不多,更不用说编译器没有留下任何注释或消息让程序员了解优化的内容和原因,所以我无法想象除了简单的其他方式在实践中学习这一切。谢谢。

#include <stddef.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
    size_t len_messages = 9;
    int messages[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    for(size_t idx=0; idx < len_messages; idx++) {
        printf("Accessing here %d and there %d\n", messages[idx], messages[idx]);
    }

    return 0;
}

【问题讨论】:

  • 我认为这取决于你工作的环境。我根本不考虑优化——我只是说-O3 并让编译器做它的事情——除非有似乎是个问题。而且在我的领域中很少有——编译器通常会生成非常好的代码。在许多领域,我怀疑你必须更加积极主动。老实说,我怀疑你会对此发表意见,但没有明确的答案。
  • 我很少看汇编代码,尽管我非常关心优化。看两块汇编代码并说:这比那快——现代处理器非常复杂,这绝非易事。此外,性能的一个关键决定因素是程序与内存系统(所有这些缓存!)的配合程度,这通常更容易从更高级别的视图中看到。对我来说,我怀疑其他许多人,优化的时间都花在查看分析器输出和尝试更高级别的“算法”上
  • 您应该首先通过探查器运行代码,而不是手动检查程序集。寻找热点,然后首先关注这些领域的算法复杂性、缓存一致性等。只有在您确信您的设计是最佳的之后,您才应该查看装配(如果那时仍然需要)。
  • 只有疯狂(或不幸)的人才会查看他们编写的所有高级语言代码的程序集。
  • 阅读this draft report。与您的问题相关的十几页

标签: c compiler-optimization


【解决方案1】:

我的基本问题是,在优化编译器时,我应该把自己放在什么样的心态上。我应该让编译器完成它的工作,但是一旦我足够精通汇编,就开始检查并确认汇编输出吗?

基本上没有。

不同的代码段对性能的影响程度不同 - 在初始化期间只使用一次的一段代码不会对性能产生太大影响,而在循环中间频繁执行的一段代码可能会产生极大的影响在性能上。通过组装进行优化会花费开发人员的时间和便携性;并且这些额外成本通常不能通过不经常执行的代码的可忽略的性能改进来证明。

因此,主要策略是使用分析器来确定最重要(对于性能)的代码片段在哪里;并仅调查这些部分的性能改进。

但是,“调查性能改进”并不一定意味着直接进行组装。您考虑改进算法、改进数据结构和缓存局部性、改进并行度(“更多线程!”)等等。

在所有这些之后,您可能会查看编译器生成的程序集,看看您是否可以找到一种方法来手动改进/优化它。你也可能不会。

您仍然可能不使用汇编语言的原因是不同的 CPU 是不同的。您可以针对一个 CPU(无论您的计算机有什么)进行优化,并使软件在其他 CPU 上显着变慢(无论运行您的软件的最终用户有什么);或者您可以依赖可能不存在的功能(例如 AVX512)。当然,这也意味着您从分析中获得的结果并不像您想象的那么有用(对于粗略估计来说已经足够了,而且永远不能用作适用于所有 CPU 的准确表示)。

要解决这个问题,您可能需要针对不同 CPU 使用多种不同版本的汇编语言 - 一种用于“64 位 Intel 与 AVX-512”,一种用于“64 位 Intel 与 AVX2”,一种用于“64 位”英特尔没有任何 AVX”,AMD 的另外 2 个版本,因为您发现一些指令在 AMD 上花费的时间更长,而其他一些指令在 AMD 上更快;然后是 64 位 ARM 的另一个不同版本的集合,然后是 PowerPC,然后......

基本上;在装配中进行优化是很少见的。对于一个“重磅炸弹”的库(例如 MPEG 解码器、大数字库……),它可能很有意义,而对于大型程序的一些性能关键部分,它可能是合理的;但除此之外,您的时间可能还有更重要的事情要做。

【讨论】:

  • 这是一个有趣的答案,我同意你的看法,但问题更多的是“你怎么知道编译器会像你期望的那样优化事情,因为它会默默地发生”而不是“何时在装配中进行优化”。我认为你开始从前一个角度回答,但无论如何你还是完成了后者:-) 如果你可以添加一个注释,根据你的经验,除非一个人的低级代码是数学繁重的,或者除非一个人自己编写编译器,很少有人检查现代编译器的功能?
  • @Terry:这不是它的工作原理。如果您启用优化(“-O3”),您知道编译器已尽力而为,并且您知道编译器的最佳性能可能“比理想更糟糕” " 而且您根本不在乎(并且知道编译器的最佳性能可能比您当时的期望更好或更差)。如果您不启用优化,那么您就知道编译器没有尝试(并且可以预期结果很糟糕)。
  • @Terry:请注意,这可能被视为“委托”——您将优化的责任委托给编译器(和编译器开发人员),这样您就可以说“哈哈,不再是我的问题了!”。
  • 我希望我能接受两个答案 - 最后我接受了来自@rurban 的一个,因为他在此过程中向我介绍了一个新工具。再次感谢 Brendan,您的回答也很有帮助。
【解决方案2】:

我很少单独看拆卸。大多数情况下,我使用 Ghidra 对函数进行反编译,以查看优化器发生了什么。你会得到一个更大更好的画面。使用更熟悉的语言,您仍然可以看到生成的程序集。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-07-10
    • 2012-04-07
    • 2021-11-14
    • 1970-01-01
    • 2011-11-07
    • 2012-06-09
    • 1970-01-01
    • 2012-04-01
    相关资源
    最近更新 更多