Чи є технічні обмеження чи мовні функції, які не дозволяють моєму сценарію Python бути таким же швидким, як еквівалентна програма C ++?


10

Я давній користувач Python. Кілька років тому я почав вивчати C ++, щоб побачити, що він може запропонувати з точки зору швидкості. Протягом цього часу я б продовжував використовувати Python як інструмент для прототипування. Це, здавалося, була гарною системою: спритний розвиток з Python, швидке виконання в C ++.

Останнім часом я все більше і більше використовую Python і вчуся, як уникнути всіх підводних каменів та анти-шаблонів, якими я швидко користувався мовою в попередні роки. Я розумію, що використання певних функцій (розуміння списків, перерахування тощо) може підвищити продуктивність.

Але чи існують технічні обмеження чи мовні функції, які не дозволяють моєму сценарію Python бути таким же швидким, як еквівалентна програма C ++?


2
Так, це може. Дивіться PyPy про стан сучасних у компіляторах Python.
Грег Хьюгілл

5
Усі змінні в python є поліморфними, тобто тип змінної відомий лише під час виконання. Якщо ви бачите (припускаючи цілі числа) x + y в мовах, подібних С, вони роблять ціле додавання. У python відбудеться перемикання типів змінних на x і y, а потім буде обрана відповідна функція додавання, а потім буде перевірка переповнення, а потім є додавання. Якщо пітон не навчиться статичному набору, цей наклад ніколи не піде.
nwp

1
@nwp Ні, це легко, див. PyPy. Більш складними, досі відкритими проблемами є: Як подолати затримку запуску компіляторів JIT, як уникнути виділень для складних довготривалих об'єктних графіків та як добре використовувати кеш загалом.

Відповіді:


11

Я якось потрапив у цю стіну сам, коли пару років тому я взяв на роботу програмування Python. Мені подобається Python, я справді це роблю, але коли я почав робити певну настройку продуктивності, у мене були деякі грубі потрясіння.

Суворі піфоністи можуть мене виправити, але ось речі, які я знайшов, намальовані дуже широкими штрихами.

  • Використання пам'яті 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
Рядок "a" не є хорошим прикладом для першої точки кулі. Рядок Java також має значні накладні витрати для одиночних символьних рядків, але це постійні накладні витрати, які досить добре амортизуються, оскільки рядок зростає в довжину (один-чотири байти або символи в залежності від версії, параметрів побудови та вмісту рядків). Ти маєш рацію щодо визначених користувачем об'єктів, хоча б тих, які не використовують __slots__. PyPy повинен зробити набагато краще в цьому плані, але я не знаю достатньо, щоб судити.

1
Друга проблема, яку ви вказуєте, стосується лише конкретної реалізації та не властива мові. Перша проблема вимагає пояснення: те, що "важить" 28 байт, - це не сам символ, а те, що він упакований у рядок класу, який має власні методи та властивості. Представлення одного символу як масив байтів (буквальний b'a ') "важить лише 18 байт на Python 3.3, і я впевнений, що існує більше способів оптимізувати зберігання символів у пам'яті, якщо вашій програмі це справді потрібно.
Червоний

C # може компілюватись на власному рівні (наприклад, майбутні MS-технології, Xamarin для iOS).
День

13

Реалізація посилання Python є інтерпретатором "CPython". Він намагається бути досить швидким, але в даний час не використовує розширених оптимізацій. І для багатьох сценаріїв використання це добре: компіляція до деякого посередницького коду відбувається безпосередньо перед початком виконання, і кожен раз, коли програма виконується, код збирається заново. Таким чином, час, необхідний для оптимізації, повинен бути співставлений з часом, отриманим оптимізаціями - якщо чистий приріст відсутній, оптимізація марна. Для дуже давно запущеної програми або програми з дуже тугими петлями, корисним буде використання розширених оптимізацій. Однак CPython використовується для деяких завдань, які виключають агресивну оптимізацію:

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

  • 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 з синтаксисом Pythonic". Якщо ви перейдете на PyPy, ваш ванільний код Python отримає значне збільшення швидкості, але також не зможе взаємодіяти з розширеннями, написаними на C або C ++.

