15

更新:2009-05-29

感谢所有的建议和建议。 我使用您的建议使我的生产代码的平均执行速度比几天前的最佳结果快 2.5 倍。 最后,我能够使 java 代码最快。

教训:

  • 我下面的示例代码显示了原始整数的插入,但生产代码实际上是存储字符串(我的错)。当我更正 python 执行时间从 2.8 秒变为 9.6 时。所以马上,java 在存储对象时实际上更快。

  • 但它并不止于此。我一直在执行java程序,如下所示:

    java -Xmx1024m 速度测试

但是,如果您将初始堆大小设置如下,您将获得巨大的改进:

java -Xms1024m -Xmx1024m SpeedTest

这个简单的更改将执行时间减少了 50% 以上。所以我的 SpeedTest 的最终结果是 python 9.6 秒。Java 6.5 秒。

原始问题:

我有以下python代码:

import time
import sys

def main(args):    
    iterations = 10000000
    counts = set()
    startTime = time.time();    
    for i in range(0, iterations):
        counts.add(i)
    totalTime = time.time() - startTime
    print 'total time =',totalTime
    print len(counts)

if __name__ == "__main__":
    main(sys.argv)

它在我的机器上执行了大约 3.3 秒,但我想让它更快,所以我决定用 java 编程。我假设因为 java 是编译的并且通常被认为比 python 快,所以我会看到一些巨大的回报。

这是java代码:

import java.util.*;
class SpeedTest
{    
    public static void main(String[] args)
    {        
        long startTime;
        long totalTime;
        int iterations = 10000000;
        HashSet counts = new HashSet((2*iterations), 0.75f);

        startTime = System.currentTimeMillis();
        for(int i=0; i<iterations; i++)
        {
            counts.add(i);
        }
        totalTime = System.currentTimeMillis() - startTime;
        System.out.println("TOTAL TIME = "+( totalTime/1000f) );
        System.out.println(counts.size());
    }
}

所以这个java代码和python代码做的事情基本上是一样的。但它在 8.3 秒而不是 3.3 秒内执行。

我从一个真实的例子中提取了这个简单的例子来简化事情。关键元素是我有(set 或 hashSet)最终有很多成员,就像这个例子一样。

以下是我的问题:

  1. 为什么我的 python 实现比我的 java 实现快?

  2. 有没有比 hashSet (java) 更好的数据结构来保存唯一的集合?

  3. 什么会使 python 实现更快?

  4. 什么会使java实现更快?

更新:

感谢所有迄今为止做出贡献的人。请允许我添加一些细节。

我没有包含我的生产代码,因为它非常复杂。并且会产生很多干扰。我上面介绍的案例是最简单的。我的意思是java put调用似乎比python set`s add()慢得多。

生产代码的 java 实现也比 python 版本慢 2.5 - 3 倍——就像上面一样。

我不关心 vm 预热或启动开销。我只想比较从我的 startTime 到我的 totalTime 的代码。请不要为其他事情操心。

我用足够多的桶初始化了哈希集,这样它就不必重新哈希了。(我总是会提前知道集合最终将包含多少元素。)我想有人可能会争辩说我应该将它初始化为迭代次数/0.75。但是,如果您尝试一下,您会发现执行时间并没有受到显着影响。

我为那些好奇的人设置了 Xmx1024m(我的机器有 4GB 的内存)。

我正在使用 java 版本:Java(TM) SE Runtime Environment (build 1.6.0_13-b03)。

在生产版本中,我在 hashSet 中存储了一个字符串(2-15 个字符),因此我不能使用原语,尽管这是一个有趣的案例。

我已经多次运行代码。我非常相信 python 代码比 java 代码快 2.5 到 3 倍。

4

20 回答 20

21

您并没有真正测试 Java 与 Python,而是java.util.HashSet使用自动装箱整数与 Python 的本机集合和整数处理进行测试。

显然,这个特定微基准测试中的 Python 端确实更快。

我尝试用TIntHashSet来自GNU trove的 HashSet 替换,并实现了 3 到 4 之间的加速因子,使 Java 略微领先于 Python。

真正的问题是您的示例代码是否真的像您想象的那样代表您的应用程序代码。您是否运行了分析器并确定大部分 CPU 时间都花在了将大量整数放入 HashSet 中?如果不是,则示例无关紧要。即使唯一的区别是您的生产代码存储了除 int 之外的其他对象,它们的创建和哈希码的计算也很容易支配集合插入(并完全破坏 Python 在专门处理 int 方面的优势),使整个问题变得毫无意义。

于 2009-05-28T09:26:11.660 回答
12

我怀疑这是 Python 使用整数值本身作为其哈希值,而基于哈希表的 set 实现直接使用该值。从中的评论:

这不一定是坏事!相反,在大小为 2**i 的表中,将低位 i 位作为初始表索引非常快,并且对于由连续范围的 int 索引的 dicts 完全没有冲突。当键是“连续的”字符串时,情况大致相同。因此,这在常见情况下提供了优于随机的行为,这是非常可取的。

这个微基准测试在某种程度上是 Python 的最佳案例,因为它会导致零散列冲突。然而,如果 Java 的 HashSet 正在重新散列密钥,它必须执行额外的工作,并且还会因冲突而变得更糟糕。

如果您将范围(迭代)存储在一个临时变量中并在循环之前对其执行 random.shuffle ,即使在循环之外完成随机播放和列表创建,运行时也会慢 2 倍以上。

于 2009-05-28T08:46:54.967 回答
7

根据我的经验,python 程序比 java 程序运行得更快,尽管 java 是一个有点“低级”的语言。顺便说一句,这两种语言都被编译成字节码(这就是那些 .pyc 文件的内容——你可以把它们想象成类似于 .class 文件)。两种语言都是在虚拟堆栈机器上解释的字节码。

您会期望 python 在诸如a.b. 在 java 中,这a.b将解析为取消引用。另一方面,Python 必须进行一个或多个哈希表查找:检查本地范围、检查模块范围、检查全局范围、检查内置函数。

另一方面,java 在某些操作方面出了名的糟糕,例如对象创建(这可能是您示例中的罪魁祸首)和序列化。

总之,没有简单的答案。对于所有代码示例,我不希望任何一种语言都更快。

更正:有几个人指出,java 在对象创建方面不再那么糟糕了。因此,在您的示例中,这是另一回事。也许它的自动装箱很昂贵,也许在这种情况下python的默认散列算法更好。在我的实践经验中,当我将 java 代码重写为 python 时,我总是看到性能提升,但这可能是由于语言的原因,因为重写通常会导致性能提升。

于 2009-05-27T23:04:37.917 回答
7

另一种可能的解释是 Python 中的集合是在 C 代码中本地实现的,而 Java 中的 HashSet 是在 Java 本身中实现的。因此,Python 中的集合本质上应该更快。

于 2009-05-27T23:06:52.827 回答
6

我想消除我在答案中看到的几个神话:

是的,Java 被编译为字节码,但在大多数运行时环境中最终编译为本机代码。说 C 本质上更快的人并没有说出全部故事,我可以证明字节编译语言本质上更快,因为 JIT 编译器可以进行机器特定的优化,而这些优化对于提前编译器是不可用的。

可能产生差异的一些因素是:

  • Python 的哈希表和集合是 Python 中优化程度最高的对象,Python 的哈希函数旨在为类似的输入返回类似的结果:对整数进行哈希处理只返回整数,保证您永远不会在连续的哈希表中看到冲突Python 中的整数。
  • 上述的第二个效果是 Python 代码将具有较高的引用局部性,因为您将按顺序访问哈希表。
  • 当您将整数添加到集合中时,Java 会对整数进行一些精美的装箱和拆箱。在好处方面,这使得 Java 中的算术方式比 Python 更快(只要你远离 bignums),但不利的一面是它意味着比你习惯的分配更多。
于 2009-05-28T09:53:03.367 回答
5

编辑:对于实际用例,TreeSet 可能更快,具体取决于分配模式。我在下面的评论仅涉及这种简化的场景。但是,我不相信这会产生非常显着的差异。真正的问题在别处。

这里有几个人建议用 TreeSet 替换 HashSet。对我来说,这听起来像是一个非常奇怪的建议,因为插入时间为 O(log n) 的数据结构不可能比预先分配足够的桶来存储所有元素的 O(1) 结构更快。

这是一些对此进行基准测试的代码:

import java.util.*;
class SpeedTest
{    
    public static void main(String[] args)
    {        
        long startTime;
        long totalTime;
        int iterations = 10000000;
        Set counts;

        System.out.println("HashSet:");
        counts = new HashSet((2*iterations), 0.75f);
        startTime = System.currentTimeMillis();
        for(int i=0; i<iterations; i++) {
            counts.add(i);
        }
        totalTime = System.currentTimeMillis() - startTime;
        System.out.println("TOTAL TIME = "+( totalTime/1000f) );
        System.out.println(counts.size());

        counts.clear();

        System.out.println("TreeSet:");
        counts = new TreeSet();
        startTime = System.currentTimeMillis();
        for(int i=0; i<iterations; i++) {
            counts.add(i);
        }
        totalTime = System.currentTimeMillis() - startTime;
        System.out.println("TOTAL TIME = "+( totalTime/1000f) );
        System.out.println(counts.size());
    }
}

这是我机器上的结果:

$ java -Xmx1024M SpeedTest
HashSet:
TOTAL TIME = 4.436
10000000
TreeSet:
TOTAL TIME = 8.163
10000000

一些人还认为,装箱不是性能问题,而且对象创建成本低廉。虽然对象创建确实很快,但它肯定不如原语快:

import java.util.*;
class SpeedTest2
{    
    public static void main(String[] args)
    {        
        long startTime;
        long totalTime;
        int iterations = 10000000;

        System.out.println("primitives:");
        startTime = System.currentTimeMillis();
        int[] primitive = new int[iterations];
        for (int i = 0; i < iterations; i++) {
            primitive[i] = i;
        }
        totalTime = System.currentTimeMillis() - startTime;
        System.out.println("TOTAL TIME = "+( totalTime/1000f) );

        System.out.println("primitives:");
        startTime = System.currentTimeMillis();
        Integer[] boxed = new Integer[iterations];
        for (int i = 0; i < iterations; i++) {
            boxed[i] = i;
        }
        totalTime = System.currentTimeMillis() - startTime;
        System.out.println("TOTAL TIME = "+( totalTime/1000f) );
    }
}

结果:

$ java -Xmx1024M SpeedTest2
primitives:
TOTAL TIME = 0.058
primitives:
TOTAL TIME = 1.402

此外,创建大量对象会导致垃圾收集器产生额外的开销。当您开始在内存中保存数以千万计的活动对象时,这变得很重要。

于 2009-05-27T23:33:12.947 回答
4

我发现这样的基准毫无意义。我不解决看起来像测试用例的问题。这不是很有趣。

我更愿意看到使用 NumPy 和 JAMA 的有意义的线性代数解决方案的解决方案。也许我会尝试并报告结果。

于 2009-05-27T23:20:36.293 回答
3

我对python不太熟悉,但我知道HashSet不能包含原语,所以当你说counts.add(i)i自动装箱到new Integer(i)调用时。那可能是你的问题。

如果出于某种原因你真的需要一个介于 0 和某个大 n 之间的整数的“集合”,那么最好将其声明为“boolean[] set = new boolean[n]”。然后,您可以遍历数组并将集合中的项目标记为“真”,而不会产生创建 n 个整数包装器对象的开销。如果您想更进一步,您可以使用大小为 n/8 的 byte[] 并直接使用各个位。或者也许是 BigInteger。

编辑

停止投票我的答案。这是错的。

编辑

不,真的,它错了。如果我按照问题的建议进行操作,我会获得可比的性能,用 N 个整数填充集合。如果我用这个替换for循环的内容:

    Integer[] ints = new Integer[N];
    for (int i = 0; i < N; ++i) {
        ints[i] = i;
    }

然后只需要2秒。如果您根本不存储整数,那么它需要不到 200 毫秒。强制分配 10000000 个 Integer 对象确实需要一些时间,但看起来大部分时间都花在了 HashSet put 操作中。

于 2009-05-27T22:54:07.597 回答
3

这里有很多问题我想汇总一下。

首先,如果它是一个你只打算运行一次的程序,多花几秒钟有关系吗?

其次,这只是一个微基准。微基准对于比较性能毫无意义。

创业有很多问题。

Java 运行时比 Python 大得多,因此从磁盘加载需要更长的时间并占用更多内存,这在交换时可能很重要。

如果您还没有设置-Xms,您可能只是在运行 GC 来调整堆的大小。还不如在开始时正确调整堆大小。

Java 确实从解释开始然后编译。Sun 客户端 [C1] Hotspot 大约 1,500 次迭代,服务器 [C2] 大约 10,000 次。服务器热点最终会为您提供更好的性能,但会占用更多内存。我们可能会看到客户端 Hotspot 使用服务器来执行非常频繁的代码,以实现两全其美。但是,这通常不应该是几秒钟的问题。

最重要的是,您可能会在每次迭代中创建两个对象。对于大多数代码,您不会为如此比例的执行创建这些微小的对象。TreeSet 在对象数上可能会更好,6u14 和 Harmony 会更好。

Python 可能会通过在引用中存储小整数对象而不是实际拥有一个对象来获胜。这无疑是一个很好的优化。

许多基准测试的问题是您在一种方法中混合了许多不同的代码。你不会像那样写你关心的代码,对吗?那么,您为什么要尝试执行与您实际上想要快速运行的代码不同的性能测试呢?

更好的数据结构:类似的东西BitSet似乎是有道理的(尽管它有同步,这可能会或可能不会影响性能)。

于 2009-05-27T23:21:41.503 回答
2

您需要多次运行它才能真正了解每次运行的“速度”。JVM 启动时间 [for one] 增加了 Java 版本的单次运行时间。

您还创建了一个初始容量较大的 HashSet,这意味着将使用许多可用插槽创建支持 HashMap,这与创建基本 Set 的 Python 不同。很难说这是否会阻碍,因为当你的 HashSet 增长时,它必须重新分配存储的对象。

于 2009-05-27T22:53:09.017 回答
2

您是否在 jvm 中使用 -server 标志?没有它,您将无法测试性能。(在进行测试之前,您还必须预热 jvm。)

此外,您可能想使用TreeSet<Integer>. 从长远来看,HashSet 会变慢。

你使用的是哪个jvm?我希望是最新的。

编辑

当我说使用 TreeSet 时,我的意思是一般来说,不是为了这个基准。TreeSet 处理对象的非均匀散列的现实世界问题。如果您在 HashSet 的同一个 bin 中获得太多对象,您将执行大约 O(n)。

于 2009-05-27T22:56:58.090 回答
2

如果您真的想将原始类型存储在一个集合中,并对其进行繁重的工作,请在 Java 中创建您自己的集合。通用类对于科学计算来说不够快。

正如 Ants Aasma 所提到的,Python 绕过散列并直接使用整数。Java 创建一个 Integer 对象(autoboxing),然后将其转换为 Object (在您的实现中)。该对象也必须经过哈希处理,才能在哈希集中使用。

为了有趣的比较,试试这个:

爪哇

import java.util.HashSet;
class SpeedTest
{ 
  public static class Element {
    private int m_i;
    public Element(int i) {
      m_i = i;
    }
  }

  public static void main(String[] args)
  {        
    long startTime;
    long totalTime;
    int iterations = 1000000;
    HashSet<Element> counts = new HashSet<Element>((int)(2*iterations), 0.75f);

    startTime = System.currentTimeMillis();
    for(int i=0; i<iterations; ++i)
    {
      counts.add(new Element(i));
    }
    totalTime = System.currentTimeMillis() - startTime;
    System.out.println("TOTAL TIME = "+( totalTime/1000f) );
    System.out.println(counts.size());
  }
}

结果:

$java SpeedTest
TOTAL TIME = 3.028
1000000

$java -Xmx1G -Xms1G SpeedTest
TOTAL TIME = 0.578
1000000

Python

#!/usr/bin/python
import time
import sys

class Element(object):
  def __init__(self, i):
    self.num = i

def main(args):    
    iterations = 1000000
    counts = set()
    startTime = time.time();    
    for i in range(0, iterations):
        counts.add(Element(i))
    totalTime = time.time() - startTime
    print 'total time =',totalTime
    print len(counts)


if __name__ == "__main__":
  main(sys.argv)

结果:

$./speedtest.py 
total time = 20.6943161488
1000000

'python 比 java 快' 怎么样?

于 2009-05-28T09:30:18.170 回答
1

Just a stab in the dark here, but some optimizations that Python is making that Java probably isn't:

  • The range() call in Python is creating all 10000000 integer objects at once, in optimized C code. Java must create an Integer object each iteration, which may be slower.
  • In Python, ints are immutable, so you can just store a reference to a global "42", for example, rather than allocating a slot for the object. I'm not sure how Java boxed Integer objects compare.
  • Many of the built-in Python algorithms and data structures are rather heavily optimized for special cases. For instance, the hash function for integers is, simply the identity function. If Java is using a more "clever" hash function, this could slow things down quite a bit. If most of your time is spent in data structure code, I wouldn't be surprised at all to see Python beat Java given the amount of effort that has been spent over the years hand-tuning the Python C implementation.
于 2009-05-28T01:53:24.287 回答
1

你用多少内存来启动 JVM?这取决于?当我使用 1 Gig RAM 的程序运行 JVM 时:

$ java -Xmx1024M -Xms1024M -classpath . SpeedTest 
TOTAL TIME = 5.682
10000000
$ python speedtest.py 
total time = 4.48310899734
10000000

如果我以更少的内存运行 JVM,则需要更长的时间……更长的时间:

$ java -Xmx768M -Xms768M -classpath . SpeedTest 
TOTAL TIME = 6.706
10000000
$ java -Xmx600M -Xms600M -classpath . SpeedTest 
TOTAL TIME = 14.086
10000000

我认为这HashSet是这个特定实例中的性能瓶颈。如果我HashSet用 a替换LinkedList,程序会变得更快。

最后——请注意,Java 程序最初是被解释的,只有那些被多次调用的方法才会被编译。因此,您可能将 Python 与 Java 的解释器进行比较,而不是编译器。

于 2009-05-27T23:14:12.343 回答
1

一些用于更快 Python 的更改。

#!/usr/bin/python
import time
import sys

import psyco                 #<<<<  
psyco.full()

class Element(object):
    __slots__=["num"]        #<<<<
    def __init__(self, i):
        self.num = i

def main(args):    
    iterations = 1000000
    counts = set()
    startTime = time.time();
    for i in xrange(0, iterations):
        counts.add(Element(i))
    totalTime = time.time() - startTime
    print 'total time =',totalTime
    print len(counts)

if __name__ == "__main__":
  main(sys.argv)

(env)~$ python speedTest.py
total time = 8.82906794548
1000000

(env)~$ python speedTest.py
total time = 2.44039201736
1000000

现在一些很好的旧作弊和...

#!/usr/bin/python
import time
import sys
import psyco

psyco.full()

class Element(object):
    __slots__=["num"]
    def __init__(self, i):
        self.num = i

def main(args):    
    iterations = 1000000
    counts = set()
    elements = [Element(i) for i in range(0, iterations)]
    startTime = time.time();
    for e in elements:
        counts.add(e)
    totalTime = time.time() - startTime
    print 'total time =',totalTime
    print len(counts)

if __name__ == "__main__":
  main(sys.argv)

(env)~$ python speedTest.py
total time = 0.526521921158
1000000
于 2009-06-02T13:53:25.490 回答
1

好吧,如果您要调整 Java 程序,那么您不妨也调整 Python 程序。

>>> import timeit
>>> timeit.Timer('x = set()\nfor i in range(10000000):\n    x.add(i)').repeat(3, 1)
[2.1174559593200684, 2.0019571781158447, 1.9973630905151367]
>>> timeit.Timer('x = set()\nfor i in xrange(10000000):\n    x.add(i)').repeat(3, 1)
[1.8742368221282959, 1.8714439868927002, 1.869229793548584]
>>> timeit.Timer('x = set(xrange(10000000))').repeat(3, 1)
[0.74582195281982422, 0.73061800003051758, 0.73396801948547363]

只需使用xrange它就可以在我的机器上快 8% 左右。表达式set(xrange(10000000))构建完全相同的集合,但速度提高了2.5 倍(从 1.87 秒到 0.74 秒)。

我喜欢如何调整 Python 程序使其更短。:) 但是 Java 也可以做到这一点。众所周知,如果你想要 Java 中的一组密集的小整数,你不会使用哈希表。你用一个java.util.BitSet

BitSet bits = new BitSet(iterations);

startTime = System.currentTimeMillis();
bits.set(0, iterations, true);
totalTime = System.currentTimeMillis() - startTime;
System.out.println("TOTAL TIME = "+( totalTime/1000f) );
System.out.println(bits.cardinality());

那应该相当快。不幸的是,我现在没有时间测试它。

于 2009-11-20T23:53:31.430 回答
0

我同意 Gandalf 关于启动时间的观点。此外,您正在分配一个与您的 python 代码完全不同的巨大 HashSet。我想象如果你把它放在一个分析器下,很多时间会花在那里。此外,在这种尺寸下插入新元素确实会很慢。我会按照建议研究 TreeSet。

于 2009-05-27T23:01:27.517 回答
0

您可能想看看您是否可以“启动” JIT 编译器来编译您感兴趣的代码部分,方法是预先将其作为函数运行一次并在之后短暂休眠。这可能允许 JVM 将函数编译为本机代码。

于 2009-05-28T01:59:17.143 回答
0

最大的问题可能是给定的代码测量的是墙上时间——你的手表测量的是什么——但是为了比较代码运行时间应该测量的是进程时间——CPU执行特定代码而不是其他代码所花费的时间任务。

于 2009-05-27T23:24:43.493 回答
0

只需添加一些简单的额外功能,您就可以使 Java microbenchamrk 更快。

    HashSet counts = new HashSet((2*iterations), 0.75f);

变成

    HashSet counts = new HashSet((2*iterations), 0.75f) {
        @Override public boolean add(Object element) { return false; }
    };

简单、快速并获得相同的结果。

于 2009-05-27T23:59:54.450 回答