Моя программа на Python выполняется быстрее, чем моя Java-версия той же программы. Что дает? - PullRequest
15 голосов
/ 28 мая 2009

Обновление: 2009-05-29

Спасибо за все предложения и советы. Я воспользовался вашими предложениями, чтобы мой рабочий код выполнялся в среднем в 2,5 раза быстрее, чем мой лучший результат пару дней назад. В итоге я смог сделать Java-код быстрее всего.

Уроки:

  • Мой пример кода ниже показывает вставку примитивных целых, но рабочий код на самом деле хранит строки (мой плохой). Когда я исправил это, время выполнения питона изменилось с 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?

UPDATE:

Спасибо всем, кто внес свой вклад. Пожалуйста, позвольте мне добавить некоторые детали.

Я не включил свой производственный код, потому что он довольно сложный. И будет генерировать много отвлечения. Случай, который я представляю выше, является максимально упрощенным. Под этим я подразумеваю, что вызов java put кажется намного медленнее, чем add () набора python.

Реализация производственного кода на Java также примерно в 2,5 - 3 раза медленнее, чем в версии Python, как и выше.

Меня не беспокоит разогрев или запуск виртуальной машины. Я просто хочу сравнить код моего startTime с моим totalTime. Пожалуйста, не занимайтесь другими вопросами.

Я инициализировал хэш-набор с более чем достаточным количеством сегментов, чтобы его никогда не приходилось перефразировать. (Я всегда буду заранее знать, сколько элементов в конечном итоге будет содержать коллекция.) Полагаю, можно утверждать, что я должен был инициализировать его до итераций / 0.75. Но если вы попробуете это, вы увидите, что время выполнения не сильно изменится.

Я установил Xmx1024m для любопытных (на моей машине 4 ГБ оперативной памяти).

Я использую версию Java: среда выполнения Java (TM) SE (сборка 1.6.0_13-b03).

В рабочей версии я храню строку (2-15 символов) в hashSet, поэтому я не могу использовать примитивы, хотя это интересный случай.

Я запускал код много-много раз. Я очень уверен, что код Python в 2,5-3 раза быстрее, чем код Java.

Ответы [ 20 ]

21 голосов
/ 28 мая 2009

Вы на самом деле не тестируете Java против Python, вы тестируете java.util.HashSet, используя целочисленные значения в автоматическом ящике, а также собственный набор Python и обработку целочисленных значений.

Очевидно, что сторона Python в этом микробенчмарке действительно быстрее.

Я попытался заменить HashSet на TIntHashSet из GNU trove и достиг коэффициента ускорения между 3 и 4, в результате чего Java немного опередила Python.

Реальный вопрос в том, действительно ли ваш пример кода является таким же репрезентативным, как вы думаете. Запустили ли вы профилировщик и определили, что большая часть процессорного времени тратится на помещение огромного числа целых чисел в HashSet? Если нет, то пример не имеет значения. Даже если единственное отличие состоит в том, что в вашем производственном коде хранятся другие объекты, кроме целых, их создание и вычисление их хеш-кода могут легко доминировать при вставке набора (и полностью уничтожить преимущество Python в обработке целых чисел специально), что делает весь этот вопрос бессмысленным. *

12 голосов
/ 28 мая 2009

Я подозреваю, что Python сам использует целочисленное значение в качестве своего хэша, и реализация set на основе хеш-таблицы использует это значение напрямую. Из комментариев в источнике :

Это не обязательно плохо! Наоборот, в таблице размером 2 ** я, принимая младшие биты i в качестве начального индекса таблицы чрезвычайно быстры, и там нет столкновений вообще для диктов, проиндексированных непрерывным диапазоном целых чисел. То же самое примерно верно, когда ключи являются «последовательными» строками. Так это в большинстве случаев дает поведение лучше случайного, и это очень желательно.

Этот микробенчмарк - лучший пример для Python, потому что он приводит к абсолютно нулевым коллизиям хеша. Принимая во внимание, что если Javas HashSet перефразирует ключи, он должен выполнить дополнительную работу, а также столкнуться с гораздо худшим поведением при столкновениях.

