Python和Java代码的一个简单比较

标签:Java, Python, 性能

我想概述什么的大家都看烦了,所以我就直接以代码来说明了。

这个例子是从一个UTF-8编码的文本文件里读取所有字符,转换成Shift-JIS编码,再将每个字节与0xAB异或,最后写入另一个文件。可以算是破解日文游戏经常需要做的事,尚据一定代表性吧~

测试平台:
CPU:Intel Core2 Duo T9400 @ 2.53GHz
内存:3GB
操作系统:Windows XP Pro SP2 英文版
Python SDK:2.5.4
Java SDK:1.6.0_13

测试的文本是从《家族计划》里选的最大的一个,大小为781 KB (800,053 bytes)。(不要和我扯为什么不弄几百兆的来测试,我不是来翻译百科全书的。)

先上Python的代码:
from __future__ import with_statement
import codecs
from cStringIO import StringIO
from time import time


t = time()

with codecs.open(r'B:\test.txt', 'r', 'utf8') as inFile:
  inFile.seek(3) # skip BOM
  content = inFile.read()
  content = content.encode('sjis')

  outBuf = StringIO()
  for c in content:
    outBuf.write(chr(ord(c) ^ 0xAB))

  with open(r'B:\test2.txt', 'wb') as outFile:
    outFile.write(outBuf.getvalue())

print time() - t
测试结果:约550毫秒(波动较大)。

接着是Java:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;


public final class Hello {

	public static void main(String[] args) throws Exception {
		long t = System.currentTimeMillis();

		Reader inFile = new BufferedReader(new InputStreamReader(new FileInputStream("B:\\test.txt"), "UTF-8"));
		inFile.skip(1); // skip BOM

		int size = 1 << 20; // 1M cache
		CharBuffer inBuf = CharBuffer.allocate(size);
		inFile.read(inBuf);
		inFile.close();
		inBuf.position(0);

		ByteBuffer outBuf = Charset.forName("shift-jis").newEncoder().encode(inBuf);
		byte[] outBytes = outBuf.array();

		for (int i = 0, length = outBytes.length; i < length; ++i) {
			if (outBytes[i] == 0) {
				size = i;
				break;
			}
			outBytes[i] ^= 0xAB;
		}

		FileOutputStream outFile = new FileOutputStream("B:\\test2.txt");
		outFile.write(outBytes, 0, size);
		outFile.close();

		System.out.println(System.currentTimeMillis() - t);
	}

}
测试结果:约62毫秒。

性能约是Python的9倍,非常值得Java程序员骄傲了。这点也和通常的看法一样,静态语言一般是比动态语言快1个数量级的。

但性能背后却留下了很多问题,我仍不得不提。

首先显而易见的是代码量,Java足足比Python多了一倍。而且初看上去,Python的核心代码不到10行,逻辑一目了然;Java却多了很多稀奇古怪的东西,语句也特别长。

其次是写代码所用时间。我对API算不上熟悉,实际上里面几乎每行代码我都查看了文档或Google了一下,Python顶多用了半小时,Java用了2个多小时。

接着是代码的健壮性。为了减少代码量,在Java代码里,我设置了1M的固定缓冲区,超过的话这个程序就是不能正常工作的;我也没去捕捉IO异常,这可能导致文件不会正常关闭;还有一点下面会说到,我不知道怎么解决。而在Python里,我不需要多余的代码去做这些事(除了引入with语句)。

