成长路上除了 发表于 2019-6-3 15:59:33

MATLAB性能测试框架


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

% 对不预先分配的数组X赋值计时
tic
for r = 1:cols
    for c = 1:rows
      X(r,c) = 1;
    end
end
toc

% 对预先分配的数组Y赋值计时
tic
Y = zeros(rows,cols);
for r = 1:cols
    for c = 1:rows
      Y(r,c) = 1;
    end
end
toc
运行结果可以预料,预先分配数组赋值比不预先分配更快。
% Command Line,fontsize=\small
>> alloc_tictoc
Elapsed time is 0.449438 seconds.    % 不预先分配
Elapsed time is 0.016257 seconds.    % 预先分配
tic,toc可以快速简单的获得定性的结果,但是有时候,在工程计算中需要代码耗时的定量结果,比如对1000 X 1000的数组赋值,想确切知道预先分配比不预先分配究竟快多少? ,再使用tic toc就捉襟见肘了,运行上述script多次, 可以发现得到的其实是一些随机的分布的结果。
>> alloc_tictoc
Elapsed time is 0.472567 seconds.
Elapsed time is 0.014476 seconds.
>> alloc_tictoc
Elapsed time is 0.434714 seconds.
Elapsed time is 0.016879 seconds.
>> alloc_tictoc
Elapsed time is 0.448822 seconds.
Elapsed time is 0.012684 seconds.
>> alloc_tictoc
Elapsed time is 0.474179 seconds.
Elapsed time is 0.013808 seconds.
>> alloc_tictoc
Elapsed time is 0.467369 seconds.
Elapsed time is 0.014176 seconds.
定性的来说,可以肯定预先分配数组的方法要快得多,但是每次测量得到的结果,其实是符合一定分布规律的随机变量{(MATLAB的每一步的计算都要经过确定的函数和优化,从这个角度来说,每次测量应该得到精确唯一的结果。现实中,MATLAB工作在操作系统中,而操作系统会统筹分配系统的计算资源,不同的时刻,资源的分配不一定相同,从而带来了一定的随机性。) },测量结果在一定的范围内波动给获得定量结果造成困难。当两个算法的差别不是很大的时候,这样的波动可能甚至会影响定性的结果。如何得到可靠的性能测量的数据就是我们这章要解决的问题。最容易想到的一个改进就是把运行多次,把每次的结果收集起来,然后求平均,比如:
tic
for iter = 1: 100
for r = 1:cols
    for c = 1:rows
      X(r,c) = 1;
    end
end
end
toc
% 再把得到的结果求平均,略
但是循环的次数很难有一个统一的标准,到底循环多少次结果求平均才可靠,次数少了结果不可靠,次数多了浪费时间。还有,理论上能否保证提高循环次数就一定可以得到统计意义上可靠的结果?一个严谨的性能测试不但需要一套规范的标准,还需要统计理论的支持。 另一个测量性能时要注意的问题是:如下所示,测量结果可能对algorithm1不公平,因为MATLAB的代码在第一次运行时候会伴随编译和优化,比如Just In Time Compilation(JIT)和最新的Language Execution Engine(LXE)的加速,这就是说,前几次运行的代码会有一些编译和优化带来的的耗时,可以把它们想象成运动之前的热身,如果algorithm1和algorithm2共用一些的代码,那么algorithm1运行时,可能已经帮助algorithm2热了一部分的身,而带来的额外时间却算在了algorithm1的耗时内
% 代码优化可能带来额外的耗时,fontsize=\small
% 计时算法1
tic
algorithm1();   
toc

% 计时算法2
tic
algorithm2();
toc
所以更公平的测时方法是,剔除前几次的运行,让要比较的代码都热完身之后再计时
% 剔除代码优化可能带来额外的耗时,fontsize=\small
% 算法1热身4次
for iter= 1:4
algorithm1()
end
% 计时算法1
tic
algorithm1();
toc
% 算法2热身4次
for iter= 1:4
algorithm2()
end
% 计时算法2
tic
algorithm2();
toc
基于类的(Class-Based)性能测试框架
构造测试类
构造一个基于类的Performance测试很简单,我们只需要把Performance Test一节中的脚本转成性能测试中的方法即可。 任何基于类的Performance 测试类都要继承自matlab.perftest.TestCase父类,也就是框架的提供者;下面的类定义中,还把rows和cols两个变量放到了类的属性中,这样test1和test2可以共享这两个变量。
% AllocTest, fontsize=\small
classdef AllocTest < matlab.perftest.TestCase   % 性能测试的公共父类
    properties
      rows = 1000
      cols = 1000
    end   
    methods(Test)
      % 不预先分配赋值 测试点
      function test1(testCase)   
            for r = 1:testCase.cols
                for c = 1:testCase.rows
                  X(r,c) = 1;
                end
            end
      end      
      % 预先分配赋值 测试点
      function test2(testCase)
            X = zeros(testCase.rows,testCase.cols);         
            for r = 1:testCase.cols
                for c = 1:testCase.rows
                  X(r,c) = 1;
                end
            end
      end
    end
end
运行runperf开始Performance测试
>> r = runperf('AllocTest')
Running AllocTest
..........
.......
Done AllocTest
__________
r =

1x2 MeasurementResult array with properties:

    Name
    Valid
    Samples
    TestActivity

