解说性能之一:字符串运算VS数字运算
代码:
public class GoogleFn {
private static int MAX = 13200000;
private static int count1(int number) {
int result = 0;
String target = number + "";
int index = target.indexOf("1");
while (index >= 0) {
result++;
index = target.indexOf("1", index + 1);
}
return result;
}
private static int count2(int n) {
int count = 0;
while (n > 0) {
int mod = n % 10;
if (mod == 1)
count++;
n = n / 10;
}
return count;
}
private static void method1() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 1; i < MAX; i++) {
result += count1(i);
if (result == i) {
System.out.println("Find " + i + ", " + (System.currentTimeMillis() - start) + "ms");
}
}
}
private static void method2() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 1; i < MAX; i++) {
result += count2(i);
if (result == i) {
System.out.println("Find " + i + ", " + (System.currentTimeMillis() - start) + "ms");
}
}
}
public static void main(String[] args) {
method1();
method2();
}
}
运行结果是:
Find 1, 16ms
Find 199981, 219ms
Find 199982, 219ms
Find 199983, 219ms
Find 199984, 219ms
Find 199985, 219ms
Find 199986, 219ms
Find 199987, 219ms
Find 199988, 219ms
Find 199989, 219ms
Find 199990, 219ms
Find 200000, 219ms
Find 200001, 219ms
Find 1599981, 1516ms
Find 1599982, 1516ms
Find 1599983, 1516ms
Find 1599984, 1516ms
Find 1599985, 1516ms
Find 1599986, 1516ms
Find 1599987, 1516ms
Find 1599988, 1516ms
Find 1599989, 1516ms
Find 1599990, 1516ms
Find 2600000, 2469ms
Find 2600001, 2469ms
Find 13199998, 12594ms
Find 1, 0ms
Find 199981, 47ms
Find 199982, 47ms
Find 199983, 47ms
Find 199984, 47ms
Find 199985, 47ms
Find 199986, 47ms
Find 199987, 47ms
Find 199988, 47ms
Find 199989, 47ms
Find 199990, 47ms
Find 200000, 47ms
Find 200001, 47ms
Find 1599981, 453ms
Find 1599982, 453ms
Find 1599983, 453ms
Find 1599984, 453ms
Find 1599985, 453ms
Find 1599986, 453ms
Find 1599987, 453ms
Find 1599988, 453ms
Find 1599989, 453ms
Find 1599990, 453ms
Find 2600000, 765ms
Find 2600001, 765ms
Find 13199998, 4187ms
我们以最后一个找到的数字为例,前者的时间是后者的3倍,代码的其它部分完全一样,区别就是前者是转换为字符串查找1的个数,后者使用数学的取模运算计算。
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之二:分析问题
前面我们已经说了字符串运算和数学运算对性能的巨大影响,接下来我们看看分析程序,多思考给我们带来的好处。
如果我们做一个简单的分析就可以知道,在尾数从0到9的连续十个数字中,只有尾数为1的数字的1的个数比其它的数字多,那么我们可以以10个数为单位进行分隔,计算尾数为0的数字包含1的个数,其它的9个值就以此为基础计算:
public class GoogleFn {
private static int MAX = 13200000;
private static int MAX2 = MAX / 10;
private static int count(int n) {
int count = 0;
while (n > 0) {
int mod = n % 10;
if (mod == 1)
count++;
n = n / 10;
}
return count;
}
private static void method1() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 1; i < MAX; i++) {
result += count(i);
if (result == i) {
System.out.println("Find " + i + ", " + (System.currentTimeMillis() - start) + "ms");
}
}
}
private static void method2() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < MAX2; i++) {
int number = i * 10;
int value = count(number);
for (int j = 0; j < 10; j++) {
result += value;
if (j == 1) {
result++;
}
int x = number + j;
if (x != 0 && result == x) {
System.out.println("Find " + x + ", " + (System.currentTimeMillis() - start) + "ms");
}
}
}
}
public static void main(String[] args) {
method1();
method2();
}
}
运算结果:
Find 1, 0ms
Find 199981, 47ms
Find 199982, 47ms
Find 199983, 47ms
Find 199984, 47ms
Find 199985, 47ms
Find 199986, 47ms
Find 199987, 47ms
Find 199988, 47ms
Find 199989, 47ms
Find 199990, 47ms
Find 200000, 47ms
Find 200001, 47ms
Find 1599981, 454ms
Find 1599982, 454ms
Find 1599983, 454ms
Find 1599984, 454ms
Find 1599985, 454ms
Find 1599986, 454ms
Find 1599987, 454ms
Find 1599988, 454ms
Find 1599989, 454ms
Find 1599990, 454ms
Find 2600000, 766ms
Find 2600001, 766ms
Find 13199998, 4204ms
Find 1, 0ms
Find 199981, 0ms
Find 199982, 0ms
Find 199983, 0ms
Find 199984, 0ms
Find 199985, 0ms
Find 199986, 0ms
Find 199987, 0ms
Find 199988, 0ms
Find 199989, 0ms
Find 199990, 0ms
Find 200000, 0ms
Find 200001, 0ms
Find 1599981, 62ms
Find 1599982, 62ms
Find 1599983, 62ms
Find 1599984, 62ms
Find 1599985, 62ms
Find 1599986, 62ms
Find 1599987, 62ms
Find 1599988, 62ms
Find 1599989, 62ms
Find 1599990, 62ms
Find 2600000, 93ms
Find 2600001, 93ms
Find 13199998, 515ms
同样以1319998为例,前者花费的时间是后者的8倍以上,因为每10个数,我们只需要计算一次1的个数,所以差不多只需要1/10的时间,但是后者的逻辑比较复杂一些,多了一些中间变量的使用,并且多了一个判断条件。
如果你不具备深厚的数学功底,对于算法并不精通,有时候对问题进行简单分析就可能大幅度的提高性能。
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之三:不要小看循环中的任何一个语句
对于任何语言来讲,循环永远是非分布式系统的性能的最大杀手,循环中的任何一个简单的语句对性能都是有影响的,只是影响的大小不同而已。第一个例子中的影响是比较大的,不同的实现方法的时间开销不同,然后这个微小的差异被循环次数放大后就非常的明显(3倍),而第二个例子,其本质是减少了循环执行的次数,虽然总的循环次数是一样的,但是最耗时的操作的执行次数被减少到1/10,所以产生的差异是非常巨大的(8倍)。我们再来看一个很不起眼的微小差异带来的影响:
public class GoogleFn {
private static int MAX = 132000000;
private static int MAX2 = MAX / 10;
private static int count(int n) {
int count = 0;
while (n > 0) {
int mod = n % 10;
if (mod == 1)
count++;
n = n / 10;
}
return count;
}
private static void method1() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < MAX2; i++) {
int number = i * 10;
int value = count(number);
for (int j = 0; j < 10; j++) {
result += value;
if (j == 1) {
result++;
}
int x = number + j;
if (x != 0 && result == x) {
print(x, start);
}
}
}
}
private static void method2() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < MAX2; i++) {
int number = i * 10;
int value = count(number);
for (int j = 0; j < 10; j++) {
result += value;
if (j == 1) {
result++;
}
int x = number + j;
if (result == x && x != 0) { //
print(x, start);
}
}
}
}
private static void print(int i, long start) {
System.out.println("Find " + i + ", " + (System.currentTimeMillis() - start) + "ms");
}
public static void main(String[] args) {
method1();
method2();
}
}
运行结果:
Find 1, 0ms
Find 199981, 16ms
Find 199982, 16ms
Find 199983, 16ms
Find 199984, 16ms
Find 199985, 16ms
Find 199986, 16ms
Find 199987, 16ms
Find 199988, 16ms
Find 199989, 16ms
Find 199990, 16ms
Find 200000, 16ms
Find 200001, 16ms
Find 1599981, 63ms
Find 1599982, 63ms
Find 1599983, 63ms
Find 1599984, 63ms
Find 1599985, 63ms
Find 1599986, 63ms
Find 1599987, 78ms
Find 1599988, 78ms
Find 1599989, 78ms
Find 1599990, 78ms
Find 2600000, 110ms
Find 2600001, 110ms
Find 13199998, 531ms
Find 35000000, 1453ms
Find 35000001, 1453ms
Find 35199981, 1453ms
Find 35199982, 1453ms
Find 35199983, 1453ms
Find 35199984, 1453ms
Find 35199985, 1453ms
Find 35199986, 1453ms
Find 35199987, 1453ms
Find 35199988, 1453ms
Find 35199989, 1453ms
Find 35199990, 1453ms
Find 35200000, 1453ms
Find 35200001, 1453ms
Find 117463825, 5000ms
Find 1, 0ms
Find 199981, 16ms
Find 199982, 16ms
Find 199983, 16ms
Find 199984, 16ms
Find 199985, 16ms
Find 199986, 16ms
Find 199987, 16ms
Find 199988, 16ms
Find 199989, 16ms
Find 199990, 16ms
Find 200000, 16ms
Find 200001, 16ms
Find 1599981, 63ms
Find 1599982, 63ms
Find 1599983, 63ms
Find 1599984, 63ms
Find 1599985, 63ms
Find 1599986, 63ms
Find 1599987, 78ms
Find 1599988, 78ms
Find 1599989, 78ms
Find 1599990, 78ms
Find 2600000, 109ms
Find 2600001, 109ms
Find 13199998, 516ms
Find 35000000, 1438ms
Find 35000001, 1438ms
Find 35199981, 1438ms
Find 35199982, 1438ms
Find 35199983, 1438ms
Find 35199984, 1438ms
Find 35199985, 1438ms
Find 35199986, 1438ms
Find 35199987, 1438ms
Find 35199988, 1438ms
Find 35199989, 1438ms
Find 35199990, 1438ms
Find 35200000, 1438ms
Find 35200001, 1438ms
Find 117463825, 4938ms
注意我们的MAX值比以前的例子放大了10倍,因为这个例子的差异比较小,值太小看不出差异,注意到两个方法的不同了吗?对,仅仅是if里面的条件换了个位置:
x != 0 && result == x -》 result == x && x != 0
你还可以稍稍修改下代码,把后面的那个的&& x != 0去掉,你会发现它的结果和这个结果很类似。
这个原理就是&&运算符的短路原理,如果前面的条件不为true,那么后面的计算不被执行。
所以循环中的任何代码都要比较小心,即使一点细小的不同都会在循环的放大作用下产生一些不好的结果。
PS: 条件判断中有多个并列条件时,前面放的应该是最少满足的条件,这样可以更好的过滤。
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之四:优化无止境
其实在例子二的基础上,我们进一步的分析,可以把缓存10个结果换成缓存100个结果,性能可以得到进一步提升:
public class GoogleFn {
private static int MAX = 13200000;
private static int MAX2 = MAX / 10;
private static int MAX3 = MAX2 / 10;
private static int count(int n) {
int count = 0;
while (n > 0) {
int mod = n % 10;
if (mod == 1)
count++;
n = n / 10;
}
return count;
}
private static void method1() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < MAX2; i++) {
int number = i * 10;
int value = count(number);
for (int j = 0; j < 10; j++) {
result += value;
if (j == 1) {
result++;
}
int x = number + j;
if (result == x && x != 0) {
print(x, start);
}
}
}
}
private static void method2() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < MAX3; i++) {
int number = i * 100;
int value = count(number);
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
int x = number + j * 10 + k;
result += value;
if (j == 1) {
result++;
}
if (k == 1) {
result++;
}
if (result == x && x != 0) {
print(x, start);
}
}
}
}
}
private static void print(int n, long start) {
System.out.println("Find " + n + ", "
+ (System.currentTimeMillis() - start) + "ms");
}
public static void main(String[] args) {
method1();
method2();
}
}
运行结果:
Find 1, 0ms
Find 199981, 16ms
Find 199982, 16ms
Find 199983, 16ms
Find 199984, 16ms
Find 199985, 16ms
Find 199986, 16ms
Find 199987, 16ms
Find 199988, 16ms
Find 199989, 16ms
Find 199990, 16ms
Find 200000, 16ms
Find 200001, 16ms
Find 1599981, 78ms
Find 1599982, 78ms
Find 1599983, 78ms
Find 1599984, 78ms
Find 1599985, 78ms
Find 1599986, 78ms
Find 1599987, 78ms
Find 1599988, 78ms
Find 1599989, 78ms
Find 1599990, 78ms
Find 2600000, 125ms
Find 2600001, 125ms
Find 13199998, 625ms
Find 1, 0ms
Find 199981, 16ms
Find 199982, 16ms
Find 199983, 16ms
Find 199984, 16ms
Find 199985, 16ms
Find 199986, 16ms
Find 199987, 16ms
Find 199988, 16ms
Find 199989, 16ms
Find 199990, 16ms
Find 200000, 16ms
Find 200001, 16ms
Find 1599981, 31ms
Find 1599982, 31ms
Find 1599983, 31ms
Find 1599984, 31ms
Find 1599985, 31ms
Find 1599986, 31ms
Find 1599987, 31ms
Find 1599988, 31ms
Find 1599989, 31ms
Find 1599990, 31ms
Find 2600000, 47ms
Find 2600001, 47ms
Find 13199998, 219ms
可以看出,缓存100个结果比缓存10个结果又快了近3倍,这个时候我们可能会想,那么我缓存1000个结果呢,很遗憾,按照这个方法缓存1000个的结果和缓存100个结果无异。当然,肯定还有其他的更优解。
<script language=JavaScript>
</script>
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之五:人比电脑聪明
在例子四的基础上,我们可以进行更加深入的分析,我们还是以100为例,我们其实在大部分情况下可以省略循环,如果数字的百位数以上包含1的个数为0,而十位数不为1,那么当个位数大于1以后,我们可以中断底层的循环,这样我们又节省了很多的运算:
public class GoogleFn {
private static int MAX = 1320000000;
private static int MAX2 = MAX / 10;
private static int MAX3 = MAX2 / 10;
private static int count(int n) {
int count = 0;
while (n > 0) {
int mod = n % 10;
if (mod == 1)
count++;
n = n / 10;
}
return count;
}
private static void method1() {
long start = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < MAX3; i++) {
int number = i * 100;
int value = count(number);
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
int x = number + j * 10 + k;
result += value;
if (j == 1) {
result++;
}
if (k == 1) {
result++;
}
if (result == x && x != 0) {
print(x, start);
}
}
}
}
}
private static void method2() {
long start = System.currentTimeMillis();
int result = 20;
for (int i = 1; i < MAX3; i++) {
int number = i * 100;
int value = count(i);
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
if (value == 0 && j != 1 && k > 1 && result < number) {
break;
}
int x = number + j * 10 + k;
result += value;
if (j == 1) {
result++;
}
if (k == 1) {
result++;
}
if (result == x && x != 0) {
print(x, start);
continue;
}
}
}
}
}
private static void print(int n, long start) {
System.out.println("Find " + n + ", "
+ (System.currentTimeMillis() - start) + "ms");
}
public static void main(String[] args) {
method1();
method2();
}
}
运行结果:
Find 1, 0ms
Find 199981, 16ms
Find 199982, 16ms
Find 199983, 16ms
Find 199984, 16ms
Find 199985, 16ms
Find 199986, 16ms
Find 199987, 16ms
Find 199988, 16ms
Find 199989, 16ms
Find 199990, 16ms
Find 200000, 16ms
Find 200001, 16ms
Find 1599981, 32ms
Find 1599982, 32ms
Find 1599983, 32ms
Find 1599984, 32ms
Find 1599985, 32ms
Find 1599986, 32ms
Find 1599987, 32ms
Find 1599988, 32ms
Find 1599989, 32ms
Find 1599990, 32ms
Find 2600000, 47ms
Find 2600001, 47ms
Find 13199998, 204ms
Find 35000000, 532ms
Find 35000001, 579ms
Find 35199981, 594ms
Find 35199982, 594ms
Find 35199983, 594ms
Find 35199984, 594ms
Find 35199985, 594ms
Find 35199986, 594ms
Find 35199987, 594ms
Find 35199988, 594ms
Find 35199989, 594ms
Find 35199990, 594ms
Find 35200000, 594ms
Find 35200001, 594ms
Find 117463825, 1860ms
Find 500000000, 8079ms
Find 500000001, 8079ms
Find 500199981, 8141ms
Find 500199982, 8141ms
Find 500199983, 8141ms
Find 500199984, 8141ms
Find 500199985, 8141ms
Find 500199986, 8141ms
Find 500199987, 8141ms
Find 500199988, 8141ms
Find 500199989, 8141ms
Find 500199990, 8141ms
Find 500200000, 8141ms
Find 500200001, 8141ms
Find 501599981, 8157ms
Find 501599982, 8157ms
Find 501599983, 8157ms
Find 501599984, 8157ms
Find 501599985, 8157ms
Find 501599986, 8157ms
Find 501599987, 8157ms
Find 501599988, 8157ms
Find 501599989, 8157ms
Find 501599990, 8157ms
Find 502600000, 8188ms
Find 502600001, 8188ms
Find 513199998, 8485ms
Find 535000000, 8844ms
Find 535000001, 8844ms
Find 535199981, 8844ms
Find 535199982, 8844ms
Find 535199983, 8844ms
Find 535199984, 8844ms
Find 535199985, 8844ms
Find 535199986, 8844ms
Find 535199987, 8844ms
Find 535199988, 8844ms
Find 535199989, 8844ms
Find 535199990, 8844ms
Find 535200000, 8844ms
Find 535200001, 8844ms
Find 1111111110, 18172ms
Find 199981, 16ms
Find 199982, 16ms
Find 199983, 16ms
Find 199984, 16ms
Find 199985, 16ms
Find 199986, 16ms
Find 199987, 16ms
Find 199988, 16ms
Find 199989, 16ms
Find 199990, 16ms
Find 200000, 16ms
Find 200001, 16ms
Find 1599981, 31ms
Find 1599982, 31ms
Find 1599983, 31ms
Find 1599984, 31ms
Find 1599985, 31ms
Find 1599986, 31ms
Find 1599987, 31ms
Find 1599988, 31ms
Find 1599989, 31ms
Find 1599990, 31ms
Find 2600000, 47ms
Find 2600001, 47ms
Find 13199998, 188ms
Find 35000000, 453ms
Find 35000001, 453ms
Find 35199981, 453ms
Find 35199982, 453ms
Find 35199983, 453ms
Find 35199984, 453ms
Find 35199985, 453ms
Find 35199986, 453ms
Find 35199987, 453ms
Find 35199988, 453ms
Find 35199989, 453ms
Find 35199990, 453ms
Find 35200000, 453ms
Find 35200001, 453ms
Find 117463825, 1406ms
Find 500000000, 6438ms
Find 500000001, 6438ms
Find 500199981, 6438ms
Find 500199982, 6438ms
Find 500199983, 6438ms
Find 500199984, 6438ms
Find 500199985, 6438ms
Find 500199986, 6438ms
Find 500199987, 6438ms
Find 500199988, 6438ms
Find 500199989, 6438ms
Find 500199990, 6438ms
Find 500200000, 6438ms
Find 500200001, 6438ms
Find 501599981, 6453ms
Find 501599982, 6453ms
Find 501599983, 6453ms
Find 501599984, 6453ms
Find 501599985, 6453ms
Find 501599986, 6453ms
Find 501599987, 6453ms
Find 501599988, 6453ms
Find 501599989, 6453ms
Find 501599990, 6453ms
Find 502600000, 6485ms
Find 502600001, 6485ms
Find 513199998, 6688ms
Find 535000000, 7031ms
Find 535000001, 7031ms
Find 535199981, 7031ms
Find 535199982, 7031ms
Find 535199983, 7031ms
Find 535199984, 7031ms
Find 535199985, 7031ms
Find 535199986, 7031ms
Find 535199987, 7031ms
Find 535199988, 7031ms
Find 535199989, 7031ms
Find 535199990, 7031ms
Find 535200000, 7031ms
Find 535200001, 7031ms
Find 1111111110, 14250ms
注意我们把MAX放大了100倍,这样才能看出差别,否则运算时间太短,就看不出来差异了。虽然这样做只提高了大约20%的性能,但是如果这个提升是发生在占用系统最多时间的部分,其累加效应将是惊人的。
<script language=JavaScript>
</script>
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之六:数学显神威
其实很多问题一旦涉及到数学问题或者数据处理密集型问题,那么最终显现神威的就是数学公式,这个面试题也是这类问题,所以如果我们能够推导出一个数学公式就是最理想的,在前面的例子中,我们进行了一些深入的分析,根据前面的例子,你可能会尝试把步长从100扩展到1000或者10000,但是实际上这个方法遇到了瓶颈,因为循环嵌套的层次太多,计算公式太复杂也会导致问题。如果我们最开始尝试的时候把全部的f(n)的结果打印出来,你会发现这样的内容:
- f(9) = 1
- f(99) = 20
- f(999) = 300
- f(9999) = 4000
- ……
这个是我们的第一个规律:位数乘以((位数-1)的10的次方)。
根据这个f(n)的说明,我们定义另外一个方法x(n),它的定义就是n这个数包含的1的个数,例如
x(1)=1,x(2)=0,x(11)=2,那么我们可以把f(n)展开为:
f(n)=x(0)+x(1)+……+x(n)
同时我们可以把x(n)也展开,假设n=XYZ,那么x(n)的展开式为:
x(n)= x(X)+x(Y)+x(Z)
也等于:
x(n)= x(X)+x(YZ)
再结合上面的规律我们就可以推导出一个规律了,先用例子来说明,以106为例:
f(106) = x(0)+…+x(99)+x(100)+…+x(106) = f(99) + x(100)+…+x(106)
f(99)我们使用上面的第一个规律很容易计算得到,那么后面的这7个数包含多少个1呢,其实也很简单,应用可能小学就学过的公因子概念,当然这里不是真正的公因子,而是这些数里面包含的1的个数相同的部分,结合x(n)的展开式,我们进一步推演出:
f(106) = f(99) + x(1)+ x(00)+x(1)+x(01)+…+x(1)+x(06) = f(99) + x(1) * (6+1) + x(00) + .. x(06) = f(99) + x(1) * (6+1) + f(6)
这样计算就很简单了,不是吗?
好,再看看f(345)的情况,有点不太一样:
f(345) = x(0)+…x(99)+x(100)+…+x(199)+x(200)+..+x(299)+x(300)+…+x(345)= f(99) + x(1) * (99+1) + f(99)+ x(2)*(99+1)+f(99)+x(3)*(45+1)+f(45)
这个例子足够典型了吗?看到规律了吗?
给定一个数n,假设最高位为x,除去最高位的数字为y,位数为z,那么
如果x=1,那么f(n)等于f(pow(10,z-1)-1)+(y+1)+f(y)
如果x>1,那么f(n)等于f(pow(10,z-1)-1)*x+pow(10,z-1)+f(y)
转换为代码就是:
private static int fn(int number) {
if (number < 10) {
return number > 0 ? 1 : 0;
}
String s = number + "";
int length = s.length();
int end = Integer.parseInt(s.substring(1, length));
int x = s.charAt(0) - ‘0′;
int result = 0;
if (x == 1) {
result = (length - 1) * (int) Math.pow(10, length - 1 - 1) + fn(end)
+ (end + 1);
} else {
result = (length - 1) * (int) Math.pow(10, length - 1 - 1) * x
+ (int) Math.pow(10, length - 1) + fn(end);
}
return result;
}
你可以运行试试这个公式是否准确。
最后需要强调一下的是,这个方法可以快速的计算给定的一个数的f(n)的结果,但是如果用一个简单的循环来查找符合f(n)=n的结果是不合适的,这个我会另外再谈的。
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之七:缓存中间结果
上次已经说了fn的实现不能用来查找符合条件的n,因为这样做比前面的第一个例子中的性能比较差的那个还要差,原因就是有太多的重复计算,如果只是计算一个指定的数的结果,那么那个实现是无与匹敌的。但是我们是讲的性能优化,所以,我们就用它来做,放慢速度,然后使用其它的技巧来提高性能,这次的方法就是简单的使用缓存:
public class GoogleFn {
private static final int MAX = 2600002;
private static long start = System.currentTimeMillis();
private static int[] bases = new int[15];
private static int[] values = new int[15];
private static int fn(int number) {
if (number < 10) {
return number > 0 ? 1 : 0;
}
String s = number + "";
int length = s.length();
int end = Integer.parseInt(s.substring(1, length));
int x = s.charAt(0) - ‘0′;
int result = 0;
if (x == 1) {
result = values[length - 1] + fn(end) + (end + 1);
} else {
result = values[length - 1] * x + bases[length - 1] + fn(end);
}
return result;
}
private static int fnOld(int number) {
if (number < 10) {
return number > 0 ? 1 : 0;
}
String s = number + "";
int length = s.length();
int end = Integer.parseInt(s.substring(1, length));
int x = s.charAt(0) - ‘0′;
int result = 0;
if (x == 1) {
result = (length - 1) * (int) Math.pow(10, length - 1 - 1) + fnOld(end) + (end + 1);
} else {
result = (length - 1) * (int) Math.pow(10, length - 1 - 1) * x + (int) Math.pow(10, length - 1) + fnOld(end);
}
return result;
}
private static void print(int n) {
System.out.println("Find " + n + ", " + (System.currentTimeMillis() - start) + "ms");
}
public static void main(String[] args) {
for (int i = 1; i < MAX; i++) {
if (i == fnOld(i)) {
print(i);
}
}
bases[0] = 0;
bases[1] = 10;
values[0] = 0;
values[1] = 1;
for (int i = 2; i < values.length; i++) {
bases[i] = (int) Math.pow(10, i);
values[i] = i * (int) Math.pow(10, i - 1);
}
start = System.currentTimeMillis();
for (int i = 1; i < MAX; i++) {
if (i == fn(i)) {
print(i);
}
}
}
}
运行结果:
Find 1, 0ms
Find 199981, 1672ms
Find 199982, 1672ms
Find 199983, 1672ms
Find 199984, 1672ms
Find 199985, 1672ms
Find 199986, 1672ms
Find 199987, 1672ms
Find 199988, 1672ms
Find 199989, 1672ms
Find 199990, 1672ms
Find 200000, 1672ms
Find 200001, 1672ms
Find 1599981, 17032ms
Find 1599982, 17032ms
Find 1599983, 17032ms
Find 1599984, 17032ms
Find 1599985, 17032ms
Find 1599986, 17032ms
Find 1599987, 17032ms
Find 1599988, 17032ms
Find 1599989, 17032ms
Find 1599990, 17032ms
Find 2600000, 29875ms
Find 2600001, 29875ms
Find 1, 0ms
Find 199981, 1000ms
Find 199982, 1000ms
Find 199983, 1000ms
Find 199984, 1000ms
Find 199985, 1000ms
Find 199986, 1000ms
Find 199987, 1000ms
Find 199988, 1000ms
Find 199989, 1000ms
Find 199990, 1000ms
Find 200000, 1000ms
Find 200001, 1000ms
Find 1599981, 8563ms
Find 1599982, 8563ms
Find 1599983, 8563ms
Find 1599984, 8563ms
Find 1599985, 8563ms
Find 1599986, 8563ms
Find 1599987, 8563ms
Find 1599988, 8563ms
Find 1599989, 8563ms
Find 1599990, 8563ms
Find 2600000, 14594ms
Find 2600001, 14594ms
可以看到,我们仅仅是缓存了几个简单的中间结果,就将性能提升了一倍。另外请和第一个例子的结果对比一下,我们优化后的结果也比那个差的实现的结果大5倍。
-------------------------------------------------------------------------------------------------------------------------------------
解说性能之八:工欲善其事必先利其器
这次我们按照总结篇中提到的方法实际演示下代码覆盖工具如何帮助我们优化程序提高性能,先给出我们未经好好优化的程序:
package com.jiehoo.util;
public class GoogleFn {
private static final int MAX = 2600002;
private static long start = System.currentTimeMillis();
private static int[] bases = new int[15];
private static int[] values = new int[15];
static {
bases[0] = 0;
bases[1] = 10;
values[0] = 0;
values[1] = 1;
for (int i = 2; i < values.length; i++) {
bases[i] = (int) Math.pow(10, i);
values[i] = i * (int) Math.pow(10, i - 1);
}
}
private static int fn(int number) {
if (number < 10) {
return number > 0 ? 1 : 0;
}
String s = number + "";
int length = s.length();
int end = Integer.parseInt(s.substring(1, length));
int x = s.charAt(0) - ‘0′;
int result = 0;
if (x == 1) {
result = values[length - 1] + fn(end) + (end + 1);
} else {
result = values[length - 1] * x + bases[length - 1] + fn(end);
}
return result;
}
private static void print(int n) {
System.out.println("Find " + n + ", " + (System.currentTimeMillis() - start) + "ms");
}
public static void findMatch() {
for (int i = 1; i < MAX; i++) {
int result = fn(i);
if (result == i) {
print(i);
}
}
}
}
使用单元测试工具和代码覆盖工具运行一下,得到代码覆盖报告:
从这个报告,我们看到fn方法的哪些部分被执行的次数最多,findMatch方法中直接调用fn的次数是2600001次,fn方法实际调用的次数是15860005,可以看到fn被自身递归调用了很多次(大致是数字的位数减一),最开始的那个判断是否小于10的部分我们不太可能优化,而那个取得数字的长度,尾数以及最高位的数字的调用执行的次数很多,是否可以优化呢?答案是肯定的,在单次执行的时候,那个字符串操作的速度的影响可以忽略不计,但是一旦是千万级以上,就很可观了,我们修改一下代码:
private static int fn(int number) {
if (number < 10) {
return number > 0 ? 1 : 0;
}
int length = 1;
int x = number / bases[length];
while (x > 0) {
length++;
x = number / bases[length];
}
x = number / bases[length - 1];
int end = number % bases[length - 1];
int result = 0;
if (x == 1) {
result = values[length - 1] + fn(end) + (end + 1);
} else {
result = values[length - 1] * x + bases[length - 1] + fn(end);
}
return result;
}
这个其实就是我们第一个例子说到的,如果可以使用数字运算,那么它往往比字符串运算要快(但是字符串运算有时候更简单直观)。
修改后运行的最后一个输出:
Find 2600001, 1953ms
而修改前的是:
Find 2600001, 14859ms
这个就可以看出这个差异的巨大了,相差了7倍多。
使用好的工具往往可以极大的帮助我们解决问题,找到问题的症结所在。
------------------------------------------------------------------------------------------------------------------------------------
解说性能之总结
呵呵,说了这么多,到底怎么优化性能还是没有说多少,而且一个产品的代码比这个例子复杂得多,怎么才能优化产品代码呢?
很简单,找到性能瓶颈,而大部分的性能瓶颈都有一个特点:被执行的次数太多。一个耗时2分钟的操作,如果系统运行一天才需要运行一次,那么我们根本就不要去理会它,如果一个操作耗时2秒,但是一般运行一天它要被执行几千亿次,那么你就要小心了。
如何才能知道系统中的哪些代码被执行的次数最多呢?有很多工具可以,有的是挂到系统上一起运行,有的是可以单独运行,但是我推荐的方法就是使用单元测试工具和代码覆盖工具,运行所有的单元测试,查看代码覆盖报告中被执行的次数最多的那些语句,看看他们是否可以被优化,或者可以被减少执行的次数。
可以参考我以前的一些日志:
Ant+JUnit+Cobertura :http://www.jiehoo.com/ant-junit-cobertura.htm
成功提高20倍性能:http://www.jiehoo.com/improve-performance-twenty-times.htm
很多情况下,找到性能的瓶颈并不是很困难,真正困难的是如何进行优化。这个没有通用的解决方法,只能结合具体的问题具体解决,一个大部分情况下有效的方法是使用某种缓存机制(实际上,我的第二个例子也是使用了缓存机制,把运算结果缓存了9次)。
<script language=JavaScript> </script>
<script language=JavaScript> </script> <script language=JavaScript> </script>
<script language=JavaScript> </script>