Але які властивості або особливості перешкоджають ванільному Python досягти С-подібних рівнів продуктивності, окрім довгого часу запуску?

  • Вкладники та фінансування. На відміну від 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 ++ не є ідеальним: через структуроподібний характер навіть приватні поля належать до публічного інтерфейсу об'єкта.)

  • Збір сміття. У багатьох випадках ГК можна було уникнути повністю. C ++ дозволяє статично виділити об'єкти , які знищуються автоматично , коли поточна область залишається: Type instance(args);. До цього часу об’єкт живий і його можна позичати на інші функції. Зазвичай це робиться за допомогою "прохідного посилання". Такі мови, як Rust, дозволяють компілятору статично перевіряти, що жоден вказівник на такий об’єкт не перевищує термін експлуатації об'єкта. Ця схема управління пам'яттю є повністю передбачуваною, високоефективною і підходить у більшості випадків без складних графіків об'єктів. На жаль, Python не розроблявся з урахуванням управління пам'яттю. Теоретично аналіз втечі може бути використаний для пошуку випадків, коли ГК можна уникнути. На практиці це прості ланцюжки методів, такі якfoo().bar().baz() доведеться виділити велику кількість недовговічних об’єктів на купі (генераційний GC - це один із способів зберегти цю проблему невеликою).

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

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

    • Упаковані масиви конкретного нативного типу можна створити в Python через arrayвбудований модуль. Також numpyпропонує ефективні подання буферів даних із конкретними формами для нативних числових типів.

Підсумок

Python був розроблений для зручності використання, а не для продуктивності. Його дизайн робить створення високоефективної реалізації досить важким. Якщо програміст утримується від проблемних особливостей, то компілятор, що розуміє ідіоми, що залишилися, зможе видавати ефективний код, який може конкурувати з C по продуктивності.


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) поширюється на ці мови, вони також наближаються (як правило, до 2x) швидкості добре написаного коду C / C ++.

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


"Пам'ятайте, що код бібліотеки часу виконання може бути написаний тією ж мовою, що і ваші програми, і мати однакові обмеження продуктивності." і "Для Python / Ruby / Perl вартість усіх трьох цих факторів відносно висока. Подумайте, у 5 разів порівняно з C / C ++ або гірше." Власне, це неправда. Наприклад, Hashклас Rubinius (одна з основних структур даних у Ruby) написаний у Ruby, і він працює порівняно, іноді навіть швидше, ніж Hashклас YARV, написаний у C. І однією з причин є те, що великі частини часу виконання Rubinius Система написана в Ruby, щоб вони могли…
Jörg W Mittag

... наприклад, накреслить компілятор Рубіній. Надзвичайними прикладами є Клейн VM (метациркулярний VM для самоврядування) та Maxine VM (метациркулярний VM для Java), де написано все , навіть код відправлення методу, збирач сміття, розподільник пам'яті, примітивні типи, основні структури даних та алгоритми. Self або Java. Таким чином, навіть частини основної VM можуть бути вбудовані в код користувача, і VM може перекомпілювати і переоптимізувати себе, використовуючи зворотний зв'язок під час виконання програми з користувацької програми.
Йорг W Міттаг

@ JörgWMittag: Все ще правда. Рубіній має JIT, а JIT-код часто перемагає C / C ++ на окремих еталонах. Я не можу знайти жодних доказів того, що цей метациркулярний матеріал робить багато для швидкості за відсутності JIT. [Див. Редагування для наочності щодо JIT.]
david.pfx

1

Але чи існують технічні обмеження чи мовні функції, які не дозволяють моєму сценарію Python бути таким же швидким, як еквівалентна програма C ++?

Ні. Це лише питання грошей і ресурсів, що виливаються в те, щоб C ++ працював швидко проти грошей і ресурсів, що вкладаються в те, щоб Python швидко пробіг.

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

Але тоді Sun скасував проект Self (зріла мова загального призначення для розробки великих систем), щоб зосередитись на невеликій мові сценаріїв для анімованих меню у телевізорах (ви, можливо, чули про це, це називається Java), не було більше фінансування. У той же час Intel, IBM, Microsoft, Sun, Metrowerks, HP та ін. витратили величезні гроші та ресурси, роблячи C ++ швидкими. Виробники процесорів додали функції до своїх чіпів, щоб зробити C ++ швидким. Операційні системи були написані або модифіковані для швидкого C ++. Отже, C ++ швидкий.

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

Ніщо не властиве 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.