Блог пользователя IlyaCk

Автор IlyaCk, история, 5 лет назад, По-русски

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

Сразу уточню, что вопрос меня интересует не столько в применении к CF, сколько в разрезах "как правильно настраивать ту копию ejudge, которую я со-админю" и "что делать мне как тренеру, когда есть ученики, которые лучше всего знают Python (и непонятно, стОит ли заставлять/убеждать их выучить C++), которых надо хоть как-то подготовить к вряд ли победному, но хоть не слишком провальному выступлению на некоем (скажем, областном) туре, проводимом на централизованном (не моём) сервере".

По моим наблюдениям, В_СРЕДНЕМ Python оказывается медленнее C++ в 1,2--3 раза, и это относительно терпимо, если ставить time limit в 3--5 раз выше времени авторского решения.

Однако, ИНОГДА (в частности, в задачах, где очень-очень много используется операция %, называемая также mod) Python оказывается медленнее C++ в добрых 10--20 раз. Увеличивать TL аж настолько я не_хочу, даже когда имею такую административную (техническую) возможность. (Или, может, дело не в mod... Если кто-то (значительно более, чем я, шарящий) захочет помочь, начиная с выяснения первопричины -- дам права на своей копии ejudge; judge set -- вообще без проблем, более глубокие -- смотря по обстоятельствам.)

Настолько резкие проседания на конкретных задачах -- они вообще лечатся на стороне сервера? Если да, то как? (вроде есть какой-то специальный Python? Кажется, pypy? А он нормально взаимодействует с ejudge? есть какие-то особенности, которые надо изучить перед тем, как его ставить?) И принято ли их лечить на именно тех серверах, которые используются на украинских областных (в централизованном варианте) и финальных этапах? (Не_окажутся ли усилия по нивелированию этой проблемы на нашем сервере жесточайшей медвежьей услугой тем участникам, которые потом будут писать на другом сервере?)

  • Проголосовать: нравится
  • +52
  • Проголосовать: не нравится

»
5 лет назад, # |
Rev. 3   Проголосовать: нравится +27 Проголосовать: не нравится

TL;DR Конкретно для спортивного программирования CPython (стандартный интерпретатот Патона) медленный, PyPy в несколько раз быстрее CPython, но оба всё еще проигрывают C++.

Почему CPython "так плох"?

CPython — это интерпретатор, что значит, что при каждом запуске программы ему приходится токенизировать исходный код, потом строить AST и т.д. — помимо непосредственно выполнения программы. Уже из-за одного этого факта получаем время старта предельно простой программы на Python примерно на уровне 20мс — не очень много, но не прям "незаметно" (особенно на фоне TL в, скажем, 100мс). Как только мы подключаем какой-то модуль — скажем, import itertools — незамедлительно получим увеличение времени старта на 10-15мс. Это не касается модулей, написаных на Си (math, к примеру) — их подключение влечёт увеличение времени выполнения в худшем случае на пару милисекунд.

Кроме того, с точки зрения интерпретатора, любая переменная (даже примитив — число или булево значение) является объектом. Даже функция! А для объекта приходится хранить и ссылку на тип, и ссылку на непосредственно данные, и много-много прочего. Целое число "0" занимает 24 байт в CPython, целое "1" — уже 28 (как и любое другое натуральное число до 2**30 - 1). Очевидно, что это немыслемое расточительство, поэтому в CPython хранится таблица с использованными значениями целых чисел — что значит, что если у вас недавно уже была где-то "77" в качестве результата чего-либо, то новая "77" будет ссылкой на тот же объект. Получается, что памяти на "старые" числа мы используем поменьше (ну, как указатель 32/64-битный указатель), но жертвуем при этом быстродействием — нам ведь для каждого нового числа придётся искать в таблице "а не было ли у нас уже такого числа". Реализация, само собой, отличается от "просто таблицы" — но это не существенно. Важно, что это спортивному программисту мало помагает.

Очевидно, что, работая с объектами, нам еще и приходится мириться с дополнительными расходами на работу с ними — к примеру, посчитать 1 + 2 — значит вызвать метод объекта для суммирования с другим объектом. А поскольку оба имеют тип int, то мы получим еще и дополнительные проверки "а длинное ли это число?", "а при суммировании не получится ли длинное число?" — спасибо нативной длинной арифметике :)

Можно приводить еще очень много примеров особенностей — от генерации range(n) списка из n чисел (UPD: касается Py2) до необходимости проверки существования оператора [] при каждом обращении к list — что особенно больно при наличии чего-то вроде двумерного списка.

Каким образом решает проблему PyPy?

Всё достаточно просто. Во-первых, PyPy не интерпретирует программу — он её, фактически, компилирует на лету (JIT). Из-за этого время старта у PyPy еще больше, чем у CPython. Лично у меня на ПК время запуска достаточно простой программы составляет примерно 150мс, а подключая тот же itertools — так и вовсе 210мс.

