VS2019: 标准库函数的优化
在最新的Visual Studio 2019 16.2中,VS开发团队优化了一些标准库函数,例如除法运算(std::div, std::ldiv, std::lldiv)和判断是否为非数字的std::isnan。
不管是否开启了编译优化,VS都将客户端对标准库函数的调用生成为对各个std::div和std::isnan变种的调用,而不是将其内联为汇编指令。因为这些标准库函数的定义位于runtime中,这些定义对于编译器来说,都是黑盒子,所以不太合适被内联和优化。而且,对std::div和std::isnan的调用开销甚至比操作本身还要高。在大多数平台上,std::div可以被单条CPU指令计算,而std::isnan也只需要一次对比指令和条件标志检查就可以完成。将这些调用内联可以移除这部分的函数调用开销,而且,因为此时编译器已经有了函数调用的上下文,所以可以对此部分展开优化。
为了支持内联汇编代码生成,开发团队添加了一些编译器内置指令(又称之为compiler intrinsics),用于std::isnan,std::div以及其他的一些标准库函数。注册一个编译器内置函数,相当于教会编译器对这个函数的理解并在代码生成阶段拥有更多的控制。开发团队采用这种在代码生成阶段的优化方案,而不是直接修改标准库,这样可以避免对标准库头文件的修改,也就不会对客户程序产生兼容性影响。
优化std::div
MSVC编译器早就支持除法的优化了,因此,开发团队将对std::div的调用转变为对编译器内置指令的调用,然后将std::div的参数转变为规范化的格式,这些格式可以被MSVC编译器直接识别。
优化std::isnan
转换对std::isnan的调用,显得更加复杂一些。因为需要对C和C++标准的兼容,会产生两种截然不同的冲突。比如,对于C标准,isnan需要被实现为一个宏,而在C++标准中则需要被实现为一个函数重载。在下图的C和C++调用对比中,蓝色方框中是需要被实现为函数重载的函数,紫色方框中是需要被实现为宏的函数,而对实现没有要求的函数则放在了绿色方框中。
为了得到一种统一的实现方案,开发团队不得不将std::isnan和std::fpclassify这两个函数bypass掉,而是集中到std::fpclssify的函数封装上面。因为上图中的绿色函数没有实现上的要求,所以开发团队决定将上图中的绿色函数注册为编译器内置指令。另外,因为每个函数重载都是执行相同的任务,开发团队还可以将C++的标准内置指令转为C版本的内置指令。在下图中,通过这种方式,开发团队得到了一个统一的方式来将原本6个内置指令减少3个。
让我们在回到std::isnan。添加6个新的编译器内置指令的原因,主要是为了优化对std::isnan的代码生成过程。但是,从上图可以知道,将函数调用转为std::fpclassify调用,并没有改变std::isnan的代码生成结果。如果我们以_dclass为例,编译器可以识别所有的_dclass的调用并将其转换为编译器内部指令,但是编译器却并不能改变对_dclassd的代码生成的结果,代码调用还是和转换前一样。
所以最后一个识别std::isnan为编译器内部指令的步骤是:引入模式识别。例如判断一个double类型的变量是否是一个NaN时,可以实现如下:
这里的FP_NAN是一个定义在C和C++标准中的常量。这样编译器就可以很容易的识别出对_dclass的调用,通过一定的模式识别技术,编译器可以为std::isnan添加一个新的编译器内置指令:IV_ISNAN。
测试对比结果
为了展示了不同的代码生成结果,下面是在对std::isnan和std::div在x64平台上的代码生成结果对比:
通过上述这样的优化,到底产生了怎样程度的优化呢?让我们继续来看测试结果。通过对每个对每个函数进行Benchmark测试,开发团队得到了如下的结果:
从上图我们可以看到,启用了编译器内置指令的情况下,对std::div和std::isnan的调用性能均有所提升。
结论
开发团队声称,上述的优化措施将在新版本的16.3中开启/O2编译选项时得到启用,同时请注意,为了启用编译器内置指令,还需要开启/Oi选项。
在计划中的16.4中,开发团队还将对std::fma进行类似的优化。
敬请期待。