〇、前言
系统自带计时器 -System.Diagnostics.StopWatch- 的使用
System.Diagnostics.StopWatch 的基本使用方法
using System.Diagnostics;
Stopwatch timer = new Stopwatch();
timer.Start();
timer.Stop();
decimal second = timer.Elapsed.Ticks * 0.000_000_1m;
decimal microSecond = timer.Elapsed.Ticks * 0.1m;
Debug.Log($"Takes {second:F4} second");
知识区: 微秒
一、循环类型测试
C# 提供的基本循环类型如下:
----类型---- | --------------------------------作用------------------------------ |
---|
for 循环 | 多次执行一个语句序列,简化管理循环变量的代码 | while 循环 | 当给定条件为 true 时,重复语句或语句组。它会在执行循环主体之前测试条件。 | do…while 循环 | 除了它是在循环主体结尾测试条件外,其他与 while 语句类似。 |
资料链接: 菜鸟教程 C# 循环.
1. for 循环测试
for循环 标准格式:
for(初始化; 条件; 步进操作){ 代码块操作 }
流程图展示:
Created with Rapha?l 2.3.0
进入
初始化
满足条件?
代码块操作
步进操作
离开循环
yes
no
(1). 前后置自增自减测试
本博客主用了简单的 for 循环 测试了一下自增自减的运行时间比较:
int count = 100_000
for(int i = 0; i < count; i++){}
for(int i = 0; i < count; ++i){}
for(int i = count - 1; i >= 0; i--){}
for(int i = count - 1; i >= 0; --i){}
然后在循环开始前计时,结束后记录代码运行的时长:
using System.Diagnostics;
Stopwatch timer = new Stopwatch();
timer.Start();
for(int i = 0; i < count; i++){}
timer.Stop();
Debug.Log($"Takes -{timer.Elapsed.Ticks}- ticks");
为了方便记录、观察和比较,本博客主直接将代码写在一个脚本里,并通过一次性将所有代码在较短的时间段里完成来减小电脑在较大时间差的时间点上运行效率可能不一致的偶然误差,完整代码如下:
using System.Diagnostics;
using UnityEngine;
using UnityEngine.UI;
public class TimerTest : MonoBehaviour
{
public Button button;
public int count = 100_000;
private void Start()
=> button.onClick.AddListener(OnClicked);
private void OnClicked()
{
for(int testIndex = -1; testIndex < 4; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch(testIndex)
{
case -1:
break;
case 0:
for(int i = 0; i < count; i++){}
break;
case 1:
for(int i = 0; i < count; ++i){}
break;
case 2:
for(int i = count - 1; i >= 0; i--){}
break;
case 3:
for(int i = count - 1; i >= 0; --i){}
break;
}
timer.Stop();
UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
}
Unity里编译完成后记录实验数据:
次数 | 后置自增 | 前置自增 | 后置自减 | 前置自减 |
---|
1 | 267 | 269 | 297 | 267 | 2 | 270 | 260 | 260 | 261 | 3 | 380 | 348 | 363 | 368 | 4 | 348 | 350 | 347 | 350 | 5 | 326 | 327 | 326 | 331 |
控制台中编译完成后记录实验数据:
次数 | 后置自增 | 前置自增 | 后置自减 | 前置自减 |
---|
1 | 1258 | 1286 | 1647 | 1647 | 2 | 1249 | 1232 | 1647 | 1648 | 3 | 1261 | 1276 | 1647 | 1643 | 4 | 1318 | 1257 | 1646 | 1648 | 5 | 1275 | 1268 | 1643 | 1666 |
由此可得出结论:
- 自增自减的前后置在循环区域里面进行的时间消耗无太大区别,但是选择自增还是自减对循环区域还是有区别的,从数据上可以看出选自减大概会多出 20%~25%个自增 的时间消耗.
前后置在对内存的操作上,i++ 是 使用了 i 这个值后再进行 +1,所以需要储存在一个临时的变量当中,而++i 是直接 +1,没有了对内存的操作环节,相对而言++i 性能是比i++ 更好。
(2). 与 foreach 循环比较
首先来了解一下 foreach 循环 内部的原理:
foreach (T element in elementPool)
{
DoSomething();
}
T a;
System.Collections.IEnumerator ienumerator = ((System.Collections.IEnumerable)elementPool).GetEnumerator();
try
{
while (ienumerator.MoveNext())
{
a = (T)ienumerator.Current;
DoSomething();
}
}
finally
{
System.IDisposable disposable = ienumerator as System.IDisposable;
if (disposable != null) disposable.Dispose();
}
然后再通过同样的方法:
...
public int count = 100_000;
...
private void OnClicked()
{
List<bool> boolList = new List<bool>();
for (int i = 0; i < count; ++i)
boolList.Add(true);
for (int testIndex = -1; testIndex < 2; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (testIndex)
{
case -1:
break;
case 0:
for (int i = 0; i < count; ++i)
{
bool tempBool = boolList[i];
}
break;
case 1:
foreach(bool tempBool in boolList)
{
}
break;
}
timer.Stop();
UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
编译完成后记录实验数据:
次数 | for 循环 | foreach 循环 |
---|
1 | 1049 | 2596 | 2 | 1087 | 2722 | 3 | 1278 | 3216 | 4 | 1379 | 3252 | 5 | 1297 | 3249 |
在Unity 编译器下,相对来说在这种较少运算的实验记录中,for 循环 的运行时间快于 foreach 循环 ,不过只通过 Unity 编译器 给出的结果并不能明朗地分辨出那个性能更好。 下面这是在控制台运行的实验数据:
次数 | for 循环 | foreach 循环 |
---|
1 | 3416 | 3033 | 2 | 3401 | 3032 | 3 | 3401 | 3041 | 4 | 3412 | 3035 | 5 | 3402 | 3035 |
在控制台中运行,foreach 循环 比 for循环 略快,本博客主推荐看准控制台记录的实验数据。 接下来列举出 foreach 循环 和 for 循环 的优缺点:
- foreach 循环
- 优点:
- 1.代码简洁
- 2.不用因为数组下标上下限而纠结,特别是在上下限在外部环境下发生变化的时候
- 3.可以一次性输出多维数组所有元素(交叉数组除外)
- 4.循环 ArrayList 等数据集合时无需进行显式地装箱拆箱
- 缺点:
- 1.foreach 循环可以看做是只读循环,也就是被枚举出来的元素不能被修改
- 2.不能对集合本身进行修改,否则会报错
- 3.循环后会进行对应成员的垃圾回收(GC),释放使用完的资源
- for 循环
- 优点:
- 1.可以通过设置上下限来控制数组的输出
- 2.能够对数组内元素进行更改
- 3.不仅仅可以对数组进行操作,还可以用于多次重复的计算,可用性广泛
- 缺点:
- 1.下文会介绍到的 C#为强类型判断 “通过下标访问数组元素的时候会多进行一次判断”
但是本博客主推荐: 尽量在遍历所有成员的情况下使用 foreach 循环,正常来说会比正常的 for循环 少 12%~13% 时间消耗 其他情况下视条件而选择
(3). 循环中画蛇添足的事
<1> array.Length 和 arrayLength 的抉择
有些时候我们会习惯性的储存变量以便后来使用。 不过在 C# 的 for 循环 中在不正确的地方使用,可能还会多此一举,例如以下的写法:
for (int i = 0; i < boolArray.Length; ++i) { }
int boolArrayLength = boolArray.Length;
for (int i = 0; i < boolArrayLength; ++i) { }
同样本博客主用计时器进行耗费时间上的比较:
...
public int count = 100_000;
...
private void OnClicked()
{
bool[] boolArray = new bool[count];
for (int i = 0; i < count; ++i)
boolArray[i] = true;
for (int testIndex = -1; testIndex < 5; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (testIndex)
{
case -1:
break;
case 0:
for (int i = 0; i < count; ++i) { }
break;
case 1:
for (int i = 0; i < boolArray.Length; ++i) { }
break;
case 2:
int boolArrayLength = boolArray.Length;
for (int i = 0; i < boolArrayLength; ++i) { }
break;
case 3:
for (int i = boolArray.Length - 1; i >= 0; --i) { }
break;
}
timer.Stop();
UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
编译完成后记录实验数据:
循环次数/循环编号 | 编号0 | 编号1 | 编号2 | 编号3 |
---|
1 | 260 | 260 | 258 | 258 | 2 | 315 | 314 | 316 | 314 | 3 | 258 | 258 | 258 | 268 | 4 | 314 | 314 | 337 | 314 | 5 | 314 | 315 | 315 | 314 |
发现其实这几种方法在运行时间耗费上没有太大的区别,于是本博客主再在每个循环块中加入:
boolArray[i] = false;
并且加入指针循环片段
case 4:
unsafe
{
bool* pBoolArray = (bool*)System.Runtime.InteropServices.Marshal.UnsafeAddrOfPinnedArrayElement(boolArray, 0);
for (int i = 0; i < boolArray.Length; ++i)
{
pBoolArray[i] = false;
}
}
break;
再次运行,并在编译完成后记录实验数据:
循环次数/循环编号 | 编号0 | 编号1 | 编号2 | 编号3 | 编号4 |
---|
1 | 889 | 1312 | 1312 | 1311 | 697 | 2 | 907 | 1365 | 1331 | 1314 | 698 | 3 | 884 | 1343 | 1313 | 1324 | 692 | 4 | 973 | 1413 | 1381 | 1408 | 729 | 5 | 817 | 1208 | 1207 | 1208 | 641 |
按我们平常的思路想的话:编号1 每次都访问引用类型的属性,它最有可能是最慢的 ;编号1 的确没有快到哪里去,但是最后发现编号1 、编号2 和编号3 居然差不多一样快,于是本博客主就去搜了一下资料,刚刚好看到 “C#是强类型检查”,就顺便对 强类型检查 知识记笔记, 强类型检查语言对于访问数组的时候,要对索引的有效值进行判断,于是在 for循环 内部遍历数组时,都会转化为下面这样的形式:
int boolArrayLength = boolArray.Length;
for(int i = 0; i < boolArrayLength; ++i)
{
if(i < boolArrayLength.Length)
{
bool tempBool = boolArray[i];
}
else
throw new IndexOutOfRangeException();
}
这种强类型检查就好比我们写 属性 的时候,进行一次判断一样,更直观的就是直接举 索引器 的例子:
private Monster monster;
public Monster OwnedMonster
{
get
{
if(monster != null)
return monster
else
throw new UnassignedReferenceException();
}
}
private string[] faceStrs;
public string this[int faceIndex]
{
get
{
if(faceIndex < faceStrs.length)
return faceStrs;
else
throw new IndexOutOfRangeException();
}
}
对于编号4 中的方法:
public static IntPtr UnsafeAddrOfPinnedArrayElement(Array arr, int index);
这个静态函数的作用是返回一个数组第 index 个元素的首地址,而且没有值类型和引用类型的限制,没有数组索引越界的检查。 简单的说,它是一个 托管的C#数组 与 非托管指针 的一个合法的转换接口。 在改变数组值的时候没有进行索引越界检查,效率会略微提高。 ?????<资料来源于 C# 内存操作常用函数.>
而对于 编号0 时间消耗的意外凹陷,本博客主想挺久的,最后再决定在控制台编译:
static int count = 100_000;
static void Main(string[] args)
{
bool[] boolArray = new bool[count];
for (int i = 0; i < count; ++i)
boolArray[i] = true;
loop:
for (int testIndex = -1; testIndex < 4; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (testIndex)
{
case -1:
int u = 0;
break;
case 0:
for (int i = 0; i < count; ++i)
{
boolArray[i] = false;
}
break;
case 1:
for (int i = 0; i < boolArray.Length; ++i)
{
boolArray[i] = false;
}
break;
case 2:
int boolArrayLength = boolArray.Length;
for (int i = 0; i < boolArrayLength; ++i)
{
boolArray[i] = false;
}
break;
case 3:
for (int i = boolArray.Length - 1; i >= 0; --i)
{
boolArray[i] = false;
}
break;
case 4:
unsafe
{
bool* pBoolArray = (bool*)System.Runtime.InteropServices.Marshal.UnsafeAddrOfPinnedArrayElement(boolArray, 0);
for (int i = 0; i < boolArray.Length; ++i)
{
pBoolArray[i] = false;
}
}
break;
}
timer.Stop();
Console.WriteLine($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
Console.ReadLine();
goto loop;
}
运行,并在编译完成后记录实验数据:
循环次数/循环编号 | 编号0 | 编号1 | 编号2 | 编号3 | 编号4 |
---|
1 | 2058 | 1823 | 2058 | 1500 | 1909 | 2 | 2050 | 1572 | 2117 | 1502 | 1913 | 3 | 2046 | 2297 | 2052 | 1500 | 1942 | 4 | 2061 | 1568 | 2060 | 1555 | 1956 | 5 | 2068 | 1566 | 2067 | 1503 | 1925 |
也印证了上面所说的。 编号0 和编号2 要比编号1 多 强类型检查 ,而编号4 用指针循环数组因为不用进行 强类型检查 时间消耗稍微比编号0 和编号2 好,而编号3 因为是倒着循环,应该也是少了一次对 boolArray.Length > 0 进行一次检查了。
2. while 循环测试
while循环 标准格式:
while(条件){ 代码块操作 }
流程图展示:
Created with Rapha?l 2.3.0
进入
满足条件?
代码块操作
离开循环
yes
no
传说中 while 循环 是很多新手在战胜死循环前要跨过的最高的山。
(1). 与 for 循环的比较
与上面几次测试一样,用简单的自增循环进行测试,测试看看两个循环在进行相同操作时消耗的时间是否会相同:
...
public int count = 100_000;
...
private void OnClicked()
{
for (int testIndex = -1; testIndex < 4; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (testIndex)
{
case -1:
int u = 0;
break;
case 0:
int i_while = 0;
while (i_while < count)
++i_while;
break;
case 1:
for (int i_for = 0; i_for < count; ++i_for) { }
break;
case 2:
int i_forVariant_1 = 0;
for (; i_forVariant_1++ < count;) { }
break;
case 3:
int i_forVariant_2 = 0;
for (; i_forVariant_2 < count; ++i_forVariant_2) { }
break;
}
timer.Stop();
UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
在 unity 上编译完成后记录实验数据:
次数 | while 循环 | for 循环 | for 循环变体1 | for 循环变体2 |
---|
1 | 376 | 372 | 373 | 373 | 2 | 336 | 338 | 337 | 336 | 3 | 329 | 326 | 325 | 324 | 4 | 315 | 314 | 316 | 315 | 5 | 258 | 258 | 258 | 289 |
因为在 Unity 上编译 的实验结果没有太大区别,本博客主决定再在控制台再进行一次记录:
将这行代码
UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
改为
Console.WriteLine.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
在控制台编译完成后记录实验数据:
次数 | while 循环 | for 循环 | for 循环变体1 | for 循环变体2 |
---|
1 | 1269 | 1254 | 1605 | 1265 | 2 | 1300 | 1268 | 1591 | 1273 | 3 | 1254 | 1249 | 1602 | 1258 | 4 | 1276 | 1238 | 1590 | 1235 | 5 | 1391 | 1259 | 1591 | 1238 |
显而易见,for循环 和 while循环 经过一样流程的时间消耗上没有太大区别的,而本博客加上两个 for循环变体是为了添加一些实验变量,丰富实验,增加实验的多样性和趣味性。 具体的有说服力的还是要看汇编的实验结果:
在汇编实验结果中, while 循环 和 for 循环 执行效率一样的写法如下:
while(count-- > 0){ }
for(; count-- > 0; ){ }
选择 while循环 还是 for循环 还是要视具体情况而定,
- white 循环适合未知循环次数下进行操作;
- for 循环适合在已知循环次数下进行操作;
一般来说循环框架本身执行效率对程序的影响不大,重点优化的还是循环体里面的代码。
3. do…while 循环概括
do…while循环 标准格式:
do{ 代码块操作 }while(条件);
流程图展示:
Created with Rapha?l 2.3.0
进入
代码块操作
满足条件?
代码块操作
离开循环
yes
no
在深入理解计算机一书的第三章3.6.5节中的讲述可以看出在对循环语句进行汇编时,会先将 for, while转换为 do…while
对于 do...while 循环 ,因为 do...while 循环 可以看做是 for循环 和 while循环 的组成部分,可以有之前的实验记录间接得到在经过一样流程的 do..while循环 的运行效率与其他两个不相上下。 如果大家好奇的话就用学到的关于计时器的知识自主测试吧~
二、函数测试
函数基本上可以分为:
- 1.按返回值分类:
- 2.按传入参数分类:
- 3.其他分类:
1.申明,赋值,初始化
申明一个变量:
int a;
赋值给它一个数:
a = 0;
和初始化:
int a = 0;
它们消耗的时间一样吗? 我们在控制台简单进行一个测试:
...
public static int count = 100_000;
public static float testNum = 0;
public const float pi = 3.14159265f;
...
static void Main(string[] args)
{
for (int testIndex = -2; testIndex < 4; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (testIndex)
{
case -2:
break;
case -1:
for (int i = 0; i < count; ++i) { }
break;
case 0:
for (int i = 0; i < count; ++i) { float testDeclare; }
break;
case 1:
for (int i = 0; i < count; ++i) { testNum = pi; }
break;
case 2:
for (int i = 0; i < count; ++i) { float testInit = pi; }
break;
case 3:
for (int i = 0; i < count; ++i)
{
float testDeclare;
testDeclare = pi;
}
break;
}
timer.Stop();
Console.WriteLine($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
为了放大实验效果,本博客主把循环内部代码块复制六次(即进行六次操作)
编译完成后记录实验数据:
次数 | 空白对照 | 申明变量 | 赋值变量 | 初始化 | 申明+赋值 |
---|
1 | 1299 | 1272 | 2065 | 2066 | 2075 | 2 | 1258 | 1290 | 2061 | 2062 | 2137 | 3 | 1278 | 1277 | 2146 | 2084 | 2256 | 4 | 1291 | 1270 | 2066 | 2060 | 2077 | 5 | 1296 | 1272 | 2070 | 2078 | 2068 |
由实验记录可得:
1.无返回值无参函数测试
无参无返回值函数的标准格式:
访问权限修饰符 void 函数名() { }
直接进入正题,用简单的 for循环 测试函数调用的时间消耗:
...
public int count = 100_000;
...
private void OnClicked()
{
for (int testIndex = -1; testIndex < 6; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (Mathf.FloorToInt(testIndex*0.334f))
{
case -1:
break;
case 0:
for (int i = 0; i < count; ++i) { }
break;
case 1:
for (int i = 0; i < count; ++i)
{
DoNothing();
}
break;
}
timer.Stop();
UnityEngine.Debug.Log($"{Mathf.FloorToInt(testIndex*0.334f)}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
public void DoNothing(){ }
每个运行3遍,编译完成后记录实验数据,并计算稳定数据的平均值:
次数 | 正常循环 | - | - | 稳定值 | 空函数循环 | - | - | 稳定值 |
---|
1 | 446 | 439 | 439 | 439 | 440 | 439 | 462 | 440 | 2 | 440 | 440 | 440 | 440 | 439 | 439 | 438 | 439 | 3 | 416 | 405 | 406 | 406 | 405 | 404 | 404 | 404 | 4 | 405 | 405 | 404 | 405 | 404 | 404 | 423 | 404 | 5 | 377 | 361 | 361 | 361 | 361 | 361 | 361 | 361 |
发现调用空函数对时间消耗没有太大的影响,或许是 Unity 编译器优化这个部分了。 于是本博客主直接在控制台运行代码,并修改了一些代码:
case 0:
for (int i = 0; i < count; ++i)
{
int a = int.MaxValue;
}
break;
case 1:
for (int i = 0; i < count; ++i)
{
DoSomething();
}
break;
Console.WirteLine($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
public void DoSomething()
{
int a = int.MaxValue;
}
Console.ReadLine();
再次把每个运行3遍,编译完成后记录实验数据,并计算稳定数据的平均值:
次数 | 正常循环 | - | - | 稳定值 | 函数循环 | - | - | 稳定值 |
---|
1 | 1342 | 1283 | 1317 | 1314 | 2207 | 2222 | 2207 | 2212 | 2 | 1294 | 1397 | 1290 | 1357 | 2207 | 2260 | 2203 | 2223 | 3 | 1318 | 1284 | 1460 | 1354 | 2213 | 2207 | 2224 | 2215 | 4 | 1301 | 1289 | 1367 | 1319 | 2201 | 2203 | 2202 | 2202 | 5 | 1352 | 1290 | 1281 | 1308 | 2202 | 2201 | 2289 | 2201 |
于是这次实验稳稳地证明了 Unity 编译器会对没有用的函数优化掉,而且函数调用会多消耗部分时间。 后来本博客主为了实现真正意义上的实现函数调用的消耗时间测试,在函数里面加了有意义的代码片段:
public float a = 0;
case 0:
for (int i = 0; i < count; ++i)
{
a += 0.003_906_25f;
}
break;
case 1:
for (int i = 0; i < count; ++i)
{
AddFloat();
}
break;
a = 0;
public void AddFloat()
{
a += 0.003_906_25f;
}
最后一次把每个运行3遍,编译完成后记录实验数据,并计算稳定数据的平均值:
次数 | 正常循环 | - | - | 平均值 | 函数循环 | - | - | 平均值 |
---|
1 | 2324 | 2323 | 2323 | 2323 | 2552 | 2533 | 2502 | 2529 | 2 | 2357 | 2348 | 2330 | 2345 | 2554 | 2395 | 2490 | 2478 | 3 | 2321 | 2315 | 2323 | 2320 | 2396 | 2441 | 2502 | 2446 | 4 | 2339 | 2325 | 2322 | 2329 | 2505 | 2494 | 2539 | 2513 | 5 | 2328 | 2343 | 2323 | 2331 | 2597 | 2521 | 2472 | 2530 |
由最终结果可知,函数调用对执行时间有略微的影响,从数据上可以看出大概是多出 5%~8%个循环框架 的时间消耗. 而且由于调用函数的时候,系统会在都会在栈上开辟部分内存,具体的函数内存分配见<深入理解函数调用堆栈>。
2.传参和有返回值函数测试
正常函数的标准格式:
访问权限修饰符 返回值类型 函数名( 参数类型 参数名,....)
{
代码块操作;
return 返回值;
}
如果函数比较短,还可以直接用 Lambda表达式:
访问权限修饰符 返回值类型 函数名(参数类型 参数名,....)
=> 返回值;
下面这些函数当做更直观地感受不同的函数:
public bool Die()
{
if (this.blood <= 0) return true;
else return false;
}
private int Pay(int given)
{
return given - cost;
}
protected void AddBlood(int bloodAdd)
{
this.blood += bloodAdd;
if (this.blood > maxBlood)
this.blood = maxBlood;
}
public void Swap(ref int a, ref int b)
{
a ^= b;
b ^= a;
a ^= b;
}
public void Adder(bool input_1, bool input_2, bool input_3, out bool sum, out bool carry)
{
bool arrear_1 = input_1 ^ input_2;
bool arrear_2 = arrear_1 ^ input_3;
bool head_1 = input_1 & input_2;
bool head_2 = head_1 & input_3;
sum = arrear_2;
carry = head_1 | head_2;
}
public float GetArea(int width, int length)
=> width * Length;
进入测试环节,我们在循环的自增部分改为函数表示:
...
public static int count = 100_000;
...
static void Main(string[] args)
{
for (int testIndex = -1; testIndex < 12; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (Mathf.FloorToInt(testIndex*0.334f))
{
case -1:
int u = 0;
break;
case 0:
for (int i = 0; i < count; ++i) { }
break;
case 1:
for (int i = 0; i < count; i = Increase(i)) { }
break;
case 2:
for (int i = 0; i < count; Increase(ref i)) { }
break;
case 3:
for (int i = 0; i < count; i = AddOne(in i)) { }
break;
}
timer.Stop();
Console.WriteLine($"{Mathf.FloorToInt(testIndex*0.334f)}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
private int Increase(int num) => ++num;
private void Increase(ref int num) { ++num; }
private int AddOne(in int num) => num + 1;
同样每个运行3遍,编译完成后记录实验数据,并计算稳定数据的稳定值:
次数 | 编号0 | - | - | 稳定值 | 编号1 | - | - | 稳定值 | 编号2 | - | - | 稳定值 | 编号3 | - | - | 稳定值 |
---|
1 | 1768 | 1769 | 1724 | 1753 | 6190 | 5075 | 5298 | 5186 | 3591 | 2385 | 2374 | 2380 | 3611 | 3226 | 3176 | 3201 | 2 | 1245 | 1257 | 1250 | 1251 | 5070 | 5039 | 5046 | 5052 | 2374 | 2373 | 2381 | 2376 | 3186 | 3184 | 3190 | 3187 | 3 | 1258 | 1267 | 1251 | 1259 | 5073 | 5032 | 5045 | 5050 | 2373 | 2372 | 2372 | 2372 | 3180 | 3178 | 3184 | 3181 | 4 | 1268 | 1231 | 1249 | 1249 | 5084 | 5089 | 5089 | 5087 | 2377 | 2374 | 2382 | 2378 | 3176 | 3176 | 3202 | 3184 | 5 | 1296 | 1230 | 1259 | 1261 | 5110 | 5056 | 5082 | 5083 | 2387 | 2381 | 2391 | 2386 | 3177 | 3178 | 3189 | 3181 |
四种循环平均用时比大概是 2:8:4:5 ,本博客主就用不严谨的但能够生动形象地理解的比喻:
- i < count 条件判断占 1
- ++i 或 +1 占 1
- i = ? 赋值占 1
- 引用函数占 1
- 函数返回值占 1
- 那么最后 实参的值复制给形参占 3
这种解释大家也仅供娱乐就好,不要真的这样做
对于函数对时间性能的影响也没有比其拓展性和易用性重要,不要因为追求”跑得快“而把代码写得一坨一坨乱七八糟的,大家还是要看情况而选择要不要调用函数。 一般来说,对于冗余的重复性高的代码块,就可以用函数套起来,更多是为了以后更新迭代的方便。 把函数用妙了,程序整体性能就能得到有效的提高。
3.事件函数测试
重新复习一下委托怎么写:
访问权限修饰符 delegate 返回值类型 委托名( 参数类型 参数名,....);
有了委托,我们可以把函数当参数来使用,关于委托的这里不多说,了解和学习委托链接:
本博客主是想用委托引出事件和匿名函数:
using System;
Action 匿名函数名 = delegate () { };
Action<参数类型 参数名称,....> 匿名函数名 = delegate (参数类型 参数名称,....) { };
Func<返回值类型 返回值名称> 匿名函数名 = delegate () { return 返回值; };
Func<参数类型 参数名称,....,返回值类型 返回值名称> 匿名函数名 = delegate (参数类型 参数名称,....){ return 返回值; };
Action<参数类型 参数名称,....> 匿名函数名 = (参数类型 参数名称,....) => { };
接下来进行简单匿名函数执行所需时间:
...
public static int count = 100_000;
public delegate int IncreaseDelegate(int num);
...
static void Main(string[] args)
{
for (int testIndex = -1; testIndex < 4; ++testIndex)
{
Stopwatch timer = new Stopwatch();
timer.Start();
switch (testIndex)
{
case -1:
int u = 0;
break;
case 0:
for (int i = 0; i < count; ++i) { }
break;
case 1:
for (int i = 0; i < count; i = Increase(i)) { }
break;
case 2:
Func<int, int> increase = (int num) => ++num;
for (int i = 0; i < count; i = increase(i)) { }
break;
case 3:
IncreaseDelegate increaseDelegate = (int num) => ++num;
for (int i = 0; i < count; i = increaseDelegate(i)) { }
break;
}
timer.Stop();
Console.WriteLine($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
}
}
private int Increase(int num) => ++num;
编译完成后记录实验数据:
次数 | 编号0 | 编号1 | 编号2 | 编号3 |
---|
1 | 127 | 506 | 539 | 537 | 2 | 127 | 511 | 558 | 538 | 3 | 181 | 520 | 568 | 562 | 4 | 129 | 510 | 540 | 543 | 5 | 129 | 507 | 538 | 538 |
可以发现使用委托或事件比用普通函数消耗性能,区别不大。 不过在一个类中没有函数方法的传递的话就尽量不要用委托或事件。
新人博主,请大家多多光照~~如果有什么不正确或不足的地方请在评论区积极指出哟,一起学习一起进步~
|