【问题标题】:Performance comparison of immutable string concatenation between Java and PythonJava和Python之间不可变字符串连接的性能比较
【发布时间】:2011-04-23 12:02:20
【问题描述】:

更新:非常感谢 Gabe 和 Glenn 的详细解释。该测试不是为了语言比较基准而编写的,只是为了学习VM优化技术。

我做了一个简单的测试来了解 Java 和 Python 之间字符串连接的性能。

测试是两种语言中默认不可变字符串对象/类型的目标。所以我在 Java 测试中不使用 StringBuilder/StringBuffer。

测试只是简单地添加了 100k 次字符串。 Java 大约需要 32 秒来完成,而 Python 只使用大约 13 秒来处理 Unicode 字符串和 0.042 秒来处理非 Unicode 字符串。

我对结果有点惊讶。我认为 Java 应该比 Python 更快。 Python 利用什么优化技术来实现更好的性能?还是Java中String对象设计的太重了?

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

Java 测试确实使用 -Xms1024m 来最小化 GC 活动。

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)

【问题讨论】:

  • 你对做事错误的方式有不健康的兴趣:p
  • 如果股票 Python 字符串被实现为 Java 的 StringBuffer,我会期望得到的结果。更好的测试可能是在每个平台上实现最好的代码。
  • Java 是如何使用 StringBuilder 的?
  • 是的,老实说...你为什么会对 Java 中一个普遍谴责的过程需要很长时间感到惊讶?
  • 你非常糟糕的 Java 代码可以解释这个结果。对于这样的连接,您必须使用 StringBuilder/StringBuffer。 Integer.toString(i) 也比 String.valueOf(i) 好。

标签: java python performance string concatenation


【解决方案1】:

@Gabe 的回答是正确的,但需要清楚地表明而不是假设。

CPython(可能只有 CPython)会在可能的情况下进行就地字符串追加。什么时候可以这样做是有限制的。

首先,它不能用于实习字符串。这就是为什么如果您使用a = "testing"; a = a + "testing" 进行测试,您将永远不会看到这一点,因为分配字符串文字会产生一个实习字符串。您必须动态创建字符串,就像这段代码对str(12345) 所做的那样。 (这并没有太大的限制;一旦您以这种方式进行追加,result 就是一个非内部字符串,因此如果您在循环中追加字符串文字,这只会在第一次发生。 )

其次,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 版本快得多。

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

【讨论】:

  • 谢谢。这就是我正在寻找的答案。
【解决方案2】:

我用 StringBuilder 代替 String 运行 Java 代码,发现平均完成时间为 10 毫秒(最高 34 毫秒,最低 5 毫秒)。

至于 Python 代码,使用“方法 6”here(发现是最快的方法),我能够使用 unicode 字符串实现平均 84 毫秒(高 91 毫秒,低 81 毫秒)。使用非 unicode 字符串将这些数字减少了约 25 毫秒。

因此,基于这些高度不科学的测试可以说,使用最快的字符串连接方法,Java 大约比 Python 快一个数量级。

但我还是

【讨论】:

  • 他的结果是错误的,或者至少已经过时了:方法 1 在 2.6 中是最快的,差一点。 (如果你没有运行它也就不足为奇了,因为它甚至没有按原样运行,因为它使用了一个名为 timing 的模块,他没有包含它,而不是使用标准模块 timeit。)另请注意,ps_stats 调用需要很长时间(此处约为 18 毫秒),必须删除才能获得有意义的结果。
【解决方案3】:

我的猜测是 Python 只是在字符串上执行realloc,而不是用旧字符串的副本创建一个新字符串。由于realloc在分配后有足够的空闲空间时不会花费时间,因此非常快。

那么为什么 Python 可以调用 realloc 而 Java 不能呢? Python 的垃圾收集器使用引用计数,因此它可以判断没有其他人在使用该字符串,并且字符串是否发生变化也无关紧要。 Java 的垃圾收集器不维护引用计数,因此它无法判断对字符串的任何其他引用是否存在,这意味着它别无选择,只能在每次连接时创建字符串的全新副本。

编辑:虽然我不知道 Python 确实会在 concat 上调用 realloc,但下面是 stringobject.c 中 _PyString_Resize 的注释,说明了它为什么可以:

以下函数打破了字符串不可变的概念: 它改变了字符串的大小。我们只有在有的情况下才能逃脱 只是引用该对象的一个​​模块。你也可以想到 就像创建一个新的字符串对象并销毁旧的一样,只有 更有效率。在任何情况下,如果字符串可能,请不要使用它 代码的其他部分已经知道...

【讨论】:

  • 很好的解释;我对 Python 的了解还不够,无法知道它是否正确。但是,即使 Java 确实知道“a”没有被其他东西引用,它也会有 JLS (java.sun.com/docs/books/jls/third_edition/html/…) 来应对:结果是对 String 对象的引用(已创建,除非表达式是编译时常量表达式)... 因此,即使实现似乎也无法优化它。
  • Python 字符串和 Java 一样是不可变的,所以我怀疑 python 正在执行 realloc()。
  • GregS:正如我在第二段中提到的,Python 有一个引用计数,因此它可以执行realloc,因为它可以知道没有其他人在引用该字符串。请参阅我的编辑,了解为什么会这样。
  • @GregS 是正确的,没有使用 realloc。一个新字符串是malloc'ed,两个字符串是memcpy'ed 到新字符串中。 (一目了然 2.4 源代码。)
  • @zdav 弄错了,可能是因为他在看古老的源代码;详情见我的回答。
【解决方案4】:

我不确定答案。但这里有一些想法。首先,Java 在内部将字符串存储为 char [] 数组,其中包含字符串的 UTF-16 编码。这意味着字符串中的每个字符至少占用两个字节。因此,仅就原始存储而言,Java 必须复制的数据量大约是 python 字符串的两倍。 Python unicode 字符串因此是更好的测试,因为它们具有类似的能力。也许 python 将 unicode 字符串存储为 UTF-8 编码字节。在这种情况下,如果您在其中存储的所有内容都是 ASCII 字符,那么您将再次让 Java 使用两倍的空间,因此进行两倍的复制。为了获得更好的比较,您应该连接包含更多有趣字符的字符串,这些字符在 UTF-8 编码中需要两个或更多字节。

【讨论】:

  • Python 没有这样的优化。来自 Python 的 unicodeobject.h:“设置 Py_UNICODE_WIDE 启用 UCS-4 存储。否则,Unicode 字符串将存储为 UCS-2(对 UTF-16 的支持有限)”
  • @Gabe:谢谢,这是很好的信息。因此,也许任何性能改进都完全是由于您在回答中概述的考虑因素。
【解决方案5】:

我认为您的测试意义不大,因为 Java 和 Python 处理字符串的方式不同(我不是 Python 专家,但我知道我在 Java 中的方式)。 StringBuilders/Buffers 在 Java 中存在是有原因的。正是因为这个原因,语言设计者并没有做任何更有效的内存管理/操作:除了“String”对象之外,还有其他工具可以进行这种操作,他们希望您在编码时使用它们。

当您按照 Java 应有的方式做事时,您会惊讶于该平台的速度有多快……但我必须承认,我对一些 Python 应用程序的性能印象深刻最近试过了。

【讨论】:

  • 我部分同意你的观点,但对于一个基准,你试图将喜欢与喜欢进行比较。他们以不同方式处理字符串的事实是基准测试的重点。为什么 python 看起来明显更快?如果 OPs 基准测试有缺陷,那肯定不是这两种语言做事不同。
  • @GregS:我同意基准应该将喜欢与喜欢进行比较。然而,我相信那些“喜欢”应该是在上述平台上做事的最佳方式。而且,在 java 中,应该使用 StringBuilders 来集中操作字符串。为什么python的速度更快?我不知道,但我承认它令人印象深刻。我只是觉得这里的基准与现实世界的应用有点太远了……
猜你喜欢
  • 2020-06-26
  • 2013-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-11
  • 2021-10-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多