mtune实际上是如何工作的?

13

这里有一个相关的问题:GCC:march与mtune有什么不同?

然而,现有的回答并没有比GCC手册本身更深入地解释。最多,我们能得到:

如果你使用-mtune,那么编译器将生成可以在任何处理器上运行的代码,但会优先选择在特定CPU上运行得最快的指令序列。

-mtune = Y选项会调整生成的代码,使其在Y上比在其他可能运行的CPU上运行得更快。

但是,当构建时GCC如何支持一种特定的体系结构,同时仍然能够在其他(通常较旧)的体系结构上运行构建,尽管速度较慢?

我只知道一件事情(但我不是计算机科学家),可能具备这样的能力,那就是CPU分发程序。然而,似乎mtune并没有在幕后生成分发程序,而是可能采用其他机制。

我之所以这么认为,是有两个原因:

  1. 搜索“gcc mtune cpu dispatcher”没有找到任何相关的内容;以及
  2. 如果它是基于调度程序的,我认为它可以更加智能(即使通过除mtune之外的某些选项),并测试cpuid来检测运行时支持的指令,而不是依赖于在构建时提供的命名体系结构。

那么它真正是如何工作的呢?


3
@yugr它绝对不是重复的问题。你链接的那个问题以及OP本人链接的问题都涉及理解marchmtune之间的区别。虽然这些问题展示了mtune的承诺,但这个问题特别询问编译器可以做什么来实现这些承诺。 - bolov
1个回答

23

-mtune选项不会创建调度程序,因为我们已经告诉编译器我们的目标架构。从GCC文档可知,-mtune=cpu-type将优化生成的代码适应于指定的CPU类型,除了ABI和可用指令集之外的所有内容。这意味着 GCC 不会使用仅在特定 CPU 上可用的指令,但是它将生成在该 CPU 上运行最优的代码。

理解上述陈述需要了解架构和微体系结构之间的区别。架构涉及指令集架构(ISA),而它并不受 -mtune 的影响。微体系结构是指如何在硬件上实现架构。对于相同的指令集(即架构),由于实现的内部细节不同,代码序列可能在一种 CPU(即微体系结构)上运行得最优,但在另一种 CPU 上则不是。有时候,某个代码序列只能在某一微体系结构上运行得最优。

在生成机器代码时,GCC 通常可以自由选择指令的顺序和使用的变体。它会使用一种启发式方法生成一系列在最常见的 CPU 上运行快速的指令,有时为了不损害其他 CPU 的性能而牺牲某个特定 CPU 上的100%最优解。

当我们使用 -mtune=x 选项时,我们可以微调 GCC 以适应 CPU x,从而生成对该 CPU 来说是100%最优(从GCC角度来看)的代码。下面提供一个具体示例

float bar(float a[4], float b[4])
{
    for (int i = 0; i < 4; i++)
    {
        a[i] += b[i];
    }

    float r=0;

    for (int i = 0; i < 4; i++)
    {
        r += a[i];
    }

    return r;
} 

a[i] += b[i]; 对于Skylake和Core2,如果向量不重叠,则矢量化方式不同:

Skylake

    movups  xmm0, XMMWORD PTR [rsi]
    movups  xmm2, XMMWORD PTR [rdi]
    addps   xmm0, xmm2
    movups  XMMWORD PTR [rdi], xmm0
    movss   xmm0, DWORD PTR [rdi] 

Core2

    pxor    xmm0, xmm0
    pxor    xmm1, xmm1
    movlps  xmm0, QWORD PTR [rdi]
    movlps  xmm1, QWORD PTR [rsi]
    movhps  xmm1, QWORD PTR [rsi+8]
    movhps  xmm0, QWORD PTR [rdi+8]
    addps   xmm0, xmm1
    movlps  QWORD PTR [rdi], xmm0
    movhps  QWORD PTR [rdi+8], xmm0
    movss   xmm0, DWORD PTR [rdi]
主要区别在于如何加载一个xmm寄存器,在Core2上,使用movlpsmovhps进行两次加载,而不是使用单个movups
在Core2微架构中,这种两次加载的方式更好,如果你查看Agner Fog的指令表,你会发现movups被解码成4个微操作,并且具有2个时钟周期的延迟,而每个movXps是1个微操作和1个时钟周期的延迟。
这可能是因为当时128位访问被分成了两个64位访问。
在Skylake上相反的情况是真实的:movups比两个movXps执行得更好。

所以我们必须选择一个。
一般来说,GCC选择第一个变量,因为Core2是一种旧的微架构,但我们可以用-mtune覆盖这个设置。


1指令集是通过其他开关选择的。


5
这表明在这个网站上有经验的程序员是多么重要。解释恰到好处,你的例子形象生动,一语道出。我通常不会留下+1评论,但这真的值得一个“干得好!”。谢谢! - bolov
2
@Marc.2377,这不是关于指令独占性的问题,你可以有两个微架构支持相同的ISA,但它们的优化方式不同,因此例如一个简单的标量加法在其中一个上最好使用add指令,而在另一个上使用lea指令(暂时忽略副作用)。编译器将根据-mtune请求的优化目标选择实际的指令。顺便说一下,非常好的答案! - Leeor
1
@Marc.2377 - 或多或少,但是 mtune 实际上并不排除使用调度程序。它们或多或少是正交的。正如Margaret所解释的那样,mtune=X 的意思是“使用X的机器模型来做出优化决策”,但仍然创建基于 march 参数运行的代码。你可以想象 mtunemarch 总是有一些值:即使它们没有在命令行上指定,它们也会采用默认值。因此,一些编译器(不常见)和库(常见)喜欢使用基于调度的代码,并且如果您指定了 mtune,它们仍然可能发生。 - BeeOnRope
2
gcc 应该使用 movsd 而不是 pxor + movlps 来加载 64 位低半部分并将上半部分清零。愚蠢的编译器 :( 不过选择这个例子很好。在更近期的 CPU 中,未对齐的加载变得更加便宜(当数据恰好对齐时则免费),这是一件有趣的事情。但是 Core2 并不仅仅是分割 128 次访问。movaps 是 1 个微操作。只是未对齐的加载没有那么多硬件支持,因此它们总是使用多个微操作,并且在运行时如果数据确实对齐,则无法高效。随着更多的加载端口硬件,它们可以在 NHM 及以后的版本中成为 1 个微操作。 - Peter Cordes
2
@PeterCordes,非常好的观点,我添加了aligned_floatrestrict,这极大地清理了汇编代码,并展示了Core2的两种解决方案 https://godbolt.org/z/DvvAg_ - Z boson
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接