Однако, эта "100-милисекундная слабость" компенсируется действительно возросшим в разы быстродействием в обычных для спортивного программирования операциях. К примеру, $$$\sum_{i=1}^{10^8} (1 + 1/i)$$$ считается 20 секунд на CPython и "всего" 1.2с на PyPy. А вот, скажем, $$$\sum_{i=1}^{10^6}(gcd(i, \lfloor \frac{n}{i} \rfloor ))$$$ уже не настолько радует — 255мс PyPy против 474мс (но тут у PyPy половина времени ушла реально на запуск).

Если что, код вот:


n = int(input()) n = 10**n res = 0 def gcd(a, b): while a: a, b = b%a, a return b for i in range(1, n): res += gcd(i, n//i) print(res)

Что там с делением по модулю

Есть подозрение, что программа использовала деление по модулю в контексте "вывести остаток от деления ответа на 10^9 + 7". Если так, то тут у C++ явное преимущество, и вот почему. Прежде всего, по моему скромному опыту, операция умножения в несколько раз дешевле операции деления/взятия по модулю. Кроме того, просто посмотрите, как gcc оптимизирует деление по модулю при флаге компиляции -О2 — там нет никакого деления, только умножение! А если вместо 1'000'000'009 у нас вдруг степень двойки — то компилятор и вовсе оставляет всего лишь один "AND". Пайтон такого не умеет — у него, увы, умножение — всегда умножение (а никак не битовый сдвиг), а деление — всегда деление.

У С++ (как и у любого компилируемого языка) в спортивном программировании есть огромная фора — время компиляции никак не учитывается в оценивании (кроме того, что оно не должно превосходить условных 20 секунд — но это уже целая вечность!). Та оптимизация, которую я привёл — лишь одна из многих, которые компилятор способен сделать еще до запуска программы. Выкидывать вызовы функции, разворачивать циклы и многое другое — этого не сделаешь на лету за 100мс при интерпретации программы, как бы не хотелось.

По поводу медвежьей услуги... Я скажу очевидное: тут очень тонкий вопрос. На UOI существование решения на Python не гарантируется, это с одной стороны. С другой — заставлять переходить "питонистов" на C++ перед олимпиадой, принуждая набивать шишки на новом языке и потенциально создавать вероятность провала на олимпиаде из-за плохого знания тонкостей языка (а у плюсов их, очевидно, хватает) — тоже не очень бы хотелось.

  • »
    »
    5 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Спасибо, Матвей, за подробный ответ. Я о подобных вещах какое-то представление, конечно, имею, но полезно перечитать более-менее собранное в кучу.

    Но:

    1. А хоть что-то известно о практике отношения ко всему этому на школьном Всеукре, школьных централизованных областных, украинских студенческих?

    2. Есть ли какие-то описания особенностей того, как ставить pypy под ejudge? или там всё как-то элементарно? или как-то ужасно тяжело?

    3. Именно конкретно заявление, что range(n) генерирует list -- оно ведь, кажется, только для Python2, а не Python3? Или это неправда? Или хоть формально и неправда, но с точки зрения расхода ресурсов равносильно list-у?

    • »
      »
      »
      5 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится
      1. Я, признаться, слабо знаю, что там на IOI — возможно, arsijo поможет. Касаемо UOI — стандартная фраза "жюри не гарантирует существования решений на всех языках программирования". Хотя, субъективно, поддержка Python в этом году была чуть ли не лучшей за все UOI, в которых я так или иначе участвовал — были написаны решения в том или инном виде к большей части задач (может, и не наилучшие решения, но они были), ну и градеры тоже присутствовали (там, где они нужны). Однако всеобщая позиция по этому поводу примерно та, что я написал выше — "мы ничего не гаранитируем" (это часто упоминается в отношении паскаля, хотя чаще всего по поводу бедной стандартной библиотеки). По поводу PyPy — я не помню, на UOI он был в качестве интерпретатора (но я и не интересовался, признаться).

      2. Всё, что я нашел по этому поводу — ченжлог версии 3.1, где упоминают поддержку PyPy

      3. Да, я спутал с поведением Py2. Беглый взгляд на исходный код даёт повод думать, что ведёт он себя аналогично xrange в Py2 — память экономит аналогичным образом.

      • »
        »
        »
        »
        5 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Что там на IOI, конечно, тоже интересно; но скорее из общих соображений, чем практически важно. Более интересно, чего ждать на III (в~централизованной версии) и IV этапах UOI. (Однако, общие соображения/ обсуждения/ исследования/ информация могут быть полезны/важны и без привязки к IOI/UOI, и если кому-то есть что ещё сказать в целом — welcome.)

        "По поводу PyPy — я не помню, на UOI он был в качестве интерпретатора" — это о чём? Что он был, но с отключенной той возможностью, которая делает его (хотя бы в части случаев) быстрее обычного Python? А в чём тогда вообще смысл его применения?

        Градеры — это когда сдаётся только часть программы, а остальное реализовано жюри? То есть, headers&footers? (Просто не~помню официального использования именно термина "градер", поэтому хочу уточнить.) Если это об этом, то, как по мне, они просто обязаны быть для каждого заявленного языка и ещё не~свидетельствуют о хорошей поддержке.

  • »
    »
    5 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Прошу прощения, что как-то и навключал в предыдущий комментарий кучу всего, и добавляю это отдельно, но так уж вышло.

    Именно разница в 20 раз у меня получалась несколько раз, из которых предъявить в готовом виде готов код https://ideone.com/Etsk4M против кода https://ideone.com/2hqO3f (везде -- именно с тем примером входных данных). Кстати, попытка https://ideone.com/Itn2VQ где for-ы с range-ами заменены на while-ы, работает ещё дольше, что вроде ж как-то плохо согласуется с утверждением, что range-и сделаны медленно? Или согласуется, но надо учесть ещё какой-то неожиданный фактор?

    А речь ведь идёт ещё и о том, что это дети класса так 9-го (иногда младше), а также ещё и о том, что не~хотелось бы без самой-самой-самой крайней степени уверенности начинать переть против того, что другие учителя им рассказывают, что Питон -- это современно, модно, стильно, и как там ещё говорят.

    • »
      »
      »
      5 лет назад, # ^ |
      Rev. 5   Проголосовать: нравится 0 Проголосовать: не нравится

      У меня получилось ускорить код (не сильно, но я многого не сделал еще) — внезапно, занесением в функцию. Видимо, глобальная область видимости плохо влияет на выполнение программы :)

      UPD: видимо, проблема действительно в MOD. Код без него, но с дробным делением работает на 0.1с (из 0.5!) быстрее

      • »
        »
        »
        »
        5 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Зазеркалье какое-то — плавающая точка быстрее целых чисел. Я уж подумал, что, для полноты комплекта, ещё и exception быстрее, чем break. (Но, вроде бы, break всё же чуть быстрее, и на том спасибо...)