Если вы сохраняете диапазон (итерации) во временной переменной и выполняете его в random.shuffle перед циклом, время выполнения будет более чем в 2 раза медленнее, даже если создание случайных чисел и списков выполняется вне цикла.

7 голосов
/ 28 мая 2009

Другое возможное объяснение состоит в том, что наборы в Python реализованы изначально в коде C, в то время как HashSet в Java реализованы в самой Java. Таким образом, наборы в Python должны быть намного быстрее.

7 голосов
/ 28 мая 2009

Мой опыт, как правило, заключается в том, что программы на Python работают быстрее, чем программы на Java, несмотря на тот факт, что Java немного более низкого уровня. Кстати, оба языка скомпилированы в байтовый код (это и есть те файлы .pyc - вы можете думать о них как о файлах типа .class). Оба языка интерпретируются как байт-код на машине с виртуальным стеком.

Вы ожидаете, что python будет медленнее, например, a.b. В java это a.b преобразуется в разыменование. С другой стороны, Python должен выполнить один или несколько поисков в хеш-таблице: проверить локальную область, проверить область модуля, проверить глобальную область, проверить встроенные функции.

С другой стороны, java общеизвестно плох в определенных операциях, таких как создание объектов (что, вероятно, является виновником в вашем примере) и сериализация.

В общем, простого ответа нет. Я не ожидал бы, что какой-либо язык будет быстрее для всех примеров кода.

Исправление: несколько человек отметили, что Java больше не так уж плоха при создании объектов. Итак, в вашем примере это что-то еще. Возможно, это автобокс, это дорого, возможно, алгоритм хеширования по умолчанию в Python лучше в этом случае. Из моего практического опыта, когда я переписываю Java-код на Python, я всегда вижу повышение производительности, но это может быть связано как с языком, так и с переписыванием в целом, что приводит к повышению производительности.

6 голосов
/ 28 мая 2009

Я бы хотел развеять пару мифов, которые я видел в ответах:

Java скомпилирована, да, для байт-кода, но в конечном итоге для собственного кода в большинстве сред времени выполнения. Люди, которые говорят, что C по своей природе быстрее, не рассказывают всю историю, я мог бы привести случай, когда байтовые скомпилированные языки по своей природе быстрее, потому что JIT-компилятор может делать машинно-специфические оптимизации, которые недоступны компиляторам с опережением времени.

Некоторые вещи, которые могут иметь значение:

  • Хэш-таблицы и наборы Python являются наиболее сильно оптимизированными объектами в Python, а хэш-функция Python предназначена для возврата аналогичных результатов для аналогичных входных данных: хеширование целого числа просто возвращает целое число, гарантируя, что вы НИКОГДА не увидите столкновения в хеш-коде. таблица последовательных целых чисел в Python.
  • Вторичный эффект вышеперечисленного заключается в том, что код Python будет иметь высокую локальность ссылок, поскольку вы будете последовательно обращаться к хеш-таблице.
  • Java делает некоторые необычные упаковки и распаковки целых чисел, когда вы добавляете их в коллекции. Что касается бонусов, это делает арифметику намного быстрее в Java, чем в Python (при условии, что вы держитесь подальше от bignums), но с другой стороны это означает больше выделений, чем вы привыкли.
5 голосов
/ 28 мая 2009

Редактировать: TreeSet может быть быстрее для реального случая использования, в зависимости от шаблонов распределения. Мои комментарии ниже касаются только этого упрощенного сценария. Тем не менее, я не верю, что это будет иметь очень важное значение. Настоящая проблема лежит в другом месте.

Несколько человек здесь рекомендовали заменить HashSet на TreeSet. Это звучит как очень странный совет для меня, поскольку нет никакой возможности, чтобы структура данных со временем вставки 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

Более того, создание большого количества объектов приводит к дополнительным издержкам сборщика мусора. Это становится значительным, когда вы начинаете хранить в памяти десятки миллионов живых объектов.

4 голосов
/ 28 мая 2009

Я считаю такие тесты бессмысленными. Я не решаю проблемы, которые похожи на контрольный пример. Это не очень интересно.

