摘要:一个C#小实验引起的代码性能探究
前言
本文源于一个C#小实验:生成防伪码。与其他以“完成目标”为要求的实验不同,该实验以“性能优化”为要求。实验题目(见下)很简单,简单的数行代码即可实现。而如何让代码达到最佳的性能,即以最短的运行时间运行,成为了我纠结的问题。此处仅以此文记录我的思考过程,望日后有助于同行。
- 小实验题目:生成防伪码
- 防伪码由以下字符组成:0123456789ABCDEFGHJKLMNPQRSTUVWXYZ(数字1和字母I相近、数字0和字母O相近,所以去掉字母I和字母O,全部字母大写)
- 在命令行中输入2个参数,分别是:防伪码长度与防伪码个数。例如:在命令行中调用程序为“应用程序.exe 10 10000”指的是防伪码长度为10,生成10000个防伪码。
- 输出结果:时间(单位:ms)
- 注意
- 不需要输出每个防伪码(在调试时输出防伪码可用于检验生成码的正确性,正式输出时只输出一项结果即运行程序所需要的时间)
- 防伪码的长度由命令行参数决定
- 所生成的防伪码不能重复(按照以上例子,生成了10000个防伪码,这10000个防伪码就肯定不能重复)
先贴出公用模板 - 下述其他代码默认嵌套在此模板内执行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960// *****************************************************// author: @Seahub Seahubc@gmail.com || Seahubc@qq.com// 实验一:生成防伪码// 防伪码组成元素:0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ// input: 防伪码长度 防伪码个数// output: 防伪码运行所需时间// PS: 防伪码不能重复// *****************************************************// 测试样例:生成防伪码长度为10,防伪码个数为1,000,000的运算时间// 测试环境:Win7 i5-4670T 16GB内存// *****************************************************using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Diagnostics;using System.Collections;namespace SecurityCode{class Program{static void Main(string[] args){// ===================== 固定常量 =====================const String charArr = "0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";// ===================== 获取参数 =====================int lengthOfSecurityCode = 0;int numberOfSecurityCode = 0;if (args.Length < 2){// 当args带的参数 < 2时,提示输入两个参数System.Console.WriteLine("Please input parameter - " +"lengthOfSecurityCode:");lengthOfSecurityCode = Convert.ToInt32(Console.ReadLine());System.Console.WriteLine("Please input parameter - " +"numberOfSecurityCode:");numberOfSecurityCode = Convert.ToInt32(Console.ReadLine());} else {lengthOfSecurityCode = int.Parse(args[0]);numberOfSecurityCode = int.Parse(args[1]);}// ===================== 开始计时 =====================Stopwatch timer = new Stopwatch();timer.Start();// ===================== 核心函数 =====================// toDo - 下述思考过程代码将代替此部分// =================== 停止计时并输出 ==================timer.Stop();double dMilliseconds = timer.Elapsed.TotalMilliseconds;System.Console.WriteLine("生成个数为:" + numberOfSecurityCode +",运行时间为:" + dMilliseconds + "ms");}}}
思考过程的第一个实现 - 利用HashSet + StringBuilder 简单实现(下述代码代替公用模板toDO部分)
123456789101112131415161718192021// *****************************************************// 测试样例:生成防伪码长度为10,防伪码个数为1,000,000的运算时间// 测试环境:Win7 i5-4670T 16GB内存// 测试结果:826.5098s// *****************************************************HashSet<String> result = new HashSet<String>();Random random = new Random();StringBuilder sb = new StringBuilder(lengthOfSecurityCode);while (result.Count != numberOfSecurityCode){sb.Clear();for (int i = 0; i < lengthOfSecurityCode; ++i){// 生成一个随机数int idx = random.Next(0, charArr.Length);sb.Append(charArr[idx]);}result.Add(sb.ToString());}代码分析
- 思路:利用sb去存储字符组成字符串,因为防伪码不能重复,所以将生成防伪码后加入HashSet
思考过程的第二个实现 - HashSet + StringBuilder + 少量优化(下述代码代替公用模板toDO部分)
12345678910111213141516171819202122// *****************************************************// 测试样例:生成防伪码长度为10,防伪码个数为1,000,000的运算时间// 测试环境:Win7 i5-4670T 16GB内存// 测试结果:688.5649ms// *****************************************************HashSet<String> result = new HashSet<String>();Random random = new Random();StringBuilder sb = new StringBuilder(lengthOfSecurityCode);int charArrlength = charArr.Length;while (result.Count != numberOfSecurityCode){sb.Length = 0;for (int i = 0; i < lengthOfSecurityCode; ++i){// 生成一个随机数int idx = random.Next(charArrlength);sb.Append(charArr[idx]);}result.Add(sb.ToString());}代码分析
- 上述代码我的优化如下,下面的数据采用了控制变量进行测量
- 优化一:使用了
charArrlength
代替了charArr.Length
- 成功优化的原因:避免多次调用
- 优化结果:减少 715.5070ms - 688.5649ms = 26.9421ms
- 优化二:使用了
random.Next(charArrlength)
代替了random.Next(0, charArrlength)
- 成功优化的原因:未知
- 优化结果:减少 798.8720ms - 688.5649ms = 110.3071ms
- 优化三:使用了
sb.Length = 0
代替了sb.clear()
- 成功优化的原因:未知
- 优化结果:减少 692.9382ms - 688.5649ms = 4.3733ms
- 优化一:使用了
- 一开始我以为对性能优化最多的应该是优化一。没想到却是偶尔一试的优化二使得时间大幅下降,实在是意料之外。奈何自身水平有限,不知道优化二为何能够如此大幅度降低运行时间,猜想应该是
random.Next(0, charArrlength)
在每次循环内进行了两次判断(一次判断是否大于等于0,一次判断是否小于charArrlength),而random.Next(charArrlength)在每次循环内只判断了上界,所以效率更高。
- 上述代码我的优化如下,下面的数据采用了控制变量进行测量
思考过程的第三个实现 - HashSet + 变长字符数组(下述代码代替公用模板toDO部分)
123456789101112131415161718192021// *****************************************************// 测试样例:生成防伪码长度为10,防伪码个数为1,000,000的运算时间// 测试环境:Win7 i5-4670T 16GB内存// 测试结果:627.5172ms// *****************************************************HashSet<String> result = new HashSet<String>();Random random = new Random();char[] charSb = new char[lengthOfSecurityCode];int charArrlength = charArr.Length;while (result.Count != numberOfSecurityCode){for (int i = 0; i < lengthOfSecurityCode; ++i){// 生成一个随机数int idx = random.Next(charArrlength);charSb[i] = charArr[idx];}result.Add(new String(charSb));}代码分析
- 使用了变长字符数组来代替StringBuilder
- 成功优化的原因:字符数组非对象,减少CLI管理内存的时间损耗以及对象生成时间
- 优化结果:减少 688.5649ms - 627.5172ms = 61.0477ms
- 思考过后,还是觉得论性能,原生数组应该比对象更加有优势,因此最终我采用了本方案
- 使用了变长字符数组来代替StringBuilder
- 总结:在内存冗余的计算机时代,这点性能优化显得微不足道。然而,在我看来,偶尔研究一下性能,能够更加有助于我们不断改进代码,写出更加高效精炼的“好代码”。愿与大家一同做会思考的工程师,而非只是堆代码的码农。
- Last Edited:2016.10.13
- Author:@Seahub
- Please contact me if you want to share this Article, 3Q~