»
5 лет назад, # |
  Проголосовать: нравится +8 Проголосовать: не нравится

A little bit off topic:

Once I was interested how to speed up python and googling something like "how to speed up python" helped to learn some basic rules (this is not the link I found back than) like:

  • do not += string
  • do not reinvent wheel
  • keep namespace clean
  • high order functions sometimes work faster than loops

Following this gave another insignificant 0.01 improvement over previous result.

IMHO people who use python for CP should make sure they know basic differences between Py3 and Py2 (e.g. never in Py2

for i in range(10**9): 
 if i % 2 == 0:
    break

)

The main problem with p̶a̶s̶c̶a̶l̶ python to my mind is absence of STL, in UOI probably it is less significant but in ACM it is crucial. On the other hand, long arithmetics in PY3 is for free.
And in some regions of ACM ICPC judges provide solutions in python, so looks like it is starting to be trendy in CP also. Another interesting thing is that Kotlin is also fast to write, though slow to work.

  • »
    »
    5 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Володя, а ты серьёзно считаешь, что все эти нагромождения join-ов, map-ов и filter-ов того стОят? (Изучать именно их, а не более быстрый язык программирования?) Кстати, спрашиваю вполне серьёзно.

    А есть ли у тебя какое-то мнение о том, стОит ли рассматривать это всё на "классическом" Python, или на pypy? (Хотя, вряд ли ты знаешь, куда сейчас дрейфует UOI, а нужный мне ответ может зависеть в т.ч. и от этого.)

    И, чтоб быть уверенным, что мы говорим в рамках одной терминологии: high order functions — это в смысле Python Higher-Order Functions and Decorators A function can take one or more functions as arguments. A function can be returned as a result of another function.? Или каком-то другом?

    • »
      »
      »
      5 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

      На счёт изучать быстрые языки программирования — наверное зависит больше от потребностей рынка труда. Почти весь Data Science на Python оттуда, наверное, и его популярность. Ну а знать подводные камни и особенности языка на котором работаешь, думаю, так же важно как и структуры данных, тем более что это взаимосвязанно. В любом случае, если нет времени/желания/возможности учить С++ с 0, наверное, стоит хотя бы прочитать 1-2 статей в интернете как писать код на Python так, что-бы он не бежал вечность.

      Не знаю куда дрейфует UOI, но если не в сторону PyPy то, нужно будет видать делать разные TL на разных языках (как на некоторых ресурсах).

      Да, higher order functions, на С++ они тоже есть, не знаю на сколько популярны в СП.