Я бы предпочел увидеть решение для значимого решения линейной алгебры с использованием NumPy и JAMA. Может быть, я попробую и сообщу с результатами.

3 голосов
/ 28 мая 2009

Здесь есть ряд вопросов, которые я хотел бы объединить.

Во-первых, если это программа, которую вы собираетесь запускать только один раз, имеет ли значение, что это займет дополнительные несколько секунд?

Во-вторых, это всего лишь один микробенчмарк. Микробенчмарки не имеют смысла сравнивать производительность.

При запуске возникает ряд проблем.

Среда выполнения Java намного больше, чем Python, поэтому загрузка с диска занимает больше времени и занимает больше памяти, что может быть важно при замене.

Если вы не установили -Xms, возможно, вы используете GC только для изменения размера кучи. Также возможно, что в начале правильно настроена куча.

Это правда, что Java начинает с интерпретации, а затем компилирует. Около 1500 итераций для горячей точки клиента Sun [C1] и 10000 для сервера [C2]. Точка доступа к серверу в конечном итоге даст вам лучшую производительность, но потребует больше памяти. Мы можем видеть, что клиентская Hotspot использует сервер для очень часто выполняемого кода для лучшего из обоих миров. Однако обычно это не должно быть вопросом секунд.

Самое главное, вы можете создавать два объекта за итерацию. Для большей части кода вы не будете создавать эти крошечные объекты для такой доли выполнения. TreeSet может быть лучше по количеству объектов, с 6u14 и Harmony становится еще лучше.

Возможно, Python выигрывает, сохраняя небольшие целочисленные объекты в ссылках, вместо того, чтобы фактически иметь объект. Это, несомненно, хорошая оптимизация.

Проблема с множеством тестов заключается в том, что вы смешиваете много разного кода в одном методе. Вы бы не написали код, который вас так заботил, не так ли? Так почему же вы пытаетесь протестировать производительность, которая отличается от кода, который вы на самом деле хотели бы запустить быстро?

Улучшенная структура данных: что-то вроде BitSet, похоже, имеет смысл (хотя на нем есть синхронизация, которая может влиять или не влиять на производительность).

3 голосов
/ 28 мая 2009

Я не слишком знаком с python, но знаю, HashSet не может содержать примитивы, поэтому, когда вы говорите counts.add(i), i автоматически попадает в вызов new Integer(i). Это, вероятно, ваша проблема.

Если по какой-то причине вам действительно нужен «набор» целых чисел от 0 до некоторого большого n, его, вероятно, лучше всего объявить как «boolean [] set = new boolean [n]». Затем вы можете просмотреть массив и пометить элементы, которые находятся в наборе, как 'true', не неся при этом затрат на создание n объектов-оберток Integer. Если вы хотите пойти дальше, вы можете использовать байт [] размера n / 8 и напрямую использовать отдельные биты. Или, возможно, BigInteger.

EDIT

Хватит голосовать за мой ответ. Это не правильно.

РЕДАКТИРОВАТЬ

Нет, правда, это неправильно. Я получаю сопоставимую производительность, если я делаю то, что предлагает вопрос, заполняя набор N целыми числами. если я заменю содержимое цикла for следующим образом:

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

Тогда это займет всего 2 секунды. Если вы вообще не храните Integer, тогда это займет менее 200 миллис. Принудительное выделение 10000000 целочисленных объектов требует некоторого времени, но, похоже, большая часть времени проводится внутри операции put HashSet.

2 голосов
/ 28 мая 2009

Используете ли вы флаг -server с jvm? Вы не можете проверить производительность без него. (Вы также должны прогреть JVM перед выполнением теста.)

Кроме того, вы, вероятно, хотите использовать TreeSet<Integer>. HashSet будет медленнее в долгосрочной перспективе.

А какой JVM вы используете? Надеюсь, самое новое.

EDIT

Когда я говорю, используйте TreeSet, я имею в виду, в общем, не для этого теста. TreeSet решает реальную проблему неравномерного хеширования объектов. Если вы получите слишком много объектов в одном и том же контейнере в HashSet, вы будете выполнять примерно O (n).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...