性能分析测试
一、 开发环境
l Windows10 版本号1903
l RAM 16GB 3200MHz 三星
l AMD Ryzen 7 2700X 3.90GHz
l SSD 三星 MZVLB1TOHALR-00000
二、 文件读入
在进行测试的过程中发现1e6的数据读入大概需要300秒,远远高于写入的6秒钟。显然由于硬盘限制写入时间应该大于读入,因此文件读入代码必然是有问题的,时间是不可接受的。
原文件读入的代码如下:
- inline void mallocSudoku(Sudoku& s)
- {
- if (s == nullptr)
- {
- s = new int* [10];
- for (int i = 0; i < 10; i++)
- {
- s[i] = new int[10];
- }
- 10. }
11. }
- 12.
13. inline void freeSudoku(Sudoku& s)
14. {
- 15. if (s != nullptr)
- 16. {
- 17. for (int i = 0; i < 10; i++)
- 18. {
- 19. delete[] s[i];
- 20. }
- 21. delete[] s;
- 22. s = nullptr;
- 23. }
24. }
- 25.
26. inline void readLineFromFile(char* line)
27. {
- 28. char* tmp = new char;
- 29. DWORD num_bytes_read = 0;
- 30. for (int i = 0; i < 9; i++)//依据约定,一行最多有9个数字
- 31. {
- 32. *tmp = 0;
- 33. while (*tmp < '0' || *tmp > '9')
- 34. {
- 35. if (ReadFile(h_sudoku_problem_txt, tmp, 1, &num_bytes_read, NULL) && num_bytes_read == 0)
- 36. {
- 37. is_end = true;
- 38. return;
- 39. }
- 40. //num_bytes_read++;
- 41. }
- 42. line[i] = *tmp;
- 43. }
- 44. delete tmp;
45. }
- 46.
47. inline bool getSudokuFromFile(Sudoku s)
48. {
- 49. char* line = new char[9];
- 50. for (int i = 0; i < 9; i++)
- 51. {
- 52. readLineFromFile(line);
- 53. if (is_end)
- 54. return false;
- 55. for (int j = 0; j < 9; j++)
- 56. s[i + 1][j + 1] = line[j] - '0';
- 57. }
- 58.
- 59. delete[] line;
- 60. return true;
61. }
- 62.
63. //一次性读入一千个数独
64. inline void readSudoku()
65. {
- 66. for (int i = 0; i < BUFF_SIZE; i++)
- 67. {
- 68. Sudoku tmp = nullptr;
- 69. mallocSudoku(tmp);
- 70. if (getSudokuFromFile(tmp) == false)
- 71. {
- 72. freeSudoku(tmp);
- 73. return;
- 74. }
- 75. buff.push_back(tmp);
- 76. }
- 77. return;
- }
分析代码,不难发现问题所在,那就是调用一次ReadFile只读入了一个字节的数据。原代码主要是考虑到可能的数独文件格式问题,通过再次阅读问题要求,发现数独问题文件格式是固定的,因此可一次读入163个字节(与输出一致)。由于最后一个数独的后面没有空行,因此读入的数据是162个字节,由此可以作为结束判断。另外一方面,可以一次性读入多个数独进行分析。若读入字节数小于163的整数倍,则已读完。经过一系列测试,代码修改如下:
- inline void toSudoku(char* tmp, DWORD& n_bytes_read)
- {
- //由于读取时的限制,num_of_sudoku_in_buff <= BUFF_SIZE
- num_of_sudoku_in_buff = n_bytes_read / num_bytes_of_sudoku_infile;
- if (is_end)//因为结束时,向下取整,少了一个
- {
- num_of_sudoku_in_buff++;
- }
- for (int i = 0, j = 0; i < num_of_sudoku_in_buff; i++, j++)
- 10. {
- 11. Sudoku s = buff[i];
- 12. for (int row_idx = 1, col_idx = 1; j < (i + 1) * num_bytes_of_sudoku_infile - 1; j++, j++)
- 13. {
- 14. s[row_idx][col_idx++] = tmp[j] - '0';
- 15. if (col_idx == 10)
- 16. {
- 17. row_idx++;
- 18. col_idx = 1;
- 19. }
- 20. }
- 21. }
22. }
- 23.
24. //一次性读入一千个数独
25. inline void readSudoku()
26. {
- 27. char* tmp = new char[num_bytes_of_sudoku_infile * BUFF_SIZE + 10];
- 28. ReadFile(h_sudoku_problem_txt, tmp, num_bytes_of_sudoku_infile * BUFF_SIZE, &n_bytes_read, NULL);
- 29. if (num_bytes_of_sudoku_infile * BUFF_SIZE > n_bytes_read)
- 30. is_end = true;
- 31. toSudoku(tmp, n_bytes_read);
- 32. return;
- }
同时在文件读入修改中,将原先的vector更改为了数组形式,通过提前申请空间的方式减少了在获取数独和求解数独后释放数独空间的时间。
在只更改文件读入方式的情况下,时间从300秒(1e6数据量)降到了11秒,在将vector更改为数独数组之后,减少了new 和 delete的时间,又降低到了8秒钟。
在release模式下进行测试速度可达2秒钟(包括求解)。
三、 通过性能测试工具进行分析提高
1. 首先进行普通运行
数独生成
在release模式下,生成1e6数据所需时间如下:
数独求解
在release模式下,对1E6的数独进行求解,所需时间如下:
2. 性能测试
性能分析测试是在debug模式下进行。
性能测试工具选择的是Intel Vtune Profile,尽管我的开发平台是AMD会对其有所限制,但是基本功能都能使用。运行分析如下:
数独生成
a) 运行时间
b) 基本信息
c) 不同模块CPU占用率
d) 不同函数CPU占用时间
数独求解
a) 运行时间
b) 基本信息
c) 不同模块CPU占用率
d) 不同函数占用CPU时间
3. 分析
数独生成
在数独生成算法中,最占用时间的就是swap函数和copySudoku函数。其中swap函数主要在数独生成算法中使用,占用时间过多是因为在整个算法过程中进行了非常频繁的调用。主要是为交换数独之间的行数据和交换第一行列间的数据以生成全排列。其中在调用频次上前者大概是后者的720倍。因此应当考虑采取不同的方式尽可能降低swap的调用次数。
而copySudoku函数是把数独转为缓冲区里的char等待写入,这一步不能省略,因此很难进行提高。
writefile函数是Windows的内核API,本身无法更改代码进行提高。但是或许可以减少writefile的调用次数,但是这就需要开辟更大的内存空间。
数独求解
不难发现数独求解算法中最占用时间的就是对数独进行求解。而这一步几乎已经优化到了极限,目前没有很好的思路去做优化。initSolver是数独求解前的数据初始化内容,可以考虑减少与内存的交互等方式进行优化。
其次就是数独打印模块中的copySudoku,分析同上。
另外toSudoku是从文件中读取数独的关键算法,是把字符转为可操作的数独的关键一步,通过分析代码,难以进行性能提升。
writefile和readfile的分析同上。
四、 性能提升方案
模块 |
函数 |
改进方法 |
数独生成 |
swap |
Sudoku是int**,在交换两行时可以直接交换地址,无需交换内部数据 |
数独读入/打印 |
Writefile(), ReadFile() |
改变BUFF_SIZE大小,一次性读取、写入更多数据,减少API调用 |
1. swap
函数更改前后
- void swapRow(int n, int m)
- {
- //for (int i = 0; i < 10; i++)
- //{
- // //std::swap(sudoku[n][i], sudoku[m][i]);
- //}
- std::swap(sudoku[n], sudoku[m]);
- }
不难发现性能有了显而易见的提升。
2. ReadFile() WriteFile()
BUFF_SIZE的值是每次读取或写入的数独的数量
不同BUFF_SIZE对性能影响如下,分析数独求解功能得到下表,没有多次进行,仅做参考(Debug模式下):
BUFF_SIZE |
ReadFile()时间 |
WriteFile()时间 |
总用时 |
10 |
0.704s |
0.349s |
8.852s |
100 |
0.080s |
0.131s |
7.899 |
1000 |
0.040s |
0.099s |
7.749s |
10000 |
0.050s |
0.080s |
8.281s |
100000 |
0.050s |
0.080s |
8.242s |
1000000 |
0.060s |
0.085s |
11.485s |
不难发现当BUFF_SIZE设置为1000时最为合适。BUFF_SIZE的过大读写时间并不会有打的变化,但是诸如内存访问、段页调度等需要较大时间开销,而BUFF_SIZE过小则会导致ReadFile()和 WriteFile()的读取写入时间急剧增加。
综合以上分析将BUFF_SIZE定位1000较为合适。
五、 改进后时间测试
1. Release模式
数独生成对比
改进前
改进后
数独求解对比
改进前
改进后
六、 总结
结果发现release模式下数独生成时间几乎没有变化,数独求解也仅仅是提升了0.1s。但是在debug模式下时间确实有很大变化。怀疑是在release生成过程中VS对代码进行了大量而复杂的优化,所以我优化后的结果几乎没有改观。