测试Unity中常用代码的运行所需时间:循环和函数篇 #性能测试 # for,foreach,while循环 #委托事件

〇、前言

系统自带计时器 -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(初始化; 条件; 步进操作){ 代码块操作 }//条件满足则进入循环主体

流程图展示:

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;//添加 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里编译完成后记录实验数据

次数后置自增前置自增后置自减前置自减
1267269297267
2270260260261
3380348363368
4348350347350
5326327326331

控制台中编译完成后记录实验数据

次数后置自增前置自增后置自减前置自减
11258128616471647
21249123216471648
31261127616471643
41318125716461648
51275126816431666

由此可得出结论:

  • 自增自减的前后置在循环区域里面进行的时间消耗无太大区别,但是选择自增还是自减对循环区域还是有区别的,从数据上可以看出选自减大概会多出 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 循环
110492596
210872722
312783216
413793252
512973249

Unity 编译器下,相对来说在这种较少运算的实验记录中,for 循环 的运行时间快于 foreach 循环 ,不过只通过 Unity 编译器 给出的结果并不能明朗地分辨出那个性能更好。
下面这是在控制台运行的实验数据:

次数for 循环foreach 循环
134163033
234013032
334013041
434123035
534023035

控制台中运行,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
1260260258258
2315314316314
3258258258268
4314314337314
5314315315314

发现其实这几种方法在运行时间耗费上没有太大的区别,于是本博客主再在每个循环块中加入:

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
1889131213121311697
2907136513311314698
3884134313131324692
4973141313811408729
5817120812071208641

按我们平常的思路想的话:编号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
120581823205815001909
220501572211715021913
320462297205215001942
420611568206015551956
520681566206715031925

也印证了上面所说的。
编号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;

			//简单的 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 循环变体1for 循环变体2
1376372373373
2336338337336
3329326325324
4315314316315
5258258258289

因为在 Unity 上编译 的实验结果没有太大区别,本博客主决定再在控制台再进行一次记录:

将这行代码
UnityEngine.Debug.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");
改为
Console.WriteLine.Log($"{testIndex}: Takes -{timer.Elapsed.Ticks}- ticks");

控制台编译完成后记录实验数据

次数while 循环for 循环for 循环变体1for 循环变体2
11269125416051265
21300126815911273
31254124916021258
41276123815901235
51391125915911238

显而易见,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(条件); //条件满足则进入循环主体

流程图展示:

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.按返回值分类:
    • 无返回值
    • 单返回值
    • 多返回值(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");
	}
}

为了放大实验效果,本博客主把循环内部代码块复制六次(即进行六次操作)

编译完成后记录实验数据

次数空白对照申明变量赋值变量初始化申明+赋值
112991272206520662075
212581290206120622137
312781277214620842256
412911270206620602077
512961272207020782068

由实验记录可得:

  • 申明变量不产生时间消耗

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遍,编译完成后记录实验数据,并计算稳定数据的平均值:

次数正常循环--稳定值空函数循环--稳定值
1446439439439440439462440
2440440440440439439438439
3416405406406405404404404
4405405404405404404423404
5377361361361361361361361

发现调用空函数对时间消耗没有太大的影响,或许是 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遍,编译完成后记录实验数据,并计算稳定数据的平均值:

次数正常循环--稳定值函数循环--稳定值
113421283131713142207222222072212
212941397129013572207226022032223
313181284146013542213220722242215
413011289136713192201220322022202
513521290128113082202220122892201

于是这次实验稳稳地证明了 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遍,编译完成后记录实验数据,并计算稳定数据的平均值:

次数正常循环--平均值函数循环--平均值
123242323232323232552253325022529
223572348233023452554239524902478
323212315232323202396244125022446
423392325232223292505249425392513
523282343232323312597252124722530

由最终结果可知,函数调用对执行时间有略微的影响,从数据上可以看出大概是多出 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--稳定值
11768176917241753619050755298518635912385237423803611322631763201
21245125712501251507050395046505223742373238123763186318431903187
31258126712511259507350325045505023732372237223723180317831843181
41268123112491249508450895089508723772374238223783176317632023184
51296123012591261511050565082508323872381239123863177317831893181

四种循环平均用时比大概是 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
1127506539537
2127511558538
3181520568562
4129510540543
5129507538538

可以发现使用委托或事件比用普通函数消耗性能,区别不大。
不过在一个类中没有函数方法的传递的话就尽量不要用委托或事件。

