引言
  计算机软件作为人类智慧的结晶,帮助我们在这个日新月异的社会中完成了大量工作。我们的日常生活中已经离不开软件,玲琅满目的软件已经渗透到了我们生活的各个角落,令我们目不暇接。我们都希望软件变得更好,运行处理的速度更快,在当今硬件性能突飞猛进的变革中,软件性能的提升也是一个永不落伍的话题。软件性能测试的实质,是从哲学的角度看问题,找出其内在联系,因果关系,形式内容关系,重叠关系等等。假如这些关系我们在分析过程中理清了,那么性能测试问题会变得迎刃而解。
  在软件开发过程中,性能测试往往在开发前期容易被忽略。直到有问题暴露后,开发人员被迫的直面这个问题,大多数情况下,这是令开发人员感觉到非常痛苦事情。所以在软件开发前期以及开发过程中性能测试的考量是必要的,那么具备相应理论知识和实践方法也是一个工程师所应当具备的素养,这里我们概括有四项原则,这些原则可以帮助开发人员丰富、充实测试理论,系统的开展性能测试工作,从而获得更有价值的结果。
  实际项目中的性能测试才有意义
  第一个原则是性能测试只有在实际项目中实施才是有意义的,这样才使得测试工作具有针对性,而且目标会更加明确。这个原则中有三个类别的基准可以指导开发人员度量性能测试的结果,但是每一种方法都有它的优点和劣势,我们将结合实际例子,来总结阐述。
  微观基准,可以理解为在某一个方法或某一个组件中进行的单元性能测试。比如检测一个线程同步和一个非线程同步的方法运行时所需要的时间。或者对比创建一个单独线程和使用一个线程池的性能开销。或者对比执行一个算法中的某一个迭代过程所需要的时间。当我们遇到这些情况时,我们常常会选择做一个方法层面的性能测试。这些情况的性能测试,都可以尝试使用微观基准的方法进行性能测试。微观基准看似编写起来简单快捷,但是编写能够准确反映性能问题的代码并非一件易事。接下来通过例子让我们从代码中发现一些问题。这是一个单线程的程序片段,通过计算 50 次循环迭代来检测执行方法所耗费的时间体现性能差异:
public void doTest() {
double l;
long then = System.currentTimeMillis();
int nLoops = 50;
for (int i = 0; i < nLoops; i++) {
l = compute(50);
}
long now = System.currentTimeMillis();
System.out.println("Elapsed time:" + (now - then));
}
private double compute(int n){
if (n < 0)
throw new IllegalArgumentException("Must be > 0");
if (n == 0)
return 0d;
if (n == 1)
return 1d;
double d = compute(n - 2) + compute(n - 1);
if (Double.isInfinite(d))
throw new ArithmeticException("Overflow");
return d;
}
  执行这段代码我们会发现一个问题,那是执行时间只有短短的几秒。难道果真是程序性能很高?答案并非如此,其实在整个执行过程中 compute 计算方法并没有调用而是被编译器自动忽略了。那么解决这个问题的办法是将 double 类型的“l”换成 volatile 实例变量。这样能够确保每一个计算后所得到的结果是可以被记录下来,用 volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的后的值。
  要特别值得注意的是,当考虑为多线程写一个微基准性能测试用例时,假如几个线程同时执行一小段业务逻辑代码,这可能会引发潜在的线程同步所带来的性能开销和瓶颈。此时微观微基准测试的结果往往引导开发人员为了保持同步进行不断的优化,这样会浪费很多时间,对于解决更紧迫的性能问题,这样做显得得不偿失。
  我们再试想这样一个例子,微基准测试两个线程调用同步方法的情况,因为基准代码很小,那么测试用例大部分时间将消耗在同步过程中。即使微基准测试在整体的同步过程中只占 50%,那么两个线程尝试执行同步方法的几率也是相当高的。基准运行将会非常缓慢,添加额外的线程会造成更大的性能问题。
  基于微观基准的测试过程中,是不能含有额外的对性能产生影响的操作,我们知道执行 compute(1000) 和 compute(1) 在性能上是有很大差异的,假如我们的目标是对比两个不同实现方法之间的性能差异,那么应当考虑一系列的输入测试值作为前提,传递给测试目标,参数需要多样化。这里以我们的经验解决的办法是使用随机值:
  for (int i = 0; i < nLoops; i++) {
  l = compute(random.nextInt());
  }
  现在,产生随机数的时间也包含在了整个循环执行过程中,因此测试结果中包含了随机数生成所需要的时间,这并不能客观的体现 compute 方法真实的性能。所以在构建微观基准时,输入的测试值必须是预先准备好的,且不会对性能测试产生额外的影响。正确的做法如下:
public void doTest() {
double l;
int nLoops = 10;
Random random = new Random();
int[] input = new int[nLoops];
for (int i = 0; i < nLoops; i++) {
input[i] = random.nextInt();
}
long then = System.currentTimeMillis();
for (int i = 0; i < nLoops; i++) {
try {
l = compute(input[i]);
} catch (IllegalArgumentException iae) {
}
}
long now = System.currentTimeMillis();
System.out.println("Elapsed time:" + (now - then));
}
  微观基准中输入的测试值必须是符合业务逻辑的。所有的输入的值并不一定会被代码用到,实际的业务可能对输入的数据有特定的要求,不合理的输入值可能导致代码在执行过程中抛出异常而中断,从而使得我们难以判断代码执行的效率。所以在准备测试数据的时候应当考虑到输入数据的有效性,保证代码执行的完整性。比如下面的例子输入的参数如果是大于 1476 ,执行会立即中断,从而影响了真实性能结果的产生。
  public double ImplSlow(int n) {
  if (n < 0) throw new IllegalArgumentException("Must be > 0");
  if (n > 1476) throw new ArithmeticException("Must be < 1476");
  return verySlowImpl(n);
  }