设为首页收藏本站

EPS数据狗论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1346|回复: 0

MATLAB性能测试框架

[复制链接]

32

主题

313

金钱

473

积分

入门用户

发表于 2019-6-3 15:59:33 | 显示全部楼层 |阅读模式

为什么需要Performance Test框架
MATLAB Performance Test 框架是Mathworks在MATLAB R2016a中推出的一个新的框架,该框架用来获得代码性能在统计意义上的数据,还可以用来比较算法的性能,并且给出详细完整的报告。 如果只需要定性的性能结果,tic和toc是一个快速简单的获得代码耗时的工具,大家一定都使用过。比如下面的代码,比较对数组的不同赋值方式,衡量预先分配和不预先分配的耗时差别。
  1. % alloc_tictoc.m
  2. rows = 1000;
  3. cols = 1000;
  4. X=[];
  5. Y=[];

  6. % 对不预先分配的数组X赋值计时
  7. tic
  8. for r = 1:cols
  9.     for c = 1:rows
  10.         X(r,c) = 1;
  11.     end
  12. end
  13. toc
  14.   
  15. % 对预先分配的数组Y赋值计时
  16. tic
  17. Y = zeros(rows,cols);
  18. for r = 1:cols
  19.     for c = 1:rows
  20.         Y(r,c) = 1;
  21.     end
  22. end  
  23. toc
复制代码

运行结果可以预料,预先分配数组赋值比不预先分配更快。
  1. % Command Line,fontsize=\small
  2. >> alloc_tictoc
  3. Elapsed time is 0.449438 seconds.    % 不预先分配
  4. Elapsed time is 0.016257 seconds.    % 预先分配
复制代码

tic,toc可以快速简单的获得定性的结果,但是有时候,在工程计算中需要代码耗时的定量结果,比如对1000 X 1000的数组赋值,想确切知道预先分配比不预先分配究竟快多少? ,再使用tic toc就捉襟见肘了,运行上述script多次, 可以发现得到的其实是一些随机的分布的结果。
  1. >> alloc_tictoc
  2. Elapsed time is 0.472567 seconds.
  3. Elapsed time is 0.014476 seconds.
  4. >> alloc_tictoc
  5. Elapsed time is 0.434714 seconds.
  6. Elapsed time is 0.016879 seconds.
  7. >> alloc_tictoc
  8. Elapsed time is 0.448822 seconds.
  9. Elapsed time is 0.012684 seconds.
  10. >> alloc_tictoc
  11. Elapsed time is 0.474179 seconds.
  12. Elapsed time is 0.013808 seconds.
  13. >> alloc_tictoc
  14. Elapsed time is 0.467369 seconds.
  15. Elapsed time is 0.014176 seconds.
复制代码

定性的来说,可以肯定预先分配数组的方法要快得多,但是每次测量得到的结果,其实是符合一定分布规律的随机变量{(MATLAB的每一步的计算都要经过确定的函数和优化,从这个角度来说,每次测量应该得到精确唯一的结果。现实中,MATLAB工作在操作系统中,而操作系统会统筹分配系统的计算资源,不同的时刻,资源的分配不一定相同,从而带来了一定的随机性。) },测量结果在一定的范围内波动给获得定量结果造成困难。当两个算法的差别不是很大的时候,这样的波动可能甚至会影响定性的结果。如何得到可靠的性能测量的数据就是我们这章要解决的问题。最容易想到的一个改进就是把运行多次,把每次的结果收集起来,然后求平均,比如:
  1. tic
  2. for iter = 1: 100
  3.   for r = 1:cols
  4.     for c = 1:rows
  5.         X(r,c) = 1;
  6.     end
  7.   end
  8. end
  9. toc
  10. % 再把得到的结果求平均,略
复制代码

但是循环的次数很难有一个统一的标准,到底循环多少次结果求平均才可靠,次数少了结果不可靠,次数多了浪费时间。还有,理论上能否保证提高循环次数就一定可以得到统计意义上可靠的结果?一个严谨的性能测试不但需要一套规范的标准,还需要统计理论的支持。 另一个测量性能时要注意的问题是:如下所示,测量结果可能对algorithm1不公平,因为MATLAB的代码在第一次运行时候会伴随编译和优化,比如Just In Time Compilation(JIT)和最新的Language Execution Engine(LXE)的加速,这就是说,前几次运行的代码会有一些编译和优化带来的的耗时,可以把它们想象成运动之前的热身,如果algorithm1和algorithm2共用一些的代码,那么algorithm1运行时,可能已经帮助algorithm2热了一部分的身,而带来的额外时间却算在了algorithm1的耗时内
  1. % 代码优化可能带来额外的耗时,fontsize=\small
  2. % 计时算法1
  3. tic
  4. algorithm1();   
  5. toc

  6. % 计时算法2
  7. tic
  8. algorithm2();
  9. toc