最后看看API的易用性。
Java读写文件的组合实在太多了,看得我头疼。为了能读取UTF-8,加上文件很小,所以我并未使用nio。当然,这段代码是从网上抄来的。
接着将文件内容读入了CharBuffer中,再编码到一个ByteBuffer里,然后生成一个字节数组,再写入文件。这里又费了我不少时间。
但是运行发现出错了,生成了一堆空白的文件,调试了半天才知道得把输入缓冲的position设为0。
设完后终于出内容了,但大小却不对劲,老是生成1MB的文件。于是接着调试,发现是CharBuffer的容量设为1MB了,于是没从文件中读取到的就都是'\0'了;而它又直接全部传给了ByteBuffer,继而生成了后面全是0的字节数组。我试了很多办法,例如截取一个子序列,但没想到子序列居然共用同一个数组…
看到这个问题没法解决,我就只好在数组上下工夫了。可惜我那种方式读取文件只能知道UTF-8的字符数,和Shift-JIS的字节数是不成正比的,而且数组也不支持切片操作。
没办法只好遍历判断是不是0了,这种行为有个缺点,如果数组中间有0的话,后面就都被截断了,这也就是前面所述的第3点不健壮的地方;但好在文本文件不会这么搞,所以我的程序仍勉强凑合…
说实话写完就想骂人了,这缓冲区也太无语了吧…

当然,虽然累了大半夜,但Java高手肯定写得比我好,我也就懒得再琢磨Java代码了,转而看Python。
性能差的原因一下就能找到,因为就那么几行代码,读写文件是不可能去优化的,就只能优化循环中那句chr(ord(c) ^ 0xAB)了。
我把它改成c,时间立刻就减少到200多毫秒,也就是约有40%的时间用在这上面了。
很显然,将字符串转成数字,再转回字符串,这个操作非常别扭。Python不像C,没有内置的char类型,因此每次都构造一个字符串的开销是很大的,于是就想到了array。
稍微改了4行代码(包括import),优化版就诞生了:
from __future__ import with_statement
import codecs
#from cStringIO import StringIO
from array import array
from time import time


t = time()

with codecs.open(r'B:\test.txt', 'r', 'utf8') as inFile:
  inFile.seek(3) # skip BOM
  content = inFile.read()
  content = content.encode('sjis')

  #outBuf = StringIO()
  outBuf = array('B')
  for c in content:
    #outBuf.write(chr(ord(c) ^ 0xAB))
    outBuf.append(ord(c) ^ 0xAB)

  with open(r'B:\test2.txt', 'wb') as outFile:
    #outFile.write(outBuf.getvalue())
    outBuf.tofile(outFile)

print time() - t
虽然不敢保证array比cStringIO快,但循环里少了个chr函数,应该不会慢,而测试也证明了这点:约360毫秒,加快了约53%。

当然,ord的调用也很费时,所以可以继续优化,只是这样就比较难看懂了:
from __future__ import with_statement
import codecs
from array import array
from struct import unpack
from time import clock


t = clock()

with codecs.open(r'B:\test.txt', 'r', 'utf8') as inFile:
  inFile.seek(3) # skip BOM
  content = inFile.read()
  content = content.encode('sjis')

  outBuf = array('B')
  unpackedContent = unpack('%dB' % len(content), content)
  for byte in unpackedContent:
    outBuf.append(byte ^ 0xAB)

  with open(r'B:\test2.txt', 'wb') as outFile:
    outBuf.tofile(outFile)

print clock() - t
结果是约310毫秒,比最初版本快了约77%,是Java的1/5。

当然,剩下的就没什么可优化的了,除非把遍历content进行异或写成C扩展。(当然,Psyco或许对第2个版本有一定帮助,毕竟调用了很多次ord函数。)
顺便说下,如果不进行异或的话,Python需要约15毫秒,而Java仍然是62毫秒。也就是说,Python用C编写的模块在IO性能(读写小文件)并进行编码处理上比Java更好,只是没有内置byte类型,而在处理字节方面开销大了不少。
反观Java,初看上去完全不知道该怎么优化,或许多组合几种IO类型,调整一下缓冲区大小,改进一下数组结束的判断都可能会提升,但那需要非常枯燥而又没有技术含量的测试了。

综上,对我而言,Python在编码方面少花了2个小时,这就足够了。哪怕这个文本需要使用这个程序10000次(一般来说不会超过100次),我也少浪费了不少时间,还不必为了无语的缓冲区而吐血。
当然,Python也不是万能的,例如dll就还是得用C/C++写…大家爱挑什么毛病都能挑出来,所以我就此打住了=。=

11条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?