mtune实际上是如何工作的?

Mar*_*377 10 optimization gcc instruction-set cpu-architecture instructions

有这个相关的问题:海湾合作委员会:游行与mtune有何不同?

但是,现有答案并没有比GCC手册本身更进一步.我们最多得到:

如果您使用-mtune,那么编译器将生成适用于其中任何一个的代码,但将支持在您指定的特定CPU上运行速度最快的指令序列.

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

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

我只知道有一件事(但我不是计算机科学家)才能做到这一点,而且那是一个CPU调度员.但是,(对我来说)似乎并不是mtune在幕后生成调度程序,而是其他一些机制可能正在生效.

我觉得这样做有两个原因:

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

那么它如何运作呢?

Mar*_*oom 15

-mtune 不创建调度程序,它不需要一个:我们已经告诉编译器我们所针对的架构.

来自海湾合作委员会的文件:

-mtune = CPU型

        调整cpu-type所有适用于生成代码的内容,但ABI和
        可用指令集除外.

这意味着GCC不会使用仅在cpu-type 1上可用的指令,但它将生成在cpu-type上最佳运行的代码.

要理解这最后的陈述,有必要了解架构和微架构之间的区别.
该架构意味着ISA(指令集架构)并且不受其影响-mtune.
微架构是架构在硬件中的实现方式.对于相等的指令集(读取:体系结构),由于实现的内部细节,代码序列可以在CPU(读取微架构)上最佳地运行而在另一个上不运行.这可以使代码序列仅在一个微架构上是最佳的.

在生成机器代码时,GCC通常可以自由选择如何订购指令以及使用哪种变体.
它将使用启发式方法生成一系列指令,这些指令在最常见的CPU上快速运行,有时它将牺牲100%的CPU x最佳解决方案,如果这会损害CPU y,zw.

当我们使用-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;
} 
Run Code Online (Sandbox Code Playgroud)

a[i] += b[i];被矢量化靶向SKYLAKE微架构或核2时(如果载体不重叠)是不同的:

SKYLAKE微架构

    movups  xmm0, XMMWORD PTR [rsi]
    movups  xmm2, XMMWORD PTR [rdi]
    addps   xmm0, xmm2
    movups  XMMWORD PTR [rdi], xmm0
    movss   xmm0, DWORD PTR [rdi] 
Run Code Online (Sandbox Code Playgroud)

酷睿2

    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]
Run Code Online (Sandbox Code Playgroud)

主要区别在于如何xmm加载寄存器,在Core2上加载两个加载movlpsmovhps不是使用单个加载movups.
在Core2微架构上,两种负载方法更好,如果你看一下Agner Fog的指令表,你会看到它movups被解码为4 uop并且具有2个周期的延迟,而每个周期movXps为1 uop和1个周期潜伏.
这可能是因为当时128位访问被分成两个64位访问.
在Skylake上,情况正好相反:movups表现优于两个movXps.

所以我们必须拿起一个.
一般来说,GCC选择了第一个变种,因为Core2是一个古老的微架构,但我们可以用它来覆盖它-mtune.


1与其他开关一起选择指令集.

  • 这表明在这个网站上有经验的程序员有多重要.解释是现实的,你的例子胜过千言万语.我通常不会留下+1评论,但这确实值得"很棒!".谢谢! (4认同)
  • @ Marc.2377,它不是关于指令独占性,你可以有2个微架构支持相同的ISA,但是它们有不同的优化,所以例如一个简单的标量添加最好用一个`add`指令实现,但是另一方面是"lea"(忽略副作用一秒钟).因此,编译器将根据-mtune请求的优化目标选择实际指令.PS - 确实很棒! (2认同)
  • gcc 应该使用 `movsd` 而不是 `pxor` + `movlps` 来加载 64 位低半部分并将上半部分归零。愚蠢的编译器 :( 不错的例子选择,不过。未对齐的负载在最近的 CPU 中变得便宜(并且在数据碰巧对齐时免费)是一件有趣的事情。但 Core2 不只是拆分 128 次访问。`movaps` 是 1 uop。只是未对齐的负载没有那么多的硬件支持,所以它们总是使用多个 uop,并且在数据确实在运行时对齐的情况下效率不高。使用更多的负载端口硬件,它们可以在 NHM 及更高版本中为 1 uop。 (2认同)
  • @PeterCordes,好点,我添加了`aligned_float` 和restrict 这极大地清理了程序集并显示了core2 https://godbolt.org/z/DvvAg_ 的两种解决方案 (2认同)