Существуют ли технические ограничения или языковые функции, которые мешают моему скрипту Python работать так же быстро, как эквивалентная программа на C ++?


10

Я давний пользователь Python. Несколько лет назад я начал изучать C ++, чтобы посмотреть, что он может предложить с точки зрения скорости. В течение этого времени я продолжал использовать Python как инструмент для создания прототипов. Казалось, это была хорошая система: гибкая разработка на Python, быстрое выполнение на C ++.

В последнее время я все больше и больше использую Python и узнаю, как избежать всех ловушек и анти-паттернов, которые я быстро использовал в ранние годы работы с языком. Насколько я понимаю, использование определенных функций (списки, перечисления и т. Д.) Может повысить производительность.

Но существуют ли технические ограничения или языковые функции, которые мешают моему скрипту Python работать так же быстро, как эквивалентная программа на C ++?


2
Да, оно может. Смотрите PyPy для современного состояния компиляторов Python.
Грег Хьюгилл

5
Все переменные в python полиморфны, то есть тип переменной известен только во время выполнения. Если вы видите (предполагая целые числа) x + y в C-подобных языках, они делают целочисленное сложение. В python произойдет переключение типов переменных на x и y, затем будет выбрана соответствующая функция сложения, затем будет проверка переполнения, а затем добавление. Пока Python не научится статической типизации, эти издержки никогда не исчезнут.
NWP

1
@nwp Нет, это легко, см. PyPy. Более сложные, но все еще открытые проблемы: как преодолеть задержку запуска JIT-компиляторов, как избежать выделения для сложных долгоживущих графов объектов и как эффективно использовать кэш в целом.

Ответы:


11

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

Строгие Pythonistas могут исправить меня, но вот вещи, которые я нашел, нарисованные очень широкими мазками.

  • Использование памяти Python довольно страшно. Python представляет все как диктовку - что чрезвычайно мощно, но в результате получается, что даже простые типы данных являются гигантскими. Я помню, что символ «а» занял 28 байт памяти. Если вы используете большие структуры данных в Python, убедитесь, что вы полагаетесь на numpy или scipy, потому что они поддерживаются прямой реализацией байтового массива.

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

  • Python имеет глобальную блокировку интерпретатора, что означает, что в большинстве случаев процессы выполняются однопоточными. Могут быть библиотеки, которые распределяют задачи по процессам, но мы раскручивали 32 или около того экземпляра нашего скрипта на python и запускали каждый отдельный поток.

Другие могут общаться с моделью исполнения, но Python - это время компиляции, а затем интерпретируемое, что означает, что он не доходит до машинного кода. Это также влияет на производительность. Вы можете легко связать модули C или C ++ или найти их, но если вы просто запустите Python, это приведет к снижению производительности.

Теперь в тестах веб-сервисов Python выгодно отличается от других языков компиляции во время выполнения, таких как Ruby или PHP. Но это довольно далеко позади большинства компилируемых языков. Даже языки, которые компилируются на промежуточный язык и работают на виртуальной машине (например, Java или C #), делают намного лучше.

Вот действительно интересный набор тестов, на которые я иногда ссылаюсь:

http://www.techempower.com/benchmarks/

(Несмотря на это, я до сих пор очень люблю Python, и если у меня есть возможность выбрать язык, на котором я работаю, это мой первый выбор. В большинстве случаев я не ограничен сумасшедшими требованиями к пропускной способности в любом случае.)


2
Строка «а» не является хорошим примером для первой точки маркера. Строка Java также имеет значительные накладные расходы для односимвольных строк, но это постоянные накладные расходы, которые амортизируются довольно хорошо по мере увеличения длины строки (от одного до четырех байтов символов в зависимости от версии, параметров компоновки и содержимого строки). Вы правы в отношении пользовательских объектов, хотя бы те, которые не используют __slots__. PyPy должен быть намного лучше в этом отношении, но я не знаю достаточно, чтобы судить.

1
Вторая проблема, на которую вы указываете, связана только с конкретной реализацией, а не присуща языку. Первая проблема требует объяснения: то, что «весит» 28 байт, это не сам символ, а тот факт, что он упакован в строковый класс с собственными методами и свойствами. Представление одного символа в виде байтового массива (литерал b'a ') «только» весит 18 байт на Python 3.3, и я уверен, что есть больше способов оптимизировать хранение символов в памяти, если ваше приложение действительно нуждается в этом.
Красный

C # может компилироваться изначально (например, предстоящая технология MS, Xamarin для iOS).
День

13

Ссылочная реализация Python является интерпретатором «CPython». Он пытается быть достаточно быстрым, но в настоящее время он не использует передовые оптимизации. И для многих сценариев использования это хорошая вещь: компиляция некоторого промежуточного кода происходит непосредственно перед временем выполнения, и каждый раз, когда программа выполняется, код компилируется заново. Таким образом, время, необходимое для оптимизации, должно быть сопоставлено со временем, полученным оптимизацией - если нет чистого выигрыша, оптимизация не имеет смысла. Для очень долго работающей программы или программы с очень узкими циклами было бы полезно использовать расширенную оптимизацию. Однако CPython используется для некоторых заданий, которые исключают агрессивную оптимизацию:

  • Краткосрочные сценарии, используемые, например, для задач sysadmin. Многие операционные системы, такие как Ubuntu, строят значительную часть своей инфраструктуры поверх Python: CPython достаточно быстр для работы, но практически не имеет времени запуска. Пока это быстрее, чем bash, это хорошо.

  • CPython должен иметь четкую семантику, поскольку он является эталонной реализацией. Это позволяет выполнять простые оптимизации, такие как «оптимизировать реализацию оператора foo» или «компилировать списки для более быстрого байт-кода», но, как правило, исключает оптимизацию, которая уничтожает информацию, такую ​​как встроенные функции.

Конечно, существует больше реализаций Python, чем просто CPython:

  • Jython построен поверх JVM. JVM может интерпретировать или JIT-компилировать предоставленный байт-код и имеет оптимизированные профили. Он страдает от высокого времени запуска и занимает некоторое время, пока не вступит в силу JIT.

  • PyPy - это современное состояние JITting Python VM. PyPy написан на RPython, ограниченном подмножестве Python. Это подмножество удаляет некоторую выразительность из Python, но позволяет статически выводить тип любой переменной. Виртуальная машина, написанная на RPython, может быть перенесена на C, что дает производительность, подобную RPython C. Однако RPython по-прежнему более выразителен, чем C, что позволяет быстрее разрабатывать новые оптимизации. PyPy является примером начальной загрузки компилятора. PyPy (не RPython!) В основном совместим с эталонной реализацией CPython.

  • Cython - это (как RPython) несовместимый диалект Python со статической типизацией. Он также переносится в C-код и может легко генерировать C-расширения для интерпретатора CPython.

Если вы готовы перевести свой код Python на Cython или RPython, то вы получите производительность, подобную C. Однако их следует понимать не как «подмножество Python», а как «C с Python-синтаксисом». Если вы переключитесь на PyPy, ваш обычный код Python получит значительное повышение скорости, но также не сможет взаимодействовать с расширениями, написанными на C или C ++.

Но какие свойства или функции препятствуют достижению ванильным Python уровня производительности, подобного C, помимо продолжительного времени запуска?

  • Авторы и финансирование. В отличие от Java или C #, за языком нет ни одной движущей компании, заинтересованной в том, чтобы сделать этот язык лучшим в своем классе. Это ограничивает развитие в основном добровольцами и случайными грантами.

  • Позднее связывание и отсутствие какой-либо статической типизации. Python позволяет нам писать дерьмо так:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding
    

    В Python любая переменная может быть переназначена в любое время. Это предотвращает кэширование или вставку; любой доступ должен проходить через переменную. Эта косвенность снижает производительность. Конечно, если ваш код не делает таких безумных вещей, так что каждой переменной перед компиляцией может быть присвоен определенный тип, и каждая переменная присваивается только один раз, то - теоретически - можно выбрать более эффективную модель выполнения. Язык, помнящий об этом, обеспечит некоторый способ помечать идентификаторы как константы и, по крайней мере, разрешать необязательные аннотации типов («постепенная типизация»).

  • Сомнительная объектная модель. Если слоты не используются, сложно определить, какие поля у объекта (объект Python - это, по сути, хеш-таблица полей). И даже когда мы там, мы все еще не знаем, какие типы имеют эти поля. Это предотвращает представление объектов в виде плотно упакованных структур, как в случае с C ++. (Конечно, представление объектов в C ++ также не идеально: из-за структуроподобной природы даже частные поля принадлежат общедоступному интерфейсу объекта.)

  • Вывоз мусора. Во многих случаях GC можно полностью избежать. C ++ позволяет статически выделить объекты , которые уничтожаются автоматически , когда текущая область остается: Type instance(args);. До тех пор объект является живым и может быть передан другим функциям. Обычно это делается с помощью «передачи по ссылке». Такие языки, как Rust, позволяют компилятору статически проверять, чтобы ни один указатель на такой объект не превышал время жизни объекта. Эта схема управления памятью полностью предсказуема, высокоэффективна и подходит для большинства случаев без сложных графов объектов. К сожалению, Python не был разработан с учетом управления памятью. Теоретически, анализ побега может быть использован, чтобы найти случаи, когда GC можно избежать. На практике простые цепочки методов, такие какfoo().bar().baz() придется выделить большое количество недолговечных объектов в куче (генерация GC - один из способов сохранить эту проблему небольшой).

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

    • Списки определенного размера могут быть созданы как fixed_size = [None] * size. Однако память для объектов внутри этого списка должна быть выделена отдельно. Контраст C ++, где мы можем сделать std::array<Type, size> fixed_size.

    • Упакованные массивы определенного нативного типа могут быть созданы в Python через arrayвстроенный модуль. Кроме того, numpyпредлагает эффективные представления буферов данных с определенными формами для собственных числовых типов.

Резюме

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


8

Да. Основная проблема заключается в том, что язык определен как динамический, то есть вы никогда не знаете, что делаете, пока не собираетесь это сделать. Это делает его очень трудно производить эффективный машинный код, потому что вы не знаете , что производит машинный код для . JIT-компиляторы могут выполнять некоторую работу в этой области, но это никогда не сравнимо с C ++, потому что JIT-компилятор просто не может тратить время и память на запуск, так как это время и память, которую вы не тратите на выполнение своей программы, и существуют жесткие ограничения на то, что они могут достичь без нарушения динамической семантики языка.

Я не собираюсь утверждать, что это неприемлемый компромисс. Но для природы Python фундаментально, что реальные реализации никогда не будут такими быстрыми, как реализации C ++.


8

Есть три основных фактора, которые влияют на производительность всех динамических языков, некоторые больше, чем другие.

  1. Пояснительные накладные расходы. Во время выполнения есть какой-то байт-код, а не машинные инструкции, и для выполнения этого кода есть фиксированные накладные расходы.
  2. Отправка накладных. Цель для вызова функции неизвестна до времени выполнения, и выяснение того, какой метод вызывать, требует затрат.
  3. Затраты на управление памятью. Динамические языки хранят вещи в объектах, которые должны быть выделены и освобождены, что влечет за собой снижение производительности.

Для C / C ++ относительные затраты этих трех факторов практически равны нулю. Инструкции выполняются непосредственно процессором, диспетчеризация занимает не более одной или двух косвенных операций, куча памяти никогда не выделяется, если вы так не говорите. Хорошо написанный код может подойти на ассемблере.

Для C # / Java с JIT-компиляцией первые два являются низкими, но сборка мусора имеет свою стоимость. Хорошо написанный код может приближаться к 2x C / C ++.

Для Python / Ruby / Perl стоимость всех этих трех факторов относительно высока. Подумайте 5 раз по сравнению с C / C ++ или хуже. (*)

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


(*) Поскольку компиляция Just-In_Time (JIT) распространяется на эти языки, они также приблизятся (обычно в 2 раза) к скорости хорошо написанного кода C / C ++.

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


«Помните, что код библиотеки времени выполнения вполне может быть написан на том же языке, что и ваши программы, и иметь те же ограничения производительности». и «Для Python / Ruby / Perl стоимость всех этих трех факторов относительно высока. Подумайте в 5 раз по сравнению с C / C ++ или хуже». На самом деле это не так. Например, Hashкласс Rubinius (одна из основных структур данных в Ruby) написан на Ruby, и он работает сравнимо, иногда даже быстрее, чем Hashкласс YARV, который написан на C. И одна из причин заключается в том, что большие части времени выполнения Rubinius Система написана на Ruby, так что они могут ...
Йорг W Mittag

... например, должен быть встроен компилятором Rubinius. Экстремальными примерами являются Кляйн В.М. (мета-круговая ВМ для себя) и Максин В.М. (мета-круговая ВМ для Java), где все , даже код диспетчеризации методов, сборщик мусора, распределитель памяти, примитивные типы, базовые структуры данных и алгоритмы, написаны на Я или Ява. Таким образом, даже части базовой виртуальной машины могут быть встроены в код пользователя, и виртуальная машина может перекомпилировать и повторно оптимизировать себя, используя обратную связь во время выполнения от пользовательской программы.
Йорг Миттаг,

@ JörgWMittag: все еще правда. У Рубиниуса есть JIT, и код JIT часто превосходит C / C ++ по отдельным тестам. Я не могу найти никаких доказательств того, что этот метациркулярный материал много делает для скорости в отсутствие JIT. [См.
Правку

1

Но существуют ли технические ограничения или языковые функции, которые мешают моему скрипту Python работать так же быстро, как эквивалентная программа на C ++?

Нет. Это просто вопрос денег и ресурсов, направленных на то, чтобы заставить C ++ работать быстро, по сравнению с деньгами и ресурсами, которые заставляют Python работать быстро.

Например, когда вышла Self VM, это был не только самый быстрый динамический язык OO, это был самый быстрый период языка OO. Несмотря на то, что он был невероятно динамичным языком (например, гораздо лучше, чем Python, Ruby, PHP или JavaScript), он был быстрее, чем большинство доступных реализаций C ++.

Но затем Sun отменила проект Self (зрелый OO-язык общего назначения для разработки больших систем), чтобы сосредоточиться на небольшом языке сценариев для анимированных меню в телевизионных приставках (вы, возможно, слышали об этом, он называется Java), не было больше финансирования. В то же время Intel, IBM, Microsoft, Sun, Metrowerks, HP и соавт. потратили огромные деньги и ресурсы на быстрое создание C ++. Производители процессоров добавили функции в свои чипы, чтобы сделать C ++ быстрым. Операционные системы были написаны или изменены, чтобы сделать C ++ быстрым. Итак, С ++ работает быстро.

Я не очень хорошо знаком с Python, я скорее специалист по Ruby, поэтому приведу пример из Ruby: Hashкласс (эквивалентный по функции и важности dictв Python) в реализации Rubinius Ruby написан на 100% чистом Ruby; тем не менее, он конкурирует выгодно, а иногда даже превосходит Hashкласс в YARV, который написан на оптимизированном вручную языке C. И по сравнению с некоторыми из коммерческих систем Lisp или Smalltalk (или вышеупомянутой Self VM), компилятор Rubinius даже не настолько умен ,

В Python нет ничего, что могло бы замедлить работу. В современных процессорах и операционных системах есть функции, которые наносят вред Python (например, известно, что виртуальная память ужасна для производительности сборки мусора). Существуют функции, которые помогают C ++, но не помогают Python (современные процессоры стараются избегать промахов кэша, потому что они очень дороги. К сожалению, избежать промахов кэша сложно, если у вас OO и полиморфизм. Скорее, вы должны снизить стоимость кэша Процессор Azul Vega, который был разработан для Java, делает это.)

Если вы тратите столько же денег, исследований и ресурсов на создание Python быстро, как это было сделано для C ++, и вы тратите столько же денег, исследований и ресурсов на создание операционных систем, которые делают программы Python быстрыми, как это было сделано для C ++, и вы тратите много денег, исследований и ресурсов на создание процессоров, которые делают программы на Python быстрыми, как это было сделано для C ++, тогда я не сомневаюсь, что Python сможет достичь производительности, сопоставимой с C ++.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.