Java的System.out.println有多慢,你知道吗?

本文带你探索Java的System.out.println有多慢!

今天刷一道算法题提交后发现,运行时间Timeout,本以为是个在正常不过的现象,却让我发现了一个隐藏的”宝藏“。

我设计的算法时间复杂度是O(n)O(n),提交后竟然时间超时,难道还有比这更好的算法吗?通过查看已经通过的其他人的代码,发现其他人的设计思路和我差不多,有的甚至还不如我,但为什么他们能够通过,而我就不能呢?

经过仔细检查和对比后发现,这完全是由System.out.println造成的。

现在让我们进行试验,来探明其中的究竟。

实验一:打印5000条数据到控制台,输出其运行的时间

1
2
3
4
5
6
long startTime=System.currentTimeMillis();
for(int i=0;i<5000;i++){
System.out.print(i+" ");
}
long endTime=System.currentTimeMillis();
System.out.println("\n总共花费"+(endTime-startTime)+"ms");

输出:总共花费110ms

实验二:通过StringBuilder,一次性打印这5000条数据

1
2
3
4
5
6
7
8
long startTime=System.currentTimeMillis();
StringBuilder strBuf=new StringBuilder();
for(int i=0;i<5000;i++){
strBuf.append(i+" ");
}
System.out.print(strBuf.toString());
long endTime=System.currentTimeMillis();
System.out.println("\n总共花费"+(endTime-startTime)+"ms");

输出:总共花费11ms

这时候你可能会说:“纳尼?实现同样的效果,实验二竟然比实验一快11倍”。

是的,的确如此,现在你知道我的算法为什么那么慢了吧!

那么,为什么会出现这个现象呢?经过查阅资料后发现,System.out.println底层执行的是write系统调用接口。我们知道,系统调用会涉及到用户态和内核态的转换,用户进程需要把其缓冲区中的数据拷贝到内核态中才能执行系统调用,执行完毕后还要返回到用户态,这就会产生一定的时间开销。所以实验一相当于执行了5000次系统调用,而实验二仅仅执行了一次系统调用,故而产生上面的结果。

所以,要减少代码的执行时间,尽可能的减少System.out.println的执行次数是必要的,可通过实验二的方式输出需要的格式。

到了这里,你可能会想Java的Scanner的执行效率如何?它会不会产生系统调用,让我们再做两个实验!

实验三:依次输入5000个数据,输出其执行时间

1
2
3
4
5
6
7
8
9
Scanner sc=new Scanner(System.in);
int len=sc.nextInt();
//一定要从这里开始计时
long startTime=System.currentTimeMillis();
int[] arr=new int[len];
for(int i=0;i<len;i++)
arr[i]=sc.nextInt();
long endTime=System.currentTimeMillis();
System.out.println("\n总共花费"+(endTime-startTime)+"ms");

输入(限于篇幅,下面5000条没写完全):

1
2
5000
0 1 2 3 4 5 6 7 8 9 ... 4997 4998 4999

输出:总共花费1076ms

实验四:一次性读入5000条数据,输出其执行时间

1
2
3
4
5
6
7
8
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
int len = Integer.parseInt(bufferedReader.readLine());
long startTime=System.currentTimeMillis();
String[] readLine = bufferedReader.readLine().trim().split(" ");
int[] arr = Arrays.asList(readLine).stream().mapToInt(Integer::parseInt).toArray();
bufferedReader.close();
long endTime=System.currentTimeMillis();
System.out.println("\n总共花费"+(endTime-startTime)+"ms");

输入:同实验三。

输出:总共花费2419ms

你没有看错,一次性读入所有数据执行时间反而变得更长了。我认为,应该是实验四第五行的Arrays.asList函数导致其变慢,现在去掉实验四的第五行代码再次执行,输出**总共花费739ms**。这就符合我们的预期了,但其实也没有快多少是吧。

但是,一般情况下,如果你采用实验四的方式输入算法的测试用例,你很可能会用到Arrays.asList将输入的数据转化为int数组,这样才能进行后续的逻辑运算。所以,在实现同等效果的情况下,实验三整体上更优。

回到刚刚的问题,Java的Scanner会产生系统调用吗?答案是肯定会的,因为用户进程是无法直接操作硬件设备的,需要相应的系统调用间接操作。当你从键盘上输入数据后,用户进程会调用read这个系统调用接口,转入内核态从而读取键盘的数据。

所以,当你的算法运行超时后,不要一味地去优化算法本身的逻辑,可能换一种输出方式,算法的执行时间就缩短了N倍。

一般来说,按照一定的格式输出数据时,采用实验二的方式;输入数据时,仍然按照传统的实验三来输入,不要想着用实验四一次性读入所有的数据,除非你读取之后就是需要的格式,不用额外的转化。