复制代码

所以更公平的测时方法是,剔除前几次的运行,让要比较的代码都热完身之后再计时
  1. % 剔除代码优化可能带来额外的耗时,fontsize=\small
  2. % 算法1热身4次
  3. for iter  = 1:4
  4. algorithm1()
  5. end
  6. % 计时算法1
  7. tic
  8. algorithm1();
  9. toc
  10. % 算法2热身4次
  11. for iter  = 1:4
  12. algorithm2()
  13. end
  14. % 计时算法2
  15. tic
  16. algorithm2();
  17. toc
复制代码

基于类的(Class-Based)性能测试框架
构造测试类
构造一个基于类的Performance测试很简单,我们只需要把Performance Test一节中的脚本转成性能测试中的方法即可。 任何基于类的Performance 测试类都要继承自matlab.perftest.TestCase父类,也就是框架的提供者;下面的类定义中,还把rows和cols两个变量放到了类的属性中,这样test1和test2可以共享这两个变量。
  1. % AllocTest, fontsize=\small
  2. classdef AllocTest < matlab.perftest.TestCase   % 性能测试的公共父类
  3.     properties
  4.         rows = 1000
  5.         cols = 1000
  6.     end   
  7.     methods(Test)
  8.         % 不预先分配赋值 测试点
  9.         function test1(testCase)   
  10.             for r = 1:testCase.cols
  11.                 for c = 1:testCase.rows
  12.                     X(r,c) = 1;
  13.                 end
  14.             end
  15.         end        
  16.         % 预先分配赋值 测试点
  17.         function test2(testCase)
  18.             X = zeros(testCase.rows,testCase.cols);         
  19.             for r = 1:testCase.cols
  20.                 for c = 1:testCase.rows
  21.                     X(r,c) = 1;
  22.                 end
  23.             end
  24.         end
  25.     end
  26. end
复制代码

运行runperf开始Performance测试
  1. >> r = runperf('AllocTest')
  2. Running AllocTest
  3. ..........
  4. .......
  5. Done AllocTest
  6. __________
  7. r =

  8.   1x2 MeasurementResult array with properties:

  9.     Name
  10.     Valid
  11.     Samples
  12.     TestActivity

  13. Totals:
  14.    2 Valid, 0 Invalid
复制代码

runperf返回一个1X2的结果对象数组,两个测试点都是合格的测试,

测试结果解析
在命令行中检查对象数组中的一个元素,即test1的测试结果
  1. >> r(1)
  2. ans =
  3.   MeasurementResult with properties:

  4.             Name: 'AllocTest/test1'
  5.            Valid: 1
  6.          Samples: [5x7 table]     %简报  
  7.     TestActivity: [9x12 table]    %原始数据
  8. Totals:
  9.    1 Valid, 0 Invalid.
复制代码

其中属性TestActivity是测量的所有测量原始数据,原始sample是有用数据的简报,这里解析TestActivity中的原始数据
  1. >> r(1).TestActivity

  2. ans =

  3.          Name          Passed    Failed    Incomplete    MeasuredTime    Objective        
  4.     _______________    ______    ______    __________    ____________    _________  

  5.     AllocTest/test1    true      false     false         0.52387         warmup      
  6.     AllocTest/test1    true      false     false         0.44674         warmup      
  7.     AllocTest/test1    true      false     false         0.50816         warmup      
  8.     AllocTest/test1    true      false     false         0.38104         warmup      
  9.     AllocTest/test1    true      false     false         0.38372         sample      
  10.     AllocTest/test1    true      false     false          0.4197         sample      
  11.     AllocTest/test1    true      false     false         0.38647         sample      
  12.     AllocTest/test1    true      false     false         0.38489         sample      
  13.     AllocTest/test1    true      false     false         0.37503         sample   
复制代码