Totals:
   2 Valid, 0 Invalid
runperf返回一个1X2的结果对象数组,两个测试点都是合格的测试,

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

            Name: 'AllocTest/test1'
         Valid: 1
         Samples:    %简报
    TestActivity:     %原始数据
Totals:
   1 Valid, 0 Invalid.
其中属性TestActivity是测量的所有测量原始数据,原始sample是有用数据的简报,这里解析TestActivity中的原始数据
>> r(1).TestActivity

ans =

         Name          Passed    Failed    Incomplete    MeasuredTime    Objective      
    _______________    ______    ______    __________    ____________    _________

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

ans =

MeasurementResult with properties:

            Name: 'AllocTest/test2'
         Valid: 1
         Samples:       %简报
    TestActivity:    %原始数据

Totals:
   1 Valid, 0 Invalid.
>>
>> r(2).TestActivity

ans =

         Name          Passed    Failed    Incomplete    MeasuredTime    Objective   
    _______________    ______    ______    __________    ____________    _________   

    AllocTest/test2    true      false   false         0.018707      warmup      
    AllocTest/test2    true      false   false         0.028393      warmup      
    AllocTest/test2    true      false   false         0.013336      warmup      
    AllocTest/test2    true      false   false         0.012915      warmup      
    AllocTest/test2    true      false   false         0.013543      sample      
    AllocTest/test2    true      false   false         0.012904      sample      
    AllocTest/test2    true      false   false         0.012778      sample      
    AllocTest/test2    true      false   false          0.01312      sample   
test2有4次warmup,4次sample测试。 按照默认设置,每个测试点都要先warmup代码四次,再进入正式的sample测试,有四个sample测试意味着test2这个测试点一共被运行了四次,test2的测试次数和test1的测试次数不同,每个测试点运行几次是由测量数据集合是否到达统计目标所决定的,误差范围和置信区间小节将详细介绍。 有了多次测量的结果,我们可以利用一个帮助函数,从table中取出sample的数据,
function dispMean(result)
fullTable = vertcat(result.Samples);
varfun(@mean,fullTable,'InputVariables','MeasuredTime','GroupingVariables','Name')
end
然后对它们求均值,得到的结果才是统计意义上的测量结果。
>> dispMean(r)

ans =

         Name          GroupCount    mean_MeasuredTime
    _______________    __________    _________________

    AllocTest/test1    5            0.38996         
    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,于是停止继续测量
% 计算Relative Margin of Error的函数, fontsize = \small
function er = relMarOfEr(data)
L = length(data);
er = tinv(0.95,L-1)*std(data)/mean(data)/sqrt(L);
end
>> relMoE(r(1).Samples.MeasuredTime(1:end-1))   % 取test1的第1到第4次测量结果
ans =
    0.0519

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

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

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

Normal distribution
       mu =0.389962      % 0.95置信区间
    sigma = 0.0172059   
0.05的Margin of Error并不是所有的测试都能达到,事实上我们如果多次运行上述的同一个测试,很有可能test2的结果会有几次含有Warning。
>> r = runperf('AllocTest')
Running AllocTest
..........
..........
..........
..........
....Warning: The target Relative Margin of Error was not met after running the MaxSamples for
AllocTest/test2.
% 测试点运行超过32次任没有达到统计目标
Done AllocTest
__________

r =

1x2 MeasurementResult array with properties:

    Name
    Valid
    Samples
    TestActivity

Totals:
   2 Valid, 0 Invalid.
>>
>> r(2)

ans =

MeasurementResult with properties:

            Name: 'AllocTest/test2'
         Valid: 1
         Samples:
    TestActivity:     % test2运行了一共4+32=36次

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


ans =

1x2 MeasurementResult array with properties:

    Name
    Valid
    Samples
    TestActivity

Totals:
   2 Valid, 0 Invalid.

性能测试的适用范围讨论
性能测试框架最初是Mathworks内部使用的一个框架,使用范围和单元测试一致,单元测试保证在算法的进化过程中,功能不退化;而性能测试保证算法的性能不退化。这样一个框架对MATLAB用户的算法开发显然会带来价值,但是我们要分清什么样的测量才是有价值的,构造测试类中的例子是一个简单易懂的例子,但作为MATLAB的用户,我们其实没有必要去测量和记录这些简单的MATLAB的操作的性能(这是Mathworks内部性能测试的主要工作) ,我们只需要记住它们定性的结果,比如给数组赋值之前要先分配,运算尽量向量化等等就可以了。性能测试框架真正能给我们带来的价值的用例,是如下的测试实际算法性能的情况,在用户的算法myAlgorithm的开发过程中,我们可以定期的运行该测试文件,保证性能不退化
classdef AlgoTest1 < matlab.perftest.TestCase
    methods(Test)
      function test1(testCase)   
               myAlgorithm();
      end
    end
end
或者比较两个算法,algorithm1可以代表一个旧的算法,algorithm2代表新的改进的算法,依靠Performance Testing 框架,我们可以得到可靠的数据到底algorithm2改进了多少
classdef AlgoTest1 < matlab.perftest.TestCase
    methods(Test)
      function test1(testCase)   
               algorithm1();
      end
      
      function test2(testCase)            
               algorithm2();
      end
    end
end
页: [1]
查看完整版本: MATLAB性能测试框架