Visual Studio 2017版本15.5和15.3中的MSVC代码优化器改进

在这篇文章中,我们想给大家一个更新,在过去一年中Visual C++代码优化器的重大进展,主要集中在15.3和15.5版本中发布的特性。与VS2015 Update 3相比,VS2017 15.5平均提供 SPEC 2017基准中运行速度提高8.9% (有关详细数字,请参阅 CppCon演示 或者 CppCon会议视频 ).

null

本文的以下部分将介绍最新版本提供的一些新的优化和改进,希望对现代本机编译器后端的内部工作提供一个有趣的概述。

对SSA优化器的一般改进

SSA优化器是去年推出的一个新框架 在Visual Studio 2015 Update 3中 静态单一赋值形式 . 正如预期的那样,它允许我们在短时间内取得重大进展,这里描述的大部分优化都是在框架内实现的。在最新的编译器版本中,有几个常规的改进:

  • SSA优化器现在运行两次,在循环优化器之前和之后。这使得它能够利用循环优化和其他二阶效应带来的新机会。
  • 使用地址获取变量和间接内存加载的表达式可以通过使用别名SSA形式和加载的值编号(标识具有相同值的内存位置)得到更好的处理。
  • 一个扩展的模式集合,进一步简化代码并帮助减少代码大小。
公共子表达式消除和部分冗余消除

公共子表达式消除(CSE) 是一种优化,通过标识相同的表达式并保留一个实例,用预先计算的值替换其他实例来删除冗余代码。它是基本优化之一,通常有助于提高执行速度和减少代码大小。SSA优化器中的新方法基于 全局值编号 ,重点是消除冗余的间接内存负载,这可能非常昂贵,特别是当数据不再在CPU缓存中找到时。下面的示例演示了一个加载的源现在如何可以是另一个加载、存储或指向同一内存位置的memset/memcpy。 CSE引入的临时文件用每个路径上加载的值初始化,现在可以注册:

之前 加载后CSE
if (condition1) {
  x = * p;
  use(x);
} else if (condition2) {
  * p = 1;
} else {
  memset(p, 0, 100);
}
y = * p;
use(y);
if (condition1) {
  x = * p;
  use(x);
  temp = x;
} else if (condition2) {
  * p = 1;
  temp = 1;
} else {
  memset(p, 0, 100);
  temp = 0;
}
y = temp;
use(y);

对三元运算符和SSA Phi指令执行特殊形式的CSE加载,如下例所示:

之前 CSE后
x = * p;
use(x);
y = * q;
use(y);
a = condition ? p : q;
b = * a;
x = * p;
use(x);
y = * q;
use(y);
b = condition ? x : y;

在未能找到*a的可用源之后,将搜索所选值p、q的加载/存储,并用condition替换*a?x:是的。这种情况的一个实际例子是使用std::min/max的代码, 如本文所述 .

部分冗余消除(PRE) 是一个新的添加项,通过将表达式插入到函数缺少的路径上,使其完全冗余,从而通过函数处理仅在某些路径上冗余的表达式。PRE的一个简单示例:

之前 预处理后 代码吊装后
if (condition1) {
  x = a * b;
  use(x);
}
y = a * b;
use(y);
if (condition1) {
  x = a * b;
  use(x);
  temp = x;
} else {
  temp = a * b;
}
y = temp;
use(y);
temp = a * b;
if (condition1) {
  x = temp;
  use(x);
}
y = temp;
use(y);

PRE的一个更复杂的例子可以在SPEC2017 Imagick基准测试的hot函数中找到。在这种情况下,消除了5个冗余负载和4个冗余浮点乘法,并且由于图像通常是RGB(A)格式的,所以总是执行大多数消除的表达式。

之前 预处理后
if ((channel & RedChannel) != 0) 
  pixel.red += ( * k) * alpha * GetPixelRed(p);
if ((channel & GreenChannel) != 0) 
  pixel.green += ( * k) * alpha * GetPixelGreen(p);
