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

Bla*_*huk 15 python java microbenchmark

更新:2009-05-29

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

教训:

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

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

    java -Xmx1024m SpeedTest

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

java -Xms1024m -Xmx1024m SpeedTest
Run Code Online (Sandbox Code Playgroud)

这个简单的更改将执行时间减少了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)
Run Code Online (Sandbox Code Playgroud)

它在我的机器上执行大约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());
    }
}
Run Code Online (Sandbox Code Playgroud)

所以这个java代码与python代码基本相同.但它在8.3秒而不是3.3秒内执行.

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

这是我的问题:

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

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

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

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

更新:

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

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

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

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

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

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

我使用的是java版:Java(TM)SE Runtime Environment(版本1.6.0_13-b03).

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

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

Mic*_*rdt 21

你并没有真正测试Java与Python,你正在测试java.util.HashSet使用autoboxed Integers与Python的本机集和整数处理.

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

我试着用替换的HashSet TIntHashSetGNU宝库,取得了3和4之间的加速因素,带来的Java稍稍领先的Python.

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


Ant*_*sma 12

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

这不一定是坏事!相反,在大小为2**i的表中,将低位i位作为初始表索引是非常快的,并且对于由连续的整数范围索引的dicts,根本没有冲突.当键是"连续"字符串时,大致相同.因此,这在常见情况下提供了比随机更好的行为,这是非常理想的.

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

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


use*_*294 7

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

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

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

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

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

  • 浮渣.在大多数情况下,Java被编译为优化的机器代码,而Python通常不会.Java中的对象创建速度非常快(比任何C++实现都要快).(特别是在Azul硬件上!) (5认同)
  • @Tom Hawtin,比任何C++实现更快?真的,我想看到这个的基准. (4认同)
  • 实际上,Java*使用*在对象创建方面表现不佳,但现在已经好几年了.这一点旧信息依然存在,尽管它今天如此不真实. (2认同)

Cli*_*ler 7

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

  • 为什么C会比Java快得多?可能存在实现差异,例如,常见的是将小整数推入引用而不是实际拥有对象. (2认同)
  • "对于大多数情况来说,C IS比Java更快" - 大多数Java程序员从1.5开始就认为是错误的. (2认同)

Die*_*Epp 6

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

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

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

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


Emi*_*l H 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());
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我机器上的结果:

$ java -Xmx1024M SpeedTest
HashSet:
TOTAL TIME = 4.436
10000000
TreeSet:
TOTAL TIME = 8.163
10000000
Run Code Online (Sandbox Code Playgroud)

有几个人还认为拳击不是一个性能问题,而且创造对象很便宜.虽然对象创建速度很快,但它绝对不如原语:

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) );
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

$ java -Xmx1024M SpeedTest2
primitives:
TOTAL TIME = 0.058
primitives:
TOTAL TIME = 1.402
Run Code Online (Sandbox Code Playgroud)

而且,创建大量对象会导致垃圾收集器产生额外的开销.当您开始在内存中保留数千万个活动对象时,这将变得非常重要.


Gan*_*alf 2

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

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

  • 由于 OP 测量循环中花费的时间,因此 JVM 启动时间不适用 (5认同)