C# 循环和函数篇
〇、前言
系统自带计时器 -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;
//在输出面板输出代码所用时间, ":F4"是保留4位小数的意思
Debug.Log($"Takes {second:F4} second");
知识区: 微秒
一、循环类型测试
C# 提供的基本循环类型如下:
----类型---- | --------------------------------作用------------------------------ |
---|---|
for 循环 | 多次执行一个语句序列,简化管理循环变量的代码 |
while 循环 | 当给定条件为 true 时,重复语句或语句组。它会在执行循环主体之前测试条件。 |
do…while 循环 | 除了它是在循环主体结尾测试条件外,其他与 while 语句类似。 |
资料链接: 菜鸟教程 C# 循环.
1. for 循环测试
for循环 标准格式:
for(初始化; 条件; 步进操作){ 代码块操作 }//条件满足则进入循环主体
流程图展示:
(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;//添加 Button
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();
//因为进入 switch 的时间花费相比于1_000_000次自增自减简直微乎其微
//所以就在 switch 之前开始计时
timer.Start();
switch(testIndex)
{
case -1:
// "预热",让电脑做好准备[doge]
//因为我发现,如果不 "预热",第一个运行时间异常长与其他的
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 语句
foreach (T element in elementPool)
{
// foreach 循环 的代码主体部分
DoSomething();
}
// foreach 的后台逻辑
T a;
System.Collections.IEnumerator ienumerator = ((System.Collections.IEnumerable)elementPool).GetEnumerator();
try
{
while (ienumerator.MoveNext())
{
a = (T)ienumerator.Current;
//对应 foreach 循环 的代码主体部分
DoSomething();
}
}
finally
{
//直观地看到 foreach 循环遍历完每个元素后会释放对应的资源
System.IDisposable disposable = ienumerator as System.IDisposable;
if (disposable != null) disposable.Dispose();
}
然后再通过同样的方法:
...
public int count = 100_000;
...
private void OnClicked()
{
//创建新的列表方便 foreach 循环遍历
//创建数组也可以,这里就用列表演示
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;
// for 循环部分
case 0:
for (int i = 0; i < count; ++i)
{
bool tempBool = boolList[i];
}
break;
// foreach 循环部分
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()
{
//创建新的数组,以便达到获取 Length 的目的
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;
//编号0: 正常循环
case 0:
for (int i = 0; i < count; ++i) { }
break;
//编号1: 每次循环都会 .Length
case 1:
for (int i = 0; i < boolArray.Length; ++i) { }
break;
//编号2: 储存了 boolArrayLength
case 2:
int boolArrayLength = boolArray.Length;
for (int i = 0; i < boolArrayLength; ++i) { }
break;
//编号3: 倒着循环
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;
并且加入指针循环片段
//编号4: 指针循环
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循环 内部遍历数组时,都会转化为下面这样的形式:
/* 这个是原本的形式(编号2)
int boolArrayLength = boolArray.Length;
for(int i = 0; i < boolArrayLength; ++i)
{
bool tempBool = boolArray[i];
}
*/
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;
//编号0: 正常循环
case 0:
for (int i = 0; i < count; ++i)
{
boolArray[i] = false;
}
break;
//编号1: 每次循环都会 .Length
case 1:
for (int i = 0; i < boolArray.Length; ++i)
{
boolArray[i] = false;
}
break;
//编号2: 储存了 boolArrayLength
case 2:
int boolArrayLength = boolArray.Length;
for (int i = 0; i < boolArrayLength; ++i)
{
boolArray[i] = false;
}
break;
//编号3: 倒着循环
case 3:
for (int i = boolArray.Length - 1; i >= 0; --i)
{
boolArray[i] = false;
}
break;
//编号4: 指针循环
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(条件){ 代码块操作 }//条件满足则进入循环主体
流程图展示:
传说中 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;
//简单的 while不死循环
case 0:
int i_while = 0;
while (i_while < count)
++i_while;
break;
//简单的 for循环
case 1:
for (int i_for = 0; i_for < count; ++i_for) { }
break;
//for循环 变体1号
case 2:
int i_forVariant_1 = 0;
for (; i_forVariant_1++ < count;) { }
break;
//for循环 变体2号
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 循环 和 for 循环 执行效率一样的写法如下:
//while 传统写法
while(count-- > 0){ }
//for 改进写法
for(; count-- > 0; ){ }
选择 while循环
还是 for循环
还是要视具体情况而定,
- white 循环适合未知循环次数下进行操作;
- for 循环适合在已知循环次数下进行操作;
一般来说循环框架本身执行效率对程序的影响不大,重点优化的还是循环体里面的代码。
3. do…while 循环概括
do…while循环 标准格式:
do{ 代码块操作 }while(条件); //条件满足则进入循环主体
流程图展示:
在深入理解计算机一书的第三章3.6.5节中的讲述可以看出在对循环语句进行汇编时,会先将 for, while转换为 do…while
对于 do...while 循环
,因为 do...while 循环
可以看做是 for循环
和 while循环
的组成部分,可以有之前的实验记录间接得到在经过一样流程的 do..while循环
的运行效率与其他两个不相上下。
如果大家好奇的话就用学到的关于计时器的知识自主测试吧~
二、函数测试
函数基本上可以分为:
- 1.按返回值分类:
- 无返回值
- 单返回值
- 多返回值(out )
- 2.按传入参数分类:
- 无参函数
- 有参函数
- 值参数
- 引用参数(ref, out, in)
- 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 函数名() { }
//访问权限修饰符:public, private, protected 等等
直接进入正题,用简单的 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;
//UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
//改为
Console.WirteLine($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
//带有看起来没有用的初始化的函数
public void DoSomething()
{
int a = int.MaxValue;
}
//再与 while循环和 Console.ReadLine() 配合
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 返回值; //如果返回值类型为 void 则可以不写 return ;
}
如果函数比较短,还可以直接用 Lambda表达式:
//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;
}
//Lambda 表达式写法
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;
//编号0:正常循环
case 0:
for (int i = 0; i < count; ++i) { }
break;
//编号1:有参有返回值函数循环
case 1:
for (int i = 0; i < count; i = Increase(i)) { }
break;
//编号2:有参无返回值函数循环
case 2:
for (int i = 0; i < count; Increase(ref i)) { }
break;
//编号3:无意义的有参有返回值函数循环
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; }
//无意义的传参有返回值
//因为 in 是只读传参,所以无法 ++num
//in 适用于传入带有大型数据结构的 struct;
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 返回值类型 委托名(/*传参修饰符*/ 参数类型 参数名,....);
有了委托,我们可以把函数当参数来使用,关于委托的这里不多说,了解和学习委托链接:
Unity系列之C#四部曲-C#进阶(唐老狮). |
---|
本博客主是想用委托引出事件和匿名函数:
//用到的命名空间
using System;
//无参无返回值的匿名函数
Action 匿名函数名 = delegate () { };
//有参无返回值的匿名函数
Action<参数类型 参数名称,....> 匿名函数名 = delegate (参数类型 参数名称,....) { };
//无参有返回值的匿名函数
Func<返回值类型 返回值名称> 匿名函数名 = delegate () { return 返回值; };
//有参有返回值的匿名函数
Func<参数类型 参数名称,....,返回值类型 返回值名称> 匿名函数名 = delegate (参数类型 参数名称,....){ return 返回值; };
//匿名函数 Lambda表达式 的其中一个例子
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;
//编号0:正常循环
case 0:
for (int i = 0; i < count; ++i) { }
break;
//编号1:有参有返回值函数循环
case 1:
for (int i = 0; i < count; i = Increase(i)) { }
break;
//编号2:事件匿名函数循环
case 2:
Func<int, int> increase = (int num) => ++num;
for (int i = 0; i < count; i = increase(i)) { }
break;
//编号3:委托匿名函数循环
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 |
可以发现使用委托或事件比用普通函数消耗性能,区别不大。
不过在一个类中没有函数方法的传递的话就尽量不要用委托或事件。
新人博主,请大家多多光照~~如果有什么不正确或不足的地方请在评论区积极指出哟,一起学习一起进步~