开始用class和float[]写了一个Matrix类,后来担心GC开销,于是用struct重写了一个固定大小的Matrix4x4。结果表明CPU占用率上升了但是实际性能下降了,那么,是因为struct作为函数参数多次传递引发额外复制造成的么?在C++中,常规的使用方法是使用const T& 常引用的办法来避免额外的复制操作,而C#中只有ref,而没有常引用。那么就先使用ref作为替代研究一下。
先看Matrix4x4的定义
public struct Matrix4x4{
public float at00, at01, at02, at03;
public float at10, at11, at12, at13;
public float at20, at21, at22, at23;
public float at30, at31, at32, at33;
public Matrix4x4(ref Matrix4x4 right){
at00 = right.at00;at01 = right.at01;at02 = right.at02;at03 = right.at03;
at10 = right.at10;at11 = right.at11;at12 = right.at12;at13 = right.at13;
at20 = right.at20;at21 = right.at21;at22 = right.at22;at23 = right.at23;
at30 = right.at30;at31 = right.at31;at32 = right.at32;at33 = right.at33;
}
public Matrix4x4(Matrix4x4 right){
at00 = right.at00;at01 = right.at01;at02 = right.at02;at03 = right.at03;
at10 = right.at10;at11 = right.at11;at12 = right.at12;at13 = right.at13;
at20 = right.at20;at21 = right.at21;at22 = right.at22;at23 = right.at23;
at30 = right.at30;at31 = right.at31;at32 = right.at32;at33 = right.at33;
}
public float Sum(){
return at00 + at01 + at02 + at03
+ at10 + at11 + at12 + at13
+ at20 + at21 + at22 + at23
+ at30 + at31 + at32 + at33;
}
}
先定义了两个拷贝构造函数,其中一个是引用传参,Sum函数用于防止编译器把可能的无实际作用的代码给优化掉。先看一下这两个构造函数的性能区别吧:
static float OnlySum(out double total){
Matrix4x4 m = data[0];
QueryPerformanceFrequency(out long freq);
QueryPerformanceCounter(out long start);
float res = 0;
for(int i = 0; i < TEST_COUNT; ++i){
res += m.Sum();
}
QueryPerformanceCounter(out long finish);
total = (finish - start) * 1.0 / freq;
return res;
}
static float CopyConstruct(out double total){
QueryPerformanceFrequency(out long freq);
QueryPerformanceCounter(out long start);
float res = 0;
for(int i = 0; i < TEST_COUNT; ++i) {
var m = new Matrix4x4(data[i]);
res += m.Sum();
}
QueryPerformanceCounter(out long finish);
total = (finish - start) * 1.0 / freq;
return res;
}
static float RefCopyConstruct(out double total){
QueryPerformanceFrequency(out long freq);
QueryPerformanceCounter(out long start);
float res = 0;
for (int i = 0; i < TEST_COUNT; ++i){
var m = new Matrix4x4(ref data[i]);
res += m.Sum();
}
QueryPerformanceCounter(out long finish);
total = (finish - start) * 1.0 / freq;
return res;
}
上面三个是用于测试的函数,TEST_COUNT是1M次,OnlySum用于测量只执行Sum函数的时间,后续Copy或Ref Copy测量的时间会减去OnlySum的执行时间。例如 copy total cost - only total cost。具体运行结果:
Only Sum Matrix4x4: 0.00543733162283515s
Copy Construct :0.0173047680s, takeoff sum :0.0118674364s
Ref Construct :0.0143445799s, takeoff sum :0.0089072482s
通过结果可以看到性能略有提升,但是相对有限,使用ref会节省大约0.003时间。后续再尝试运算符重载的时候,发现运算符重载函数不能使用ref制定参数,那么运算符重载的参数拷贝会造成多大负担呢?先看三个矩阵加法的定义。
public static Matrix4x4 operator +(Matrix4x4 m1, Matrix4x4 m2){
Matrix4x4 mat;
mat.at00 = m1.at00 + m2.at00; mat.at01 = m1.at01 + m2.at01; mat.at02 = m1.at02 + m2.at02; mat.at03 = m1.at03 + m2.at03;
mat.at10 = m1.at10 + m2.at10; mat.at11 = m1.at11 + m2.at11; mat.at12 = m1.at12 + m2.at12; mat.at13 = m1.at13 + m2.at13;
mat.at20 = m1.at20 + m2.at20; mat.at21 = m1.at21 + m2.at21; mat.at22 = m1.at22 + m2.at22; mat.at23 = m1.at23 + m2.at23;
mat.at30 = m1.at30 + m2.at30; mat.at31 = m1.at31 + m2.at31; mat.at32 = m1.at32 + m2.at32; mat.at33 = m1.at33 + m2.at33;
return mat;
}
public static Matrix4x4 Add(ref Matrix4x4 m1, ref Matrix4x4 m2){
Matrix4x4 mat;
mat.at00 = m1.at00 + m2.at00; mat.at01 = m1.at01 + m2.at01; mat.at02 = m1.at02 + m2.at02; mat.at03 = m1.at03 + m2.at03;
mat.at10 = m1.at10 + m2.at10; mat.at11 = m1.at11 + m2.at11; mat.at12 = m1.at12 + m2.at12; mat.at13 = m1.at13 + m2.at13;
mat.at20 = m1.at20 + m2.at20; mat.at21 = m1.at21 + m2.at21; mat.at22 = m1.at22 + m2.at22; mat.at23 = m1.at23 + m2.at23;
mat.at30 = m1.at30 + m2.at30; mat.at31 = m1.at31 + m2.at31; mat.at32 = m1.at32 + m2.at32; mat.at33 = m1.at33 + m2.at33;
return mat;
}
public void Add(ref Matrix4x4 m){
at00 += m.at00; at01 += m.at01; at02 += m.at02; at03 += m.at03;
at10 += m.at10; at11 += m.at11; at12 += m.at12; at13 += m.at13;
at20 += m.at20; at21 += m.at21; at22 += m.at22; at23 += m.at23;
at30 += m.at30; at31 += m.at31; at32 += m.at32; at33 += m.at33;
}
operator+和一个static Add用于对比重载操作符中是否对非ref参数造成额外开销。测试函数基本类似上面的复制构造函数:在主动复制的位置变成了“var m = data[i] + data[i];”这样的加法操作。 看一下运行结果:
Operator+ :0.0484231773s, takeoff sum :0.0430463973s
Ref Add1 :0.0462727389s, takeoff sum :0.0408959589s
Ref Add2 :0.0261778063s, takeoff sum :0.0208010263s
Add2的结果基本上是没有复制的,时间可以作为Matrix执行加法的参考。operator+参数执行两次复制,返回值执行一次复制,临时变量可能执行一次构造。Add函数参数使用ref,如果没有复制的话,只有临时变量执行了一次构造和返回执行一次复制。对比operator,应该节省两次复制的时间,但是从结果上来看,似乎operator中的参数没有ref的这件事并没有多大影响(多了0.00215),为了便利性,使用operator重载还是非常方便的。那么void Add的对比函数比operator+少的一半的时间是因为return的值引起的吗?重新修改一下是static Add的定义:
public static void Add(ref Matrix4x4 m1, ref Matrix4x4 m2, out Matrix4x4 mat);
public static void Add(Matrix4x4 m1, Matrix4x4 m2, out Matrix4x4 mat)
使用out来减少一个局部变量和复制,作为对比,特意又加入一个传入参数的不是ref的对比。那么看一下这两个结果:
Ref Add3 :0.0173745563s, takeoff sum :0.0118756468s
Ref Add4 :0.0242736797s, takeoff sum :0.0186967714s
结果有一些意外,使用了out作为返回值的函数都大大低于了前面的结果。理论上返回值的复制跟参数的复制应该区别不大才对,如果在参数上使用ref没有产生这么的变化,out的结果也应该只有小幅提升才合理,用上面的结果参考一下,复制构造的时间大概是0.012,那么使用out的耗时应该是0.041(两个参数ref返回值为Matrix4x4)-0.012 = 0.029左右,这个值还是比Add3的结果高出了不少。像这样发生少了两倍的耗时,是应该还有其他状况发生的。参考C++中的情况,我猜测的一个可能是使用out的函数发生了内联(inline),这样不仅减少了临时变量和返回值的复制,还减少了一次函数调用。
结论
ref和out确实能带来一定的性能提升,但是如果恰当的写法能让编译器发生内联的话(猜测,还没有理论依据能表明out以及无复杂函数操作一定能使编译器触发内联),还是有非常显著的效果的。但是这个的使用场景也相对有限:
- operator能带来便利性(级联操作)和提升可读性
- ref没有const的限定,使用ref表明函数内部可能对外部进行修改,这给调用方带来了心智负担。
- 这一点也是猜测,使用ref并没有增加对struct参数的引用计数,如果这个struct是在堆上的,比如一个struct[]的某一项data[i]作为了ref参数传入到了其他线程中的函数里,而外部的struct[]正好被GC回收,ref可能就会是一个无效引用了。为了验证这一点,可能还要再想办法设计一个实验,暂时我还没有头绪。
- 想办法简化函数操作,争取让编译器能优化内联还是非常有效的。