新人博主,请大家多多光照~~如果有什么不正确或不足的地方请在评论区积极指出哟,一起学习一起进步~

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
课程总体目标:     本面向的学员不再是完全的编程“小白”,而是具备一定C#编程经验,需要进一步查漏补缺、或者需要进一步全面完善自己C#编程知识体系的广大Unity学员。相信通过本的学习,可以使得Unity级开发人员对于编程语言的掌握更进一步;对于开发大型游戏项目,在编程语言这一层级进一步打下坚实的语言基础。 “”课程讲解特点:       本面向初级游戏研发人员,以及Unity高级学习者。为了更加深入的刨析各个语法的本质,我们采用反编译解读IL间语言的方式,来解构语法难点,使得学员最短时间掌握语法本质。 本课程讲解内容:       C#(for Unity) 在“C#入门”、“基础”的基础之上,从以下四个方面着重研究我们游戏开发(包含软件开发)过程C#最重要、最实用的技能模块,使得广大游戏研发人员,对于C#这门Unity脚本有进一步更加完善的认识。一:.Net 框架讲解。    A) .Net 发展历史。    B)  IL  间语言。 CLR  公共语言运行时。    C) 多维数据(常用二维数组)与交错数组。    D) 可变参数 Params    E) 进一步解释“实参”,“形参”。    F) 类的实例化内存分配机制。二:深入学习对象类型    A)  里氏替换原则(LSP)    B)  类的属性极其本质特性    C)  IS ,AS 关键字    D)  深入学习字符串理论        1] 字符串的“驻留性” 原理。        2] 字符串==与Equals() 的本质区别        3] 更多字符串常用方法学习。    E)  枚举类型以及适用场合。三:深入学习集合特性    A)  什么是索引器,以及索引器的适用范围。    B)  学习自定义集合类,以及深入了解Foreach 语句的原理。    C)  深入学习 ArrayList,了解内部存储机制以及原理。    D)  深入学习 HashTable,了解内部存储机制以及原理。    E)  为什么学习泛型集合?    F)  泛型集合与普通集合的性能测试对比实验。    G)  学习“泛型约束”,以及“泛型约束”的适用条件。四:委托事件        A)  什么是委托,先从讲故事学习起:“老板来啦”!    B)  反编译掌握委托的本质。    C)  委托的四大开发步骤。    D)  什么是事件,以及委托事件的区别。    E)  事件常用使用方式。 温馨提示:       本C# for Unity 使用Virtual Studio2012,进行开发与讲解。(学员使用更高版本,对学习没有任何影响) 一、热更新系列(技术含量:高级):A:《lua热更新技术》https://edu.csdn.net/course/detail/27087B:《热更新框架设计之Xlua基础视频课程》https://edu.csdn.net/course/detail/27110C:《热更新框架设计之热更流程与热补丁技术》https://edu.csdn.net/course/detail/27118D:《热更新框架设计之客户端热更框架(上)》https://edu.csdn.net/course/detail/27132E:《热更新框架设计之客户端热更框架()》https://edu.csdn.net/course/detail/27135F:《热更新框架设计之客户端热更框架(下)》https://edu.csdn.net/course/detail/27136二:框架设计系列(技术含量:级): A:《游戏UI界面框架设计系列视频课程》https://edu.csdn.net/course/detail/27142B:《Unity客户端框架设计PureMVC视频课程(上)》https://edu.csdn.net/course/detail/27172C:《Unity客户端框架设计PureMVC视频课程(下)》https://edu.csdn.net/course/detail/27173D:《AssetBundle框架设计_框架视频课程》https://edu.csdn.net/course/detail/27169三、Unity脚本从入门到精通(技术含量:初级)A:《C# For Unity系列之入门》https://edu.csdn.net/course/detail/4560B:《C# For Unity系列之基础》https://edu.csdn.net/course/detail/4595C: 《C# For Unity系列之》https://edu.csdn.net/course/detail/24422D:《C# For Unity系列之进阶》https://edu.csdn.net/course/detail/24465四、虚拟现实(VR)与增强现实(AR):(技术含量:初级)A:《虚拟现实之汽车仿真模拟系统 》https://edu.csdn.net/course/detail/26618五、Unity基础课程系列(技术含量:初级) A:《台球游戏与FlappyBirds—Unity快速入门系列视频课程(第1部)》 https://edu.csdn.net/course/detail/24643B:《太空射击与移动端发布技术-Unity快速入门系列视频课程(第2部)》https://edu.csdn.net/course/detail/24645 C:《Unity ECS(二) 小试牛刀》https://edu.csdn.net/course/detail/27096六、Unity ARPG课程(技术含量:初级):A:《MMOARPG地下守护神_单机版实战视频课程(上部)》https://edu.csdn.net/course/detail/24965B:《MMOARPG地下守护神_单机版实战视频课程(部)》https://edu.csdn.net/course/detail/24968C:《MMOARPG地下守护神_单机版实战视频课程(下部)》https://edu.csdn.net/course/detail/24979

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值