if ((channel & BlueChannel) != 0) 
  pixel.blue += ( * k) * alpha * GetPixelBlue(p);
if ((channel & OpacityChannel) != 0) 
  pixel.opacity += ( * k) * GetPixelOpacity(p);
if (((channel & IndexChannel) != 0) && 
    (image - > colorspace == CMYKColorspace)) 
  pixel.index += ( * k) * alpha * GetPixelIndex(…);
gamma += ( * k) * alpha;
temp1 = * k;
temp2 = temp1 * alpha;
if ((channel & RedChannel) != 0) 
  pixel.red += temp2 * GetPixelRed(p);
if ((channel & GreenChannel) != 0) 
  pixel.green += temp2 * GetPixelGreen(p);
if ((channel & BlueChannel) != 0) 
  pixel.blue += temp2 * GetPixelBlue(p);
if ((channel & OpacityChannel) != 0) 
  pixel.opacity += temp1 * GetPixelOpacity(p);
if (((channel & IndexChannel) != 0) && 
    (image - > colorspace == CMYKColorspace)) 
  pixel.index += temp2 * GetPixelIndex(…);
gamma += temp2;
内联线改进

内联是最重要的优化之一,不仅消除了函数调用的开销,而且更重要的是,使内联代码适应其内联到的函数的上下文,从而提供有关参数的更精确信息,从而实现更好的优化。VS 2015 Update 3和VS2017 15.5之间的性能提升很大一部分是由于对内联线进行了一些改进,使得内联线更具侵略性,采用了更准确的启发式方法来估计盈利能力。其中一些更改包括在嵌套循环中进行更多的内联,总是内联一次调用的内部/静态函数,并在内联之后使用有关参数实际值的更多上下文信息。

非常小的函数现在总是内联的,只要这不会产生不合理的大函数。同样的改进也适用于 配置文件引导优化 ,其中非常小的函数和只转发给其他函数的函数更有可能是内联的,因为通常这会减少代码大小,内联代码比调用序列小。内联现在还能够处理由值C++对象返回的函数内联,这些对象可能引发异常。

新的CFG优化模块

SSA优化器的最初版本主要针对表达式和 窥视孔优化 s。现在除了新的CSE/PRE模块外,它还包括一个用于执行 控制流图(CFG) SSA形式的优化。这分为两部分,一部分用于执行实际优化,另一部分用于清除,例如删除函数中无用的分支/跳转和无法访问的代码。

首先实现的优化是相似表达式的早期提升和下沉。这里使用的算法比后期编译阶段的算法更具攻击性,它依赖于值编号,并且即使在编译开始/结束时存在不匹配,也能够提取指令 基本块 . 例如,相似的指令可能位于基本块的中间,提取的指令序列不必是连续的。通过这种方式,它可以找到多个独立的表达式并提升/下沉它们。除了减少代码大小外,早期提升/下沉还可以暴露其他优化机会,例如用条件移动表达式(CMOV)替换分支,如下例所示:

之前 沉入仓库后 建立CMOV后
if (condition) {
  * p = x;
} else {
  * p = x + 1;
}
if (condition) {
  temp = x;
} else {
  temp = x + 1;
}* p = temp;
temp = condition ? x : x + 1;
* p = temp;

计划在新模块中实现更多的CFG优化–在测试阶段已经有三个新的优化将在编译器的未来版本中发布。

-fp:fast下浮点优化的改进

在下进行的优化有了显著的改进 -快速浮点模型 在SSA优化器中,扩展现有的算术简化并添加对处理标准库中常见函数的支持:

  • POW强度缩减,当指数是POW(x,16.0)的精确值时,用一系列乘法替换对POW的调用。在微基准测试中,调用pow函数比计算相同值所需的4次乘法慢31倍。替换表达式是以最小形式生成的–例如,pow(a,8.0)被3个计算[(a^2)^2]^2的乘法替换。处理的情况有四种:pow(a,N.0)、pow(a,N.5)、pow(a,-N.0)和pow(a,-N.5)。
  • 基于超越函数恒等式的大量简化集合。举几个例子:
sqrt(a) * sqrt(b) - > sqrt(a * b) 
pow(a, x) * pow(a, y) - > pow(a, x + y)
pow(a, x) * a - > pow(a, x + 1) 
exp(a) * exp(b) - > exp(a + b) 
sin(a) / cos(a) - > tan(a)
  • 将sin(x)和cos(x)的调用合并到对数学库的单个调用中,在相同的时间内计算这两个值。这在x86和x64上可用,其中默认情况下启用SSE2代码生成。
  • 更多的算法简化侧重于消除除法/乘法,并改进了对分支和新身份的MIN/MAX/ABS操作的检测。举几个例子:
a / (1 / b) - > a * b 
a / b / c / d - > a / (b * c * d) 
abs(a known positive) - > a 
max(min(a, b), a) - > a

我们强烈鼓励人们使用-fp:fast标志以获得最佳性能,除非需要精确到最后一位。在几个基准测试套件中,通过以类似于整数的方式优化浮点表达式,以及通过特殊处理上述常见模式,可以获得显著的性能优势。

删除更多不必要的说明

SSA优化器包括位估计器组件,该组件能够确定已知值的哪些位总是1/0,以及其他事实(例如,请参见 上一篇博文 ). 这一点现在得到了一个复杂的分析的补充,该分析可以估计受操作影响的值的位和实际需要的位,从而可以删除不影响表达式最终结果的不必要的指令。一些例子:

之前 之后
x = a | 3;  // Sets lowest 2 bits, useless.
y = x >> 4; // Lowest 4 bits not required, shifted out.
y = a >> 4;
x = a & 0x00FFFFFF; // Clears highest 8 bits, useless. 
y = x | 0xFFFF0000; // Highest 16 bits not required, always set.
y = a | 0xFFFF0000;

这种情况在实践中经常出现,一些最有趣的例子出现在Windows内核/驱动程序中。删除这些不必要的指令也是 苏泊尔超优化器 .

循环展开改进

循环展开 是一种优化,它通过多次复制循环体和减少(或完全消除)迭代计数器的开销来公开更多的指令级并行性。VisualC++中的循环完全展开看到了很大的改进,由于计算出收益的更好的启发和计算循环的迭代次数(行程计数)的改进方法,现在由于展开量大大减少了保守。完全循环展开通常允许对表达式进行更多的后续优化,并允许存储加载转发(将加载替换为以前存储在同一内存位置的值),如下面的示例所示,其中索引变量替换为常量,从而允许表达式在以后进行常量折叠:

之前 循环展开后 在随后的优化之后
for (int i = 0; i < 4; i++) {
  p[i] = i * 4 + 2;
}
i = 0;
p[i] = i * 4 + 2;
i++;
p[i] = i * 4 + 2;
i++;
p[i] = i * 4 + 2;
i++;
p[i] = i * 4 + 2;
p[0] = 2;
p[1] = 6;
p[2] = 10;
p[3] = 14;

太大而无法完全展开的循环将部分展开,并且在不增加代码大小的情况下仍能提供性能优势。一些SPEC2017基准测试得益于改进的循环展开,最多可获得5%的性能优势。

循环if无开关改进

不切换时循环 是一种优化,通过创建循环的两个版本(每个版本的代码来自分支的一侧)从循环中删除分支,而原始分支在两个循环之间进行选择。当分支条件在循环内没有改变(循环不变)时,就可以这样做,它通过创建较短的循环而有利于现代cpu,而控制流不会污染分支预测表。Visual C++有一个更简单的IF切换版本,它现在被改进来处理更一般的情况,如下面的例子,在分支之前/之后有额外的代码。

之前 如果不切换
for (int i = 0; i < n; i++) {
  // Code before branch. 
  if (invariant_condition) {
    // “then” code. 
  } else {
    // “else” code.
  }
  // Code after branch. 
}
if (invariant_condition) {
  for (int i = 0; i < n; i++) {
    // Code before branch.        
    // “then” code.
    // Code after branch. 
  }

} else {
  for (int i = 0; i < n; i++) {
    // Code before branch.        
    // “else” code.
    // Code after branch.
  }
}
使用附近的负载下沉

