OO_Unit2 关于性能优化与测试的那些事
那么既然是为了尽可能提高性能,我们首先就需要明确具体的性能指标,这样优化才能有针对性。前两个Task的优化指标是整个电梯系统的总运行时间,task3则引入了所有乘客的总等待时间。但无论是哪一个指标,它们都有一个共同的特点,那就是优化策略与数据之间存在客观依赖,而这正是Unit2与Unit1最大的不同。在Unit1中,导函数表达式的长度客观上必然存在最小值,且这个最小值仅仅取决于最开始输入的原函数,因此理论上我们可以找到一种最优策略(但可能需要搜索);而在Unit2中,由于是强制在线,因此总的运行时间取决于整个运行过程中的输入流,这就意味着在任何一个时刻,针对电梯的不同状态,我们都可以在下一个时刻针对性地投放一组新数据使得这个状态对应的性能指标不为全部状态集中的最优值。简单来说就是不存在绝对最优的优化策略。
既然不存在绝对最优,那么我们又是否可以退而求其次,找到一种期望最优的算法呢?很遗憾,答案也是否定的。因为,我们并不知道评测机在随机生成数据时采取的期望分布,是均匀分布还是指数分布还是正态分布,楼层数据与时序之间是否存在相关性,我们一概不知。但事实上,即便我们知道这些,在强测只有至多20个数据点,输入数据不超过50条这样的前提下,其实意义也不是太大,毕竟只是个期望。
到此,我们可以得出一个初步的结论,也就是全局最优是不存在的。但是电梯还得运行呀,那策略的设计又该从哪里入手呢?一个比较常规的切入点是使用一种市面上常见的调度算法,再在其基础上进行一定的改造。笔者也不例外,不过这里笔者选择的不是理论中的那些算法,而是索性就以现实中的电梯为基本参照,首先复现现实中的电梯策略,再看看能否在局部对其进行优化。至于如何进行局部优化,由于既然是局部,那么贪心策略就显然是一种不错的选择了。
综上,笔者在进行性能优化时的基本原则大概可以归结为以下几点:
-
从OO的角度出发,将优化策略化整为零地放在各个类中,类与类之间尽量避免因为优化而增加耦合度,同时尽可能地做到细粒度的优化。
-
由于不存在全局最优,因此调度策略的设计上笔者希望尽可能做到简洁有效,那么现实中的电梯就是一个很好的参考。同时在局部笔者也会进行一些基于贪心原则的细节优化,贪心的目标是尽量优先满足使得性能损耗较小的需求。
-
整体策略上努力避免对输入做出不必要的假设,也就是保证优化不向某一类型的输入数据过度倾斜。
-
以提高整体性能指标为第一优先,必要时可以牺牲一定的单梯性能(task2, task3)
架构
无论是什么优化,都离不开具体的底层架构设计。事实上,不同的架构设计可能在一开始就决定了不同的优化思路与方向,而同样的策略基于不同的架构实现其复杂度也不尽相同。
笔者整个Unit2的架构总体上没有太大的变化,可以参见下图所示:
其中Config为属性接口,保存了整个系统的一些基本参数常量,由于这些参数本身并不复杂但无奈又种类繁多,所以笔者索性把它们放到一起,这样也便于调整。TransFloorTable为静态换乘表,底层用HashMap嵌套实现,查询换乘的方式为( (所在层, 目标层), ( (电梯类型, 运行方向), 换乘层) )。换乘通过修改对应Passenger的TransFloor属性实现。Passenger就是请求数据类,而Pair和FloorLogOutput都是工具类。真正的核心类是Building, Scheduler与Elevator,主要的优化也正是围绕Scheduler与Elevator展开,而Floor与PollLog类则是为了使得整体实现(包括优化)更加细粒度而设计的。
优化
Elevator类
-
当前状态由curFloor属性表达,状态转移方向由direction属性表达。对于电梯线程而言,在任意时刻(楼层),只需要知道其下一刻的状态该如何更新即可,无需更多的信息(比如目标层之类的),这样可以使得电梯的运行更加灵活,从而有利于贪心策略的贯彻。
-
电梯应优先服务梯内乘客(这与现实中的电梯一致),直到电梯内没有乘客时,才向调度器scheduler请求新的方向。
-
每到一层,先下客,再上客。上客前若无人下客,会对因开门所导致的梯内乘客等待时间增加与潜在新乘客等待时间的减少进行一个比较权衡,再决定是否开门。同时,若上客完毕后外面仍然有乘客未上,会对梯内乘客的最远目标层与梯外乘客的最近目标层进行比较,若,会考虑进行换客。
Scheduler类
-
在电梯请求新的乘客时,直接从对应的Floor中弹出相应的乘客即可。若成功找到符合条件的乘客,会对其inEle属性进行检查,确保他没有被别的电梯线程同时获取。(这主要是为了配合Floor类的实现)
-
当电梯请求方向时,首先优先选择一个离电梯所在层最近的乘客的相对方向作为其方向。若此时该电梯不存在等待的乘客,Scheduler还会去查找之前的pollLogs信息,看看是否有乘客的目标换乘层恰好是此类电梯的可达层,若是,则会将此方向作为电梯的新方向。
Floor类
-
采用PriorityBlockingQueue实现各个电梯的等待队列,优先原则是目标层距离所在层近的乘客优先。
-
允许同一个乘客同时出现在不同电梯不同方向的等待队列中(考虑到不同换乘策略的存在),也就是尽可能允许电梯之间的公平竞争,而不是做任何不必要的预分配。
-
在调度器向其请求乘客时,若对方向无要求,则会根据楼层的相对位置决定一个优先方向。即高层优先向上,底层优先向下。这主要是顾及到A类电梯的可达楼层分布在大楼的两端,所以试图尽量避免A电梯的来回奔波。
TransFloorTable类
-
换乘表在设计时也尽量考虑到了不同的权重(楼层间移动时间)对于性能的影响,因此在设计时的初衷就是提高整体的性能,允许单梯的性能损失,即:
-
能够让A去做的尽量让A去做,比如1-14层除了B直达外,也可以允许A先将其送到15层。
-
尽量提高C类电梯的利用率,比如若请求在偶数层,且B类电梯此时运行方向恰与之相反,可以让其先把该乘客稍带到最近的奇数层,这样C类电梯就能派上用场,同时也不会太过消耗B电梯的容量资源。
-
以上就是笔者在整个Unit2所采用的全部优化策略了,从最后的强测表现来看,效果可以说还是挺不错的吧,但要说给这些优化的小trick起个共同的名字什么的,倒也真的是有些为难笔者了。但不管怎么说,绕来绕去都还是那句话,架构与优化从来都是辩证统一的,一方面明确了优化的基本原则有助于我们厘清架构的大致思路,另一方面好的架构也可以使得一些优化的策略显得更加自然简单,希望类似的思想在接下来的规格化设计中也可以祝各位看官一臂之力吧。
测试
除了优化,测试这块笔者也有几句话想同看官们叨叨。当然,相信厉害的mina一定都有自己本地的一套完整的测试体系了吧。因此笔者这里也不多做展开,还是只讲几个可能会有用的小trick。
CTLE的检查——如何获取程序的CPU时间?
-
关于这一点呢,其实Java的库中就已经提供了这样的方法,也就是
getProcessCpuTime()
。请见下方代码:
1 import java.lang.management.ManagementFactory; 2 import java.util.concurrent.CountDownLatch; 3 4 import com.sun.management.OperatingSystemMXBean; 5 6 public class Main { 7 private static CountDownLatch doneLatch; 8 9 public static void main(String[] args) { 10 doneLatch = new CountDownLatch(5); // only if more threads need to start 11 // do something 12 try { 13 doneLatch.await(); 14 } catch (InterruptedException e) { 15 e.printStackTrace(System.err); 16 } 17 getCpuTime(); 18 } 19 20 private static void getCpuTime() { 21 OperatingSystemMXBean opSys = (OperatingSystemMXBean) 22 ManagementFactory.getOperatingSystemMXBean(); 23 long nanoTime = opSys.getProcessCpuTime(); 24 System.err.println(nanoTime / 1e6); // ms 25 } 26 }
-
-
CountDownLatch的使用:
数据可视化——肉眼debug的好工具
在多部电梯的情况下,可能有些同学不仅想借助评测机进行测试,还想自己亲自看一看一些数据的运行效果或者就是想对评测机本身进行功能测试,但是眼花缭乱的输出又使人看的头大。那么这时候,一款可以将所有的输出log转化成图表的工具就显得非常重要了。这里笔者推荐python的pyplotlib模块,功能丰富又容易上手,可以说是可视化的上上之选。笔者借助里面的scatter()
,plot()
,text()
等API实现的最终效果图如下(实际运行时局部还可以放大):
相信在后面的单元中,如果数据的形式比较复杂的话(不一定是输出),类似的可视化工具也会给同学们提供一个不错的debug思路吧。毕竟对于这些小玩意,你所需要的只是一个脑洞而已~