CPU与代码优化(1):用三元操作符替代if-else以降低CPU分支预测惩罚;函数13倍提速(Unity)。
2017-11-09 20:20
1211 查看
测试对象
1,Unity脚本(C#)2,C# DLL(mcs build 动态链接库导入Unity)
3,C Native Code(LLVM编译后导入Unity)
被测试函数源码
(C#):两个随机数数组进行大小比较,一个数组保存大数,另一个保存小数。public void Minmax1_CSharp(double[] a,double[] b,int n){ int i; for (i = 0; i < n; i++) { if (a [i] > b [i]) { double t = a [i]; a [i] = b [i]; b [i] = t; } } } public void Minmax2_CSharp(double[] a,double[] b,int n){ int i; for (i = 0; i < n; i++) { double min = a [i] < b [i] ? a [i] : b [i]; double max = a [i] < b [i] ? b [i] : a [i]; a [i] = min; b [i] = max; } }
C:
extern void Minmax1(double a[],double b[],int n){ int i; for (i = 0; i < n; i++) { if (a [i] > b [i]) { double t = a [i]; a [i] = b [i]; b [i] = t; } } } extern void Minmax2(double a[],double b[],int n){ int i; for (i = 0; i < n; i++) { double min = a [i] < b [i] ? a [i] : b [i]; double max = a [i] < b [i] ? b [i] : a [i]; a [i] = min; b [i] = max; } }
测试脚本:
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class ConditionalTrans : MonoBehaviour { private double[] array1_Double=new double[1000000]; private double[] array2_Double=new double[1000000]; void Start(){ int pTime; int cTime; for (int i = 0; i < 1000000; i++) { array1_Double [i] = (double)Random.Range (1f, 100000f); array2_Double [i] = (double)Random.Range (1f, 100000f); } pTime = System.Environment.TickCount; Minmax1 (array1_Double,array2_Double,1000000); cTime = System.Environment.TickCount; Debug.Log ("MM1:Time:"+(cTime-pTime)); for (int i = 0; i < 1000000; i++) { array1_Double [i] = (double)Random.Range (1f, 100000f); array2_Double [i] = (double)Random.Range (1f, 100000f); } pTime = System.Environment.TickCount; Minmax2 (array1_Double,array2_Double,1000000); cTime = System.Environment.TickCount; Debug.Log ("MM2:Time:"+(cTime-pTime)); for (int i = 0; i < 1000000; i++) { array1_Double [i] = (double)Random.Range (1f, 100000f); array2_Double [i] = (double)Random.Range (1f, 100000f); } pTime = System.Environment.TickCount; Minmax1_CSharp (array1_Double,array2_Double,1000000); cTime = System.Environment.TickCount; Debug.Log ("MM1_CS:Time:"+(cTime-pTime)); for (int i = 0; i < 1000000; i++) { array1_Double [i] = (double)Random.Range (1f, 100000f); array2_Double [i] = (double)Random.Range (1f, 100000f); } pTime = System.Environment.TickCount; Minmax2_CSharp (array1_Double,array2_Double,1000000); cTime = System.Environment.TickCount; Debug.Log ("MM2_CS:Time:"+(cTime-pTime)); LibTest6.MyClass mc = new LibTest6.MyClass (); for (int i = 0; i < 1000000; i++) { array1_Double [i] = (double)Random.Range (1f, 100000f); array2_Double [i] = (double)Random.Range (1f, 100000f); } pTime = System.Environment.TickCount; mc.CSDLL_Minmax1(array1_Double,array2_Double,1000000); cTime = System.Environment.TickCount; Debug.Log ("MM1_CSDLL:Time:"+(cTime-pTime)); for (int i = 0; i < 1000000; i++) { array1_Double [i] = (double)Random.Range (1f, 100000f); array2_Double [i] = (double)Random.Range (1f, 100000f); } pTime = System.Environment.TickCount; mc.CSDLL_Minmax2 (array1_Double,array2_Double,1000000); cTime = System.Environment.TickCount; Debug.Log ("MM2_CSDLL:Time:"+(cTime-pTime)); } public void Minmax1_CSharp(double[] a,double[] b,int n){ int i; for (i = 0; i < n; i++) { if (a [i] > b [i]) { double t = a [i]; a [i] = b [i]; b [i] = t; } } } public void Minmax2_CSharp(double[] a,double[] b,int n){ int i; for (i = 0; i < n; i++) { double min = a [i] < b [i] ? a [i] : b [i]; double max = a [i] < b [i] ? b [i] : a [i]; a [i] = min; b [i] = max; } } [DllImport("C_Plugin",ExactSpelling=true,EntryPoint="Minmax1")] private static extern void Minmax1(double[] a,double[] b,int n); [DllImport("C_Plugin")] private static extern void Minmax2(double[] a,double[] b,int n); }
测试环境:
CPU: 2.6 GHz Intel Core i5内存:8 GB 1600 MHz DDR3
IDE: Xcode Version 9.1 (9B55)
Mono Develop 5.9.6
Unity: 2017.1.1f1 Personal
编译器与优化级别:
1,Unity脚本 mcs/.NET JIT 未知2,C# DLL mcs/.NET JIT 未知
3,C Native LLVM -O3
测试结果:
两组一百万个double随机数比较大小后对调1,Unity脚本: 21毫秒(minmax1) 26毫秒(minmax2)
2,C# DLL 15毫秒(minmax1) 24毫秒(minmax2)
3,C Native 5毫秒(minmax1) 2毫秒(minmax2)
测试结果截图:
分析各版本各函数生成的CIL/机器码:
先了解下C#的编译流程C#脚本:源码先被mcs编译成CIL(微软中间语言),保存在Assembly-CSharp.dll中,在Unity运行时,.NET JIT编译器根据需要实时的将CIL编译成机器码,并由机器执行。
C# DLL:C#DLL中的内容仍然还是CIL,同上,在运行时由JIT实时编译成机器码。
由于JIT是实时编译,在mac不知道如何获取它生成的机器码。mono还有一个AOT编译器,它可以将CIL进行预编译,以下将会分析AOT生成的机器码,优化级别为-optimize=all。但实际上由于两种编译器的运行机制不同导致的优化策略不同,相对生成的机器码不大可能完全相同,只能希望是相差不大了。
由最慢向最快分析:
1,Unity脚本Minmax2
同一个Monodevelop编译,同C# DLL2,Unity脚本Minmax1
同C# DLL3,C# DLL Minmax2
CIL代码:IL_0041是for循环起始处,以b开头ILxxx结尾的指令都是branch分支跳转,for循环内有4次分支跳转,看来是将三元操作符翻译成了4个if。
机器码:
红线范围内是for循环,循环内四次跳转,忠实的执行了CIL版本的逻辑,comisd指令比较寄存器低64位,没有什么优化。
4,C# DLL Minmax1
CIL代码:在IL_0002处开始的循环中只有一次分支跳转,与源码逻辑相同。
机器码:
循环内只有一次comisd比较,一次循环内的分支跳转,与它的上层语言逻辑完全符合。
5,C DLL Minmax1
机器码:循环内有两次comisd比较以及跳转(源码一次),两次跳转的原因不大理解,不确定是不是循环展开。(而且两次比较都是jbe,而且向寄存器中写入都是在不选择分支的情况下,这岂不是死循环,汗-_-:,难道是反编译软件出错…)
6,C DLL Minmax2
机器码:将三元操作符译为了minsd/maxsd,在循环内无任何分支跳转,并且使用了3个xmm寄存器,目测是减少了读写相关性,提高了指令并行度,并且有二路循环展开。
总结:
1,机器指令级别的分支跳转(无法预测)次数与函数运行速度成反比
1,Unity脚本: 21毫秒(minmax1)/1 26毫秒(minmax2)/42,C# DLL 15毫秒(minmax1)/1 24毫秒(minmax2)/4
3,C Native 5毫秒(minmax1)/2 2毫秒(minmax2)/0
对现代CPU来讲,多条指令是被不同的硬件单元同时执行的,甚至在一个硬件单元中,不同的阶段也有不同的指令在同时执行中。 为了追求速度,CPU遇到分支跳转指令时,并不会等待之前指令的比较结果,而是预测一个结果(既是跳转或不跳转),并继续发射指令,这些在比较结果出来之前而发射的指令会正常执行但不会写入内存,当比较结果出来时,如果预测正确则写入内存,如果预测错误,必须全部取消并且回到分支处重新取指,这一过程在时间上的损耗既是分支预测惩罚。当分支结果比较好预测时,这种分支预测策略是相当合理的(在另一组测试中,每个数组1中的变量都比数组2中的变量大1,除了C Natice Minmax2,所有版本的函数运行速度全部变快了)。但当遇到此案例这种根本无法预测的情况时,CPU只能进行赌博式投机预测,不可避免的大量分支预测惩罚影响了程序的运行速度。
2, C# DLL 要比 C#脚本快那么一点。
但是考虑到(1),一个案例代表不了一般性,(2),优势过于微弱。更保守的判断是在不同情况下C# DLL与Unity脚本各有优劣。3,C Native+LLVM-O3编译在速度上有绝对优势
不论是测试数据上,还是生成的机器码在理论上的合理性上C都是完胜。4,源码层面上三元操作符不一定优于if-else
以逻辑的角度来看,三元操作符转化为min/max或cmov等CPU指令应该是完全合理的,但是由于当前的编译器的编译权限过高,源码所产生的机器码由编译器的分析能力决定。在此案例中,mcs编译器的智能明显还是有限的(或者是CIL语言的局限性)。由于三元操作符在CIL层面就已经被译为了分支跳转,JIT对此问题的优化逻辑成了未知数。Xcode的LLVM在-O3优化级别下合理的将三元操作符转化为了min/max指令,但是在其他优化级别依然会错误的选择分支跳转。
————————————————————————————————————————————————————
参考:
深入理解计算机系统—R.E Bryant,D.R.Hallaron
维护日志:
相关文章推荐
- 优化技巧:提前if判断帮助CPU分支预测
- 利用分支预测优化代码
- CPU与代码优化(2):关于高速缓存命中问题的实验(Unity内)与研究
- 练习2-10 重新编写将大写字母转换为小写字母的函数lower,并用条件表达式替代其中的if-else结构。
- 过多if-else分支的优化
- 多条件if...else...选择语句代码优化
- 过多if-else分支的优化
- Python中如何避免使用多个分支语句(多个if和else)——函数字典(Function Map)简介
- 关于优化冗余的多个if-else代码
- 功能代码分离 代码结构if(返回值为布尔类型的函数)判断优化
- 代码优化——去除你代码中的if...else...层层嵌套
- 过多if-else分支的优化
- java中 if-else分支语句的优化方案
- 代码优化——去除你代码中的if...else...层层嵌套
- 过长的if-else分支结构优化
- 正确的优化分段函数形式的多重分支代码
- D3D9黑屏优化大幅降低CPU占用率代码
- if-else 分支预测
- 正确的优化分段函数形式的多重分支代码
- 【第1076期】 如何无痛降低 if else 面条代码复杂度