这是一种优化,也称为部分死代码消除。它的目的是将昂贵的表达式移近它们实际使用的位置,希望在if条件下推送或函数提前退出时永远不会执行它们。另一个被处理的情况是一个表达式,它被分配给一个变量,这个变量稍后在某些路径上被重新定义,如下面的第二个示例中所示。目前这仅限于下沉加载,未来版本的编译器将扩展到更一般的表达式。

之前 下沉荷载后
x = * p;
if (condition) {
  return -1;
}
use(x);
if (condition) {
  return -1;
}
x = * p; // Delay load *p. 
use(x);
x = * p;
if (condition) {
  x = * q;
}
use(x);
if (condition) {
  x = * q;
} else {
  x = * p;
  // Avoid load *p on *q path.
}
use(x);
矢量器改进

更多的循环,不管有没有分支,现在都是矢量化的,这要归功于一种改进的启发式方法,用于估计矢量化的好处,并为指针提供更准确的别名信息。在数组中搜索最小/最大值的代码矢量化现在也支持需要所选值的索引的情况,如以下示例中所示:

for (i = 0; i < N; i++) {
    if (values[i] > max_value) {
        max_value = values[i];
        max_value_index = i;     
    }
}
use(max_value, max_value_index);
改进的CMOV生成和std::min/max的处理

改进了从分支生成条件移动指令(CMOV)的方法,特别是对于浮点值,这有助于处理分支不可预测的情况。下面是Geekbench 4基准测试的一个示例:

offset = lo + delta;
if (curve[offset] > log_exposure) {
    hi = hi - delta;
} else {
    lo = lo + delta;
}
x64之前 x64现在
comiss   xmm0, xmm4
jbe      SHORT $LN4@log_exposu
sub      ecx, r8d
jmp      SHORT $LN5@log_exposu
$LN4@log_exposu:
mov      edx, r9d
$LN5@log_exposu:
sub     eax, ecx
comiss  xmm3, xmm2
cmovbe  eax, r9d
cmovbe  edx, r8d

对于优化器来说,std::min/max以前有些问题,因为它们通过引用获取值,将局部变量的直接访问转换为通过指针的间接访问。消除整数的这些间接访问情况的改进现在也适用于浮点类型。例如,钳制操作现在具有最佳代码生成:

float clamp(float n, float lower, float upper) {
  return std::max(lower, std::min(n, upper));
}
x64之前 x64现在
n$ = 8
upper$ = 24
clamp
comiss   xmm0, xmm2
lea      rax, QWORD PTR upper$[rsp]
lea      rcx, QWORD PTR n$[rsp]
movss    DWORD PTR [rsp+24], xmm2
movss    DWORD PTR [rsp+8], xmm0
cmovbe   rax, rcx
movss    xmm0, DWORD PTR [rax]
comiss   xmm1, xmm0
jb       SHORT $LN10@clipf
movaps   xmm0, xmm1
$LN10@clipf:
ret      0
clamp
minss   xmm0, xmm2
maxss   xmm0, xmm1
ret 0


For integer values: 
clamp_int
cmp     r8d, ecx
cmovl   ecx, r8d
cmp     edx, ecx
cmovl   edx, ecx
mov     eax, edx
ret 0
最后

我们很高兴最终在编译器后端发布所有这些新的和改进的优化,并帮助您的程序更快。预计在未来的版本中会有更多的添加—我们正在不断努力实现新的优化,改进现有的优化,或者用更新、更好的方法替换一些旧的优化,比如在SSA优化器中完成的工作。

请让我们知道,如果您有任何反馈或建议的情况下,可以更好地优化。我们可以通过以下评论和电子邮件联系到您(visualcpp@microsoft.com)您还可以通过 帮助>报告产品中的问题 ,或通过 开发者社区 .

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享