Java与Python中不可变字符串拼接的性能对比

7 投票
5 回答
2476 浏览
提问于 2025-04-16 05:17

更新:非常感谢Gabe和Glenn的详细解释。这个测试不是为了比较语言的性能,而是为了我学习虚拟机优化技术。

我做了一个简单的测试,想了解Java和Python在字符串拼接方面的性能。

这个测试主要针对两种语言中默认的不可变字符串对象/类型。所以在Java的测试中我没有使用StringBuilder或StringBuffer。

测试的内容就是简单地拼接字符串,重复了10万次。Java大约花了32秒完成,而Python对于Unicode字符串只用了大约13秒,对于非Unicode字符串则只用了0.042秒。

我对这个结果有点惊讶。我原本以为Java应该比Python快。Python使用了什么优化技术来获得更好的性能呢?还是说Java的字符串对象设计得太复杂了?

操作系统:Ubuntu 10.04 x64
JDK:Sun 1.6.0_21
Python:2.6.5

Java测试确实使用了-Xms1024m来减少垃圾回收的活动。

Java代码:

public class StringConcateTest {
public static void test(int n) {
    long start = System.currentTimeMillis();
    String a = "";
    for (int i = 0; i < n; i++) {
        a = a.concat(String.valueOf(i));
    }
    long end = System.currentTimeMillis();
    System.out.println(a.length() + ", time:" + (end - start));
}

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        test(1000 * 100);           
    }
}

}

Python代码:

import time
def f(n):
    start = time.time()
    a = u'' #remove u to use non Unicode string
    for i in xrange(n):
        a = a + str(i)
    print len(a), 'time', (time.time() - start)*1000.0
for j in xrange(10):
    f(1000 * 100)

5 个回答

1

我觉得你的测试意义不大,因为Java和Python处理字符串的方式不同(我不是Python方面的专家,但我对Java还是有点了解的)。在Java中,StringBuilder和StringBuffer是有其存在的原因的。语言设计者没有选择更高效的内存管理方式,正是因为他们希望你在编程时使用这些比“String”对象更合适的工具来进行字符串操作。

如果你按照Java的设计思路来做事情,你会惊讶于这个平台的速度有多快……不过我得承认,最近我试过的一些Python应用的性能让我印象深刻。

3

我猜测,Python在处理字符串时会对原有的字符串进行重新分配(realloc),而不是创建一个新的字符串并复制旧的内容。因为如果在分配后有足够的空闲空间,realloc的速度是非常快的。

那么,为什么Python可以使用realloc而Java却不行呢?Python的垃圾回收机制使用的是引用计数,这样它就能知道没有其他地方在使用这个字符串,所以如果这个字符串发生变化也没关系。而Java的垃圾回收机制不维护引用计数,因此它无法判断是否还有其他地方在引用这个字符串,这就意味着每次拼接字符串时,它只能选择创建一个全新的字符串副本。

补充说明:虽然我不确定Python在拼接字符串时是否真的会调用realloc,但在stringobject.c文件中有关于_PyString_Resize的注释,说明了为什么它可以这样做:

       The following function breaks the notion that strings are immutable:
       it changes the size of a string.  We get away with this only if there
       is only one module referencing the object.  You can also think of it
       as creating a new string object and destroying the old one, only
       more efficiently.  In any case, don't use this if the string may
       already be known to some other part of the code...
6

@Gabe的回答是对的,但需要更清楚地说明,而不是假设。

CPython(可能只有CPython)在可以的情况下会进行原地字符串追加。 但这有一些限制。

首先,它不能对“驻留字符串”进行原地追加。 这就是为什么你在测试时不会看到这种情况,比如用 a = "testing"; a = a + "testing",因为给字符串常量赋值会生成一个驻留字符串。 你需要动态创建字符串,就像这段代码用 str(12345) 所做的那样。 (这并不是一个太大的限制;一旦你以这种方式进行追加,结果就是一个非驻留字符串,所以如果你在循环中追加字符串常量,这种情况只会在第一次发生。)

其次,Python 2.x 只对 str 进行这种处理,而不对 unicode 进行。 Python 3.x 对Unicode字符串也这样做。 这很奇怪:这是一个重大的性能差异——在复杂性上有区别。 这让人不太愿意在2.x中使用Unicode字符串,而实际上应该鼓励这样做,以帮助过渡到3.x。

最后,字符串不能有其他的引用。

>>> a = str(12345)
>>> id(a)
3082418720
>>> a += str(67890)
>>> id(a)
3082418720

这就解释了为什么在你的测试中,非Unicode版本的速度比Unicode版本快得多。

实际的代码在 string_concatenate 中,位于 Python/ceval.c,适用于 s1 = s1 + s2s1 += s2。 在 Objects/stringobject.c 中的函数 _PyString_Resize 也明确表示:以下函数打破了字符串不可变的概念。 另请参见 http://bugs.python.org/issue980695

撰写回答