测量的结果是一个table对象,从结果中看出,测试一共进行了9次,前4次是这一节尾提到对代码的热身,这四次的结果在Objective中标记被做warmup,从数值上也可以大致看出它们和后5次测量有着不同的分布,计算均值的时候需要把它们剔除,正式的测试标记做sample测试,test1的sample测试一共运行了5次。检查r(2)得到类似的结果:
  1. % Command Line , fontsize = \small
  2. >> r(2)

  3. ans =

  4.   MeasurementResult with properties:

  5.             Name: 'AllocTest/test2'
  6.            Valid: 1
  7.          Samples: [4x7 table]      %简报
  8.     TestActivity: [8x12 table]     %原始数据

  9. Totals:
  10.    1 Valid, 0 Invalid.
  11. >>
  12. >> r(2).TestActivity

  13. ans =

  14.          Name          Passed    Failed    Incomplete    MeasuredTime    Objective   
  15.     _______________    ______    ______    __________    ____________    _________   

  16.     AllocTest/test2    true      false     false         0.018707        warmup      
  17.     AllocTest/test2    true      false     false         0.028393        warmup      
  18.     AllocTest/test2    true      false     false         0.013336        warmup      
  19.     AllocTest/test2    true      false     false         0.012915        warmup      
  20.     AllocTest/test2    true      false     false         0.013543        sample      
  21.     AllocTest/test2    true      false     false         0.012904        sample      
  22.     AllocTest/test2    true      false     false         0.012778        sample      
  23.     AllocTest/test2    true      false     false          0.01312        sample   
复制代码

test2有4次warmup,4次sample测试。 按照默认设置,每个测试点都要先warmup代码四次,再进入正式的sample测试,有四个sample测试意味着test2这个测试点一共被运行了四次,test2的测试次数和test1的测试次数不同,每个测试点运行几次是由测量数据集合是否到达统计目标所决定的,误差范围和置信区间小节将详细介绍。 有了多次测量的结果,我们可以利用一个帮助函数,从table中取出sample的数据,
  1. function dispMean(result)
  2. fullTable = vertcat(result.Samples);
  3. varfun(@mean,fullTable,'InputVariables','MeasuredTime','GroupingVariables','Name')
  4. end
复制代码

然后对它们求均值,得到的结果才是统计意义上的测量结果。
  1. >> dispMean(r)

  2. ans =

  3.          Name          GroupCount    mean_MeasuredTime
  4.     _______________    __________    _________________

  5.     AllocTest/test1    5              0.38996         
  6.     AllocTest/test2    4              0.013086      
复制代码

如果算法在不断的变化中,这样的测量结果也可以保留起来,从而追踪一段时间之内算法性能的变化。

误差范围和置信区间
Performance测试框架规定,一个测试点warmup四次之后,将再运行4到32不等的次数,直到测量数据达到0.05的Relative Margin of Error,0.95的置信区间为止,一但已有的测量值到达了上述的统计目标,就停止计算,如果超过32次还是没有达到0.05的Relative Margin of Error,框架仍然停止计算,但抛出一个警告。这就是为什么前节的test1运行了5次,而test2只运行了4次,它更快达到统计目标。 在每获得一次新的测量数据时,已有数据的Relative Margin of Error都将被重新计算,来决定是否需要再次运行测试点。下面的函数(需要统计工具箱) 帮助计算Relative Margin of Error, 用它来计算test1的数据可以验证相对误差在得到第4次测量结果仍然大于0.05,直到第5次计算小于0.05,于是停止继续测量
  1. % 计算Relative Margin of Error的函数, fontsize = \small
  2. function er = relMarOfEr(data)
  3. L = length(data);
  4. er = tinv(0.95,L-1)*std(data)/mean(data)/sqrt(L);
  5. end
复制代码
  1. >> relMoE(r(1).Samples.MeasuredTime(1:end-1))   % 取test1的第1到第4次测量结果
  2. ans =
  3.     0.0519

  4. >> relMoE(r(1).Samples.MeasuredTime)            % 取test1所有测量结果
  5. ans =
  6.     0.0421
复制代码

test2的测量结果类似,第4次的测量,整体数据达到统计目标
  1. >> relMoE(r(2).Samples.MeasuredTime(1:end-1))
  2. ans =
  3.     0.0529

  4. >> relMoE(r(2).Samples.MeasuredTime)
  5. ans =

  6.     0.0302
复制代码

所谓0.95的置信区间,就是说该系列的测量将确定一个区间,有百分之95的几率实际的真实值就在该区间中。调用函数fitdist得到置信区间(需要统计工具箱。)
  1. >> fitdist(r(1).Samples.MeasuredTime,'Normal')
  2. ans =
  3.   NormalDistribution

  4.   Normal distribution
  5.        mu =  0.389962   [0.368598, 0.411326]   % 0.95置信区间
  6.     sigma = 0.0172059   [0.0103086, 0.049442]
复制代码

0.05的Margin of Error并不是所有的测试都能达到,事实上我们如果多次运行上述的同一个测试,很有可能test2的结果会有几次含有Warning。
  1. >> r = runperf('AllocTest')
  2. Running AllocTest
  3. ..........
  4. ..........
  5. ..........
  6. ..........
  7. ....Warning: The target Relative Margin of Error was not met after running the MaxSamples for
  8. AllocTest/test2.
  9. % 测试点运行超过32次任没有达到统计目标
  10. Done AllocTest
  11. __________

  12. r =

  13.   1x2 MeasurementResult array with properties:

  14.     Name
  15.     Valid
  16.     Samples
  17.     TestActivity

  18. Totals:
  19.    2 Valid, 0 Invalid.
  20. >>
  21. >> r(2)

  22. ans =

  23.   MeasurementResult with properties:

  24.             Name: 'AllocTest/test2'
  25.            Valid: 1
  26.          Samples: [32x7 table]
  27.     TestActivity: [36x12 table]    % test2运行了一共4+32=36次

  28. Totals:
  29.    1 Valid, 0 Invalid.
复制代码

Warning说明测量的操作过于的细微,噪音影响过大。我们可以通过增大计算量,或者放松统计目标来避免这个Warning,比如修改默认的Relative Margin of Error
  1. % 增大Relative Margion of Error, fontsize = \small
  2. >> import matlab.perftest.TimeExperiment
  3. >> experiment = TimeExperiment.limitingSamplingError('RelativeMarginOfError',0.10);
  4. >> suite = testsuite('AllocTest');
  5. >> run(experiment,suite)
  6. Running AllocTest
  7. ..........
  8. ......
  9. Done AllocTest
  10. __________


  11. ans =

  12.   1x2 MeasurementResult array with properties:

  13.     Name
  14.     Valid
  15.     Samples
  16.     TestActivity

  17. Totals:
  18.    2 Valid, 0 Invalid.
复制代码


性能测试的适用范围讨论
性能测试框架最初是Mathworks内部使用的一个框架,使用范围和单元测试一致,单元测试保证在算法的进化过程中,功能不退化;而性能测试保证算法的性能不退化。这样一个框架对MATLAB用户的算法开发显然会带来价值,但是我们要分清什么样的测量才是有价值的,构造测试类中的例子是一个简单易懂的例子,但作为MATLAB的用户,我们其实没有必要去测量和记录这些简单的MATLAB的操作的性能(这是Mathworks内部性能测试的主要工作) ,我们只需要记住它们定性的结果,比如给数组赋值之前要先分配,运算尽量向量化等等就可以了。性能测试框架真正能给我们带来的价值的用例,是如下的测试实际算法性能的情况,在用户的算法myAlgorithm的开发过程中,我们可以定期的运行该测试文件,保证性能不退化
  1. classdef AlgoTest1 < matlab.perftest.TestCase
  2.     methods(Test)
  3.         function test1(testCase)   
  4.                myAlgorithm();
  5.         end
  6.     end
  7. end
复制代码

或者比较两个算法,algorithm1可以代表一个旧的算法,algorithm2代表新的改进的算法,依靠Performance Testing 框架,我们可以得到可靠的数据到底algorithm2改进了多少
  1. classdef AlgoTest1 < matlab.perftest.TestCase
  2.     methods(Test)
  3.         function test1(testCase)   
  4.                algorithm1();
  5.         end
  6.         
  7.         function test2(testCase)            
  8.                algorithm2();
  9.         end
  10.     end
  11. end
复制代码

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

客服中心
关闭
在线时间:
周一~周五
8:30-17:30
QQ群:
653541906
联系电话:
010-85786021-8017
在线咨询
客服中心

意见反馈|网站地图|手机版|小黑屋|EPS数据狗论坛 ( 京ICP备09019565号-3 )   

Powered by BFIT! X3.4

© 2008-2028 BFIT Inc.

快速回复 返回顶部 返回列表