我的python程序执行速度比同一程序的java版本快。是什么赋予了?

时间:2009-05-27 22:48:42

标签: java python microbenchmark

更新:2009-05-29

感谢所有的建议和意见。 我使用你的建议使我的生产代码平均比几天前的最佳结果快2.5倍。最后我能够使java代码最快。

经验:

  • 下面的示例代码显示了原始int的插入,但生产代码实际上是存储字符串(我的坏)。当我纠正那个python执行时间从2.8秒变为9.6时。因此,在存储对象时,java实际上更快。

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

    java -Xmx1024m SpeedTest

但是,如果您按如下方式设置初始堆大小,则会获得巨大的改进:

java -Xms1024m -Xmx1024m SpeedTest

这个简单的更改将执行时间减少了50%以上。所以我的SpeedTest的最终结果是蟒蛇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实现更快?

  5. 更新:

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

    我没有包含我的生产代码,因为它非常复杂。并会产生很多分心。我上面提到的案例是最简化的。我的意思是java put调用似乎比python set的add()慢得多。

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

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

    我使用足够多的存储区初始化了hashset,因此它永远不必重新散列。 (我将提前知道集合最终将包含多少元素。)我想有人可能会说我应该将它初始化为迭代/ 0.75。但是如果你尝试它,你会发现执行时间没有受到太大影响。

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

    我正在使用java版本:Java(TM)SE运行时环境(版本1.6.0_13-b03)。

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

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

20 个答案:

答案 0 :(得分:21)

你并没有真正测试Java与Python,你使用自动装箱整数与Python的原生集和整数处理来测试java.util.HashSet

显然,这个特殊微基准测试中的Python方面确实更快。

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

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

答案 1 :(得分:12)

我怀疑是Python使用整数值本身作为其哈希,而基于哈希表的set实现直接使用该值。来自source

中的评论
  

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

这个微基准测试在某种程度上是Python的最佳案例,因为它可以产生零哈希冲突。然而,如果Javas HashSet正在重复关键,它必须执行额外的工作,并且在碰撞时会出现更糟糕的行为。

如果将范围(迭代)存储在临时变量中并在循环之前对其执行random.shuffle,则即使在循环外部执行了shuffle和list创建,运行时也会慢2倍。

答案 2 :(得分:7)

一般来说,我的经验是python程序比java程序运行得更快,尽管java是一种“低级”语言。很明显,两种语言都被编译成字节代码(这就是那些.pyc文件 - 您可以将它们视为类似.class文件)。这两种语言都是在虚拟堆栈计算机上进行字节码解释。

你会期望python在诸如a.b之类的东西上变慢。在java中,a.b将解析为解除引用。另一方面,Python必须执行一个或多个哈希表查找:检查本地范围,检查模块范围,检查全局范围,检查内置。

另一方面,java在某些操作(例如可能是您的示例中的罪魁祸首)和序列化等特定操作方面出了名。

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

更正:有几个人已经指出java在对象创建方面不再那么糟糕。所以,在你的例子中,它是另一回事。也许这是昂贵的自动装箱,也许python的默认哈希算法在这种情况下更好。根据我的实际经验,当我将java代码重写为python时,我总是看到性能提升,但这可能与语言一样多,因为重写通常会导致性能提升。

答案 3 :(得分:7)

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

答案 4 :(得分:6)

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

Java编译,是的,字节码,但最终编译到大多数运行时环境中的本机代码。那些说C本来就更快的人并不是在讲述整个故事,我可以假设字节编译语言本身就更快,因为JIT编译器可以进行特定于机器的优化,而这些优化对于提前编译器是不可用的。

可能产生差异的一些事情是:

  • Python的哈希表和集合是Python中最优化的对象,Python的哈希函数旨在为类似的输入返回类似的结果:哈希一个整数只返回整数,保证你永远不会在哈希中看到冲突Python中连续整数表。
  • 上述的第二个影响是Python代码将具有较高的引用位置,因为您将按顺序访问哈希表。
  • 当你将它们添加到集合时,Java会做一些花哨的装箱和取消装箱的整数。在奖金方面,这使得Java中的算术运算速度比Python快(只要你远离bignums),但在不利方面,它意味着比你习惯的分配更多。

答案 5 :(得分: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

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

答案 6 :(得分:4)

我觉得这样的基准没有意义。我不解决看起来像测试用例的问题。这不是非常有趣。

我更倾向于使用NumPy和JAMA来寻找有意义的线性代数解决方案。也许我会尝试并报告结果。

答案 7 :(得分:3)

我对python不是很熟悉,但我知道HashSet不能包含原语,所以当你说counts.add(i) i时,new Integer(i)会自动进入 Integer[] ints = new Integer[N]; for (int i = 0; i < N; ++i) { ints[i] = i; } 1}}打电话。那可能是你的问题。

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

修改

停止投票我的回答。这是不对的。

编辑

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

{{1}}

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

答案 8 :(得分:3)

我想在这里提出一些问题。

首先,如果它是一个你只会运行一次的程序,那么它需要多花几秒钟吗?

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

Startup有很多问题。

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

如果您尚未设置-Xms,则可能只是为了调整堆大小而运行GC。不妨在开始时正确调整堆的大小。

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

最重要的是,每次迭代可能会创建两个对象。对于大多数代码,您不会为执行的这一部分创建这些微小对象。 TreeSet可能在对象数量上更好,6u14和Harmony变得更好。

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

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

更好的数据结构:像BitSet这样的东西似乎有意义(虽然它有一个ynchronisation,它可能会或可能不会影响性能)。

答案 9 :(得分:2)

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

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

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

修改

当我说使用TreeSet时,我的意思是一般情况下,不是这个基准。 TreeSet处理对象的非偶数散列的现实问题。如果在HashSet中的同一个bin中有太多对象,则执行大约O(n)。

答案 10 :(得分: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更快'是怎么回事?

答案 11 :(得分:2)

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

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

答案 12 :(得分: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替换LinkedList,程序会大大加快。

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

答案 13 :(得分:1)

在这里只是一个黑暗的刺,但是一些优化Python正在使Java可能不是:

  • Python中的range()调用是在优化的C代码中一次创建所有10000000个整数对象。 Java必须在每次迭代时创建一个Integer对象,这可能会更慢。
  • 在Python中,int是不可变的,因此您可以只存储对全局“42”的引用,而不是为该对象分配一个槽。我不确定Java盒装的Integer对象是如何比较的。
  • 许多内置的Python算法和数据结构都针对特殊情况进行了大量优化。例如,整数的哈希函数只是身份函数。如果Java使用更“聪明”的哈希函数,这可能会使事情变得相当缓慢。如果你的大部分时间都花在了数据结构代码上,那么考虑到多年来手动调优Python C实现所付出的努力,我不会对Python击败Java感到惊讶。

答案 14 :(得分: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

答案 15 :(得分: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());

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

答案 16 :(得分:0)

最大的问题可能是给定代码测量的是wall time - 你的手表测量的是什么 - 但是比较代码运行时应该测量的是process time - 时间量cpu花费执行特定代码而不执行其他任务。

答案 17 :(得分: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; }
    };

简单,快捷并获得相同的结果。

答案 18 :(得分:0)

你可能想看看你是否可以“准备”JIT编译器来编译你感兴趣的代码部分,或者通过预先将它作为一个函数运行并在之后简短地休息。这可能允许JVM将函数编译为本机代码。

答案 19 :(得分:0)

我同意Gandalf的启动时间。此外,您正在分配一个巨大的HashSet,它与您的python代码完全不相似。如果你把它放在一个分析器下我会成像,那里会花很多时间。此外,使用此大小插入新元素实际上会很慢。我会按照建议调查TreeSet。