Words Of Life

从小实验探代码优化

  • 摘要:一个C#小实验引起的代码性能探究


    前言

  • 本文源于一个C#小实验:生成防伪码。与其他以“完成目标”为要求的实验不同,该实验以“性能优化”为要求。实验题目(见下)很简单,简单的数行代码即可实现。而如何让代码达到最佳的性能,即以最短的运行时间运行,成为了我纠结的问题。此处仅以此文记录我的思考过程,望日后有助于同行。

    • 小实验题目:生成防伪码
    1. 防伪码由以下字符组成:0123456789ABCDEFGHJKLMNPQRSTUVWXYZ(数字1和字母I相近、数字0和字母O相近,所以去掉字母I和字母O,全部字母大写)
    2. 在命令行中输入2个参数,分别是:防伪码长度与防伪码个数。例如:在命令行中调用程序为“应用程序.exe 10 10000”指的是防伪码长度为10,生成10000个防伪码。
    3. 输出结果:时间(单位:ms)
    • 注意
    1. 不需要输出每个防伪码(在调试时输出防伪码可用于检验生成码的正确性,正式输出时只输出一项结果即运行程序所需要的时间)
    2. 防伪码的长度由命令行参数决定
    3. 所生成的防伪码不能重复(按照以上例子,生成了10000个防伪码,这10000个防伪码就肯定不能重复)
  • 先贴出公用模板 - 下述其他代码默认嵌套在此模板内执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // *****************************************************
    // 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部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // *****************************************************
    // 测试样例:生成防伪码长度为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部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // *****************************************************
    // 测试样例:生成防伪码长度为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部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // *****************************************************
    // 测试样例:生成防伪码长度为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
    • 思考过后,还是觉得论性能,原生数组应该比对象更加有优势,因此最终我采用了本方案

  • 总结:在内存冗余的计算机时代,这点性能优化显得微不足道。然而,在我看来,偶尔研究一下性能,能够更加有助于我们不断改进代码,写出更加高效精炼的“好代码”。愿与大家一同做会思考的工程师,而非只是堆代码的码农。

  • Last Edited:2016.10.13
  • Author:@Seahub
  • Please contact me if you want to share this Article, 3Q~
五毛也是情, 一元也是爱