Транспайлер-цепь Python → 11l → C++ [для ускорения Python-кода и не только]




    В данной статье рассматриваются наиболее интересные преобразования, которые выполняет цепочка из двух транспайлеров (первый переводит код на языке Python в код на новом языке программирования 11l, а второй — код на 11l в C++), а также производится сравнение производительности с другими средствами ускорения/исполнения кода на Python (PyPy, Cython, Nuitka).

    Замена "слайсов"\slices на диапазоны\ranges

    Python 11l
    s[-1]
    s[-2]
    s[:-1]
    s[1:]
    s[:2]
    s[1:2]
    s[::2]
    s[1::2]
    s[3:10:2]
    
    s.last
    s[(len)-2]
    s[0..<(len)-1]
    s[1..]
    s[0..<2]
    s[1..<2]
    s[(0..).step(2)]
    s[(1..).step(2)]
    s[(3..<10).step(2)]
    
    Явное указание для индексирования от конца массива s[(len)-2] вместо просто s[-2] нужно для исключения следующих ошибок:
    1. Когда требуется к примеру получить предыдущий символ по s[i-1], но при i = 0 такая/данная запись вместо ошибки молча вернёт последний символ строки [и я на практике сталкивался с такой ошибкой — коммит].
    2. Выражение s[i:] после i = s.find(":") будет работать неверно когда символ не найден в строке [вместо ‘‘часть строки начиная с первого символа : и далее’’ будет взят последний символ строки] (и вообще, возвращать -1 функцией find() в Python-е я считаю также неправильно [следует возвращать null/None (а если требуется -1, то следует писать явно: i = s.find(":") ?? -1)]).
    3. Запись s[-n:] для получения n последних символов строки будет некорректно работать при n = 0.

    Также отказ от поддержки отрицательного index в s[index] целесообразен с точки зрения производительности — не нужно делать лишнюю проверку на index < 0.

    Цепочки операторов сравнения


    На первый взгляд выдающаяся черта языка Python, но на практике от неё легко можно отказаться/обойтись посредством оператора in и диапазонов:
    a < b < c b in a<..<c
    a <= b < c b in a..<c
    a < b <= c b in a<..c
    0 <= b <= 9 b in 0..9

    Списковое включение (list comprehension)


    Аналогично, как оказалось, можно отказаться и от другой интересной фичи Python — list comprehensions.
    В то время как одни прославляют list comprehension и даже предлагают отказаться от `filter()` и `map()`, я обнаружил, что:
    1. Во всех местах, где мне встречалось Python's list comprehension, можно легко обойтись функциями `filter()` и `map()`.
      dirs[:] = [d for d in dirs if d[0] != '.' and d != exclude_dir]
      dirs[:] = filter(lambda d: d[0] != '.' and d != exclude_dir, dirs)
      
      '[' + ', '.join(python_types_to_11l[ty] for ty in self.type_args) + ']'
      '[' + ', '.join(map(lambda ty: python_types_to_11l[ty], self.type_args)) + ']'
      
      # Nested list comprehension:
      matrix = [
          [1, 2, 3, 4],
          [5, 6, 7, 8],
          [9, 10, 11, 12],
      ]
      [[row[i] for row in matrix] for i in range(4)]
      list(map(lambda i: list(map(lambda row: row[i], matrix)), range(4)))
      
    2. `filter()` и `map()` в 11l выглядят красивее, чем в Python
      dirs[:] = filter(lambda d: d[0] != '.' and d != exclude_dir, dirs)
      dirs = dirs.filter(d -> d[0] != ‘.’ & d != @exclude_dir)
      
      '[' + ', '.join(map(lambda ty: python_types_to_11l[ty], self.type_args)) + ']'
      ‘[’(.type_args.map(ty -> :python_types_to_11l[ty]).join(‘, ’))‘]’
      
      outfile.write("\n".join(x[1] for x in fileslist if x[0]))
      outfile.write("\n".join(map(lambda x: x[1], filter(lambda x: x[0], fileslist))))
      outfile.write(fileslist.filter(x -> x[0]).map(x -> x[1]).join("\n"))
      
      и следовательно необходимость в list comprehensions в 11l фактически отпадает [замена list comprehension на filter() и/или map() выполняется в процессе преобразования Python-кода в 11l автоматически].

    Преобразование цепочки if-elif-else в switch


    В то время как Python не содержит оператора switch, это одна из самых красивых конструкций в языке 11l, и поэтому я решил вставлять switch автоматически:
    Python 11l
    ch = instr[i]
    if ch == "[":
        nesting_level += 1
    elif ch == "]":
        nesting_level -= 1
        if nesting_level == 0:
            break
    elif ch == "‘":
        ending_tags.append('’') # ‘‘
    elif ch == "’":
        assert(ending_tags.pop() == '’')
    
    switch instr[i]
        ‘[’
            nesting_level++
        ‘]’
            if --nesting_level == 0
                loop.break
        "‘"
            ending_tags.append("’") // ‘‘
        "’"
            assert(ending_tags.pop() == "’")
    
    
    Для полноты картины вот сгенерированный код на C++
    switch (instr[i])
    {
    case u'[':
        nesting_level++;
        break;
    case u']':
        if (--nesting_level == 0)
            goto break_;
        break;
    case u'‘':
        ending_tags.append(u"’"_S);
        break; // ‘‘
    case u'’':
        assert(ending_tags.pop() == u'’');
        break;
    }
    


    Преобразование небольших словарей в нативный код


    Рассмотрим такую строчку кода на Python:
    tag = {'*':'b', '_':'u', '-':'s', '~':'i'}[prev_char()]
    
    Скорее всего, такая форма записи не очень эффективна [с точки зрения производительности], зато очень удобна.

    В 11l же соответствующая данной строчке [и полученная транспайлером Python → 11l] запись не только удобная [впрочем, не настолько изящная как в Python], но и быстрая:
    var tag = switch prev_char() {‘*’ {‘b’}; ‘_’ {‘u’}; ‘-’ {‘s’}; ‘~’ {‘i’}}
    

    Приведённая строчка странслируется в:
    auto tag = [&](const auto &a){return a == u'*' ? u'b'_C : a == u'_' ? u'u'_C : a == u'-' ? u's'_C : a == u'~' ? u'i'_C : throw KeyError(a);}(prev_char());
    
    [Вызов лямбда-функции компилятор C++ встроит\inline в процессе оптимизации и останется только цепочка операторов ?/:.]

    В том случае, когда производится присваивание переменной, словарь оставляется как есть:
    Python
    rn = {'I': 1, 'V': 5, 'X': 10, 'L': 50, ...}
    11l
    var rn = [‘I’ = 1, ‘V’ = 5, ‘X’ = 10, ‘L’ = 50, ...]
    C++
    auto rn = create_dict(dict_of(u'I'_C, 1)(u'V'_C, 5)(u'X'_C, 10)(u'L'_C, 50)...);

    Захват\Capture внешних переменных


    В Python для указания того, что переменная не является локальной, а должна быть взята снаружи [от текущей функции], используется ключевое слово nonlocal [в противном случае к примеру found = True будет трактоваться как создание новой локальной переменной found, а не присваивание значения уже существующей внешней переменной].
    В 11l для этого используется префикс @:
    Python 11l
    writepos = 0
    def write_to_pos(pos, npos):
        nonlocal writepos
        outfile.write(...)
        writepos = npos
    
    var writepos = 0
    fn write_to_pos(pos, npos)
        @outfile.write(...)
        @writepos = npos
    
    
    C++:
    auto writepos = 0;
    auto write_to_pos = [..., &outfile, &writepos](const auto &pos, const auto &npos)
    {
        outfile.write(...);
        writepos = npos;
    };
    

    Глобальные переменные


    Аналогично внешним переменным, если забыть объявить глобальную переменную в Python [посредством ключевого слова global], то получится незаметный баг:
    break_label_index = -1
    ...
    def parse(tokens, source_):
        global source, tokeni, token, scope
        source = source_
        tokeni = -1
        token = None
        break_label_index = -1
        scope = Scope(None)
        ...
    
    var break_label_index = -1
    ...
    fn parse(tokens, source_)
        :source = source_
        :tokeni = -1
        :token = null
        break_label_index = -1
        :scope = Scope(null)
        ...
    
    
    Код на 11l [справа] в отличие от Python [слева] выдаст на этапе компиляции ошибку ‘необъявленная переменная break_label_index’.

    Индекс/номер текущего элемента контейнера


    Я всё время забываю порядок переменных, которые возвращает Python-функция enumerate {сначала идёт значение, а потом индекс или наоборот}. Поведение аналога в Ruby — each.with_index — гораздо легче запомнить: with index означает, что index идёт после value, а не перед. Но в 11l логика ещё проще для запоминания:
    Python 11l
    items = ['A', 'B', 'C']
    for index, item in enumerate(items):
        print(str(index) + ' = ' + item)
    
    var items = [‘A’, ‘B’, ‘C’]
    loop(item) items
       print(loop.index‘ = ’item)
    

    Производительность


    В качестве тестировочной используется программа преобразования пк-разметки в HTML, а в качестве исходных данных берётся исходник статьи по пк-разметке [так как эта статья на данный момент — самая большая из написанных на пк-разметке], и повторяется 10 раз, то есть получается из 48.8 килобайтной статьи файл размером 488Кб.

    Вот диаграмма, показывающая во сколько раз соответствующий способ исполнения Python-кода быстрее оригинальной реализации [CPython]:

    А теперь добавим на диаграмму реализацию, сгенерированную транспайлером Python → 11l → C++:

    Время выполнения [время преобразования файла размером 488Кб] составило 868 мс для CPython и 38 мс для сгенерированного C++ кода [это время включает в себя полноценный [т.е. не просто работу с данными в оперативной памяти] запуск программы операционной системой и весь ввод/вывод [чтение исходного файла [.pq] и сохранение нового файла [.html] на диск]].

    Я хотел ещё попробовать Shed Skin, но он не поддерживает локальные функции.
    Numba использовать также не получилось (выдаёт ошибку ‘Use of unknown opcode LOAD_BUILD_CLASS’).
    Вот архив с использовавшейся программой для сравнения производительности [под Windows] (требуются установленный Python 3.6 или выше и следующие Python-пакеты: pywin32, cython).

    Исходник на Python и вывод транспайлеров Python → 11l и 11l → C++:
    Python Сгенерированный 11l
    (с ключевыми словами вместо букв)
    11l
    (с буквами)
    Сгенерированный C++

    Комментарии 29

      +1
      Это как-то подозрительно хорошо.

      А имеет смысл попробовать ускорить достаточно сложный проект? Этот, например?
        0
        На данном этапе, увы, нет.
        В этом проекте слишком много чего импортируется и слишком много возможностей Python используется.
        [[[А декораторы поддерживать вообще не планируется.]]]
          0
          Жаль.
            0
            С моей точки зрения они только запутывают [на основе опыта перевода этого кода с декоратором @methodкод без декоратора получился более понятным].
            А "стандартные" декораторы (@property, @classmethod, @staticmethod), имхо, лучше поддерживать в синтаксисе языка программирования.

            Впрочем, я поторопился с ответом. И если получится найти несложный способ реализации декораторов, то можно и добавить их поддержку.
              +2
              Это с вашей точки зрения. А с точки опытного веб-разработчика, декораторы — отличный способ не сильно увеличивая код определить правила входа в конечные точки веб-приложения (CSRF проверки, разрешенные http методы, правила кеширования и пр.).
                0
                Очень бы хотелось увидеть пример хорошего использования декораторов.
                Вы не могли бы дать ссылочку на код [если он в open source, разумеется]?
                Буду крайне признателен.
              0
              скажем так — я не понимаю как сделать flexget без декораторов…
        0
        Прочитал мельком.
        Вопрос возник: если так все хорошо получилось преобразовать, то почему бы сразу не сделать C++ из Python, без прослойки 11l (думаю, что никому не нужной — еще один язык? — нее!)
          0
          Ну, начиналось всё с преобразователя 11l → C++. О компиляции/ускорении кода на Python я тогда вообще не думал.
          И лишь спустя полгода была начата работа над Python → 11l.
          Почему не над Python → C++?
          Ну, во-первых, 11l гораздо ближе к Python, чем C++ к Python.
          Во-вторых, не буду скрывать, проект Python → 11l задумывался с целью популяризации языка 11l.
            +2
            почитал прошлые ваши статьи про 11l.
            там вам пытались вправить мозги. я тоже самое скажу — забейте на 11l.
            Вот сделать переводчик (Транспайлер) быстрый — Python -> C++, а потом еще и вызов компиляции из коробки для него сразу, — было бы, наверно, полезно.
              0
              Таких проектов уже много и так.
                0
                А можете привести самые удачные?
                Просто я толковых проектов не нашёл (помимо упомянутых в статье Nuitka и Shed Skin, я попробовал py14, Pythran и Py2C).
                  0
                  Да, по сути эти проекты и так в вашей статье. Но самый удачный проект — Cython. Он конечно вводит статическую типизацию, но одновременно и самый удобный в применении и в него можно при желании скомпилить многое оптимизируя кусками, без сильных переделок, а это самое важное. Это гораздо важнее возможности транслировать любой код на питоне в Си код.
                    0
                    Главная проблема Cython [и главное препятствие на пути его широкого распространения] с моей т.з. — аннотации типов в нём задаются по-своему, отлично от Python Type Hints (доступных начиная с Python 3.5).

                    А также, насколько я понял, нет достаточно удобного отладчика для Cython (кроме DDD ничего не нашёл). [Для 11l это не такая острая проблема, так как можно отлаживать Python-код перед отдачей его транспайлеру Python → 11l, а с Cython так не получится, так как код на нём написанный не совместим с Python.]
                      0
                      Главное препятствие на пути широкого распространения любой альтернативки CPython — то что это никому не нужно. Кроме специализированных случаев: pypy, numpy, cython, когда люди готовы специально потратить время на адаптацию алгоритмов. Никто не любит ограничений любых либ и трансляторов, ибо питон любят в том числе за синтаксис, иначе проще взять Go/Swift/Rust и пилить более быстрое ПО.

                      Если ваш транслятор хорош и действительно транслирует обычный питон-код, тогда его элементы можно включить в сам CPython и получить ускорение. И вот от этого никто бы не отказался. Но то-то мне подсказывает, что вы также будете предоставлять ограниченный питон.
                +1
                в продолжение.
                я бы стал делать через LLVM, те попробовал бы сделать байт код из Python кода понятный LLVM.
                может быть уже есть такие решения, посмотреть как сделаны, проверить скорость — и если есть куда расти, то…
            0
            А как у вас с безопасностью? Что будет есть обратиться за приделы списка элементов?
              0
              Также как в Python: обращение за пределы массива бросает исключение IndexError.
              +1
              Автор вы молодец, никого не слушайте и язык не бросайте. Самое главное — это самому понять почему этот язык никому не нужен, именно это и будет самым большим опытом в этом приключении. Ну и конечно навыки трансляции бесценны.
                0
                Ну и вдобавок:
                * Напишите простенький веб-фреймворк на языке и на нём же запустите собственный сайт.
                * Для коннекторов к базе используйте линковку Си драйвера любимой базы.
                * Не забудьте про шаблонизатор
                * Не забудьте про кеш
                * И вам понадобится какой-нибудь FastCgi протокол

                Вот когда вы это всё сделаете, может быть вы что-то ещё поймёте.
                0
                А где можно посмотреть список/план фич?
                IMHO: Стоит сранить с grumpy (golang).
                  0
                  А на nim вы смотрели (если размышлять о трансляции Python->промежуточный_язык->C++)? Мне, после питона, на нем показалось вполне комфортно, жаль, он пока не особо востребован в плане коммерческой разработки.
                  Не то, чтобы я был против еще одного языка. Пока появилась автомобильная промышленность, автомобили строили именно энтузиасты. Думаю здесь происходит нечто подобное.
                  Сам язык (я про 11l), его документацию, просмотрел по диагонали, и, если честно, не зацепило. Еще один язык с непривычным синтаксисом и смутными перспективами. Я подожду с его изучением.
                    0

                    Поддерживаются ли классы?

                      0
                      Да, только они называются типами (как в Go).
                      Коротенький пример есть в документации.
                      По умолчанию типы в 11l являются "типами-значениями" [а не "типами-ссылками"] и работают как структуры (struct) в C или C++.
                      В случае, когда тип Type ссылается сам на себя, Type заменяется на SharedPtr<Type> (см. первый пример отсюда).
                      В случае, когда тип Type содержит виртуальные функции, Type заменяется на std::unique_ptr<Type> (пример).
                        0
                        Прекращение поддержки Mercurial в Bitbucket :( сломало ): две последние ссылки из данного комментария.
                        Вот новые ссылки:
                        … Type заменяется на SharedPtr<Type> (см. первый пример отсюда).
                        … Type заменяется на std::unique_ptr<Type> (пример).

                        Также хочу заметить, что такое поведение планируется пересмотреть, и в новой версии транспайлера 11l → C++ Type будет заменяться на std::unique_ptr<Type> всегда когда это возможно (т.е. при отсутствии в коде вызова share(obj) с объектом obj типа Type).
                      0
                      while (true)
                      {
                          switch (instr[i])
                          {
                          case '[':
                              nesting_level++;
                              break;
                          case ']':
                              if (--nesting_level == 0)
                                  goto break_;
                              break;
                          }
                          i++;
                          ...
                      }
                      break_:


                      А вы специально используете эти скобочки, чтобы визуально увеличить кол-во C++-кода? В контексте питона(и 11l) вы не можете сказать «мне удобнее/привычней» выделять блоки «скобкой на новой строке», а не отступом.
                        0
                        Эмм… Не понял, если честно, ваш вопрос. Вы имеете в виду, почему не так:
                        while (true) {
                            switch (instr[i]) {
                        ...
                        

                        Или как вы предлагаете не "увеличивать кол-во C++-кода"? Ведь фигурные скобки в этом примере необходимы и являются требованием языка C++.

                        В контексте питона(и 11l) вы не можете сказать «мне удобнее/привычней» выделять блоки «скобкой на новой строке», а не отступом.
                        Почему не могу? 11l позволяет выделять блоки как отступом так и/или фигурными скобками.
                          0
                          Эмм… Не понял, если честно, ваш вопрос. Вы имеете в виду, почему не так:

                          Да.

                          11l позволяет выделять блоки

                          Это хорошо, но это не распространяется на питон, как и на привычки/видение пользователей питона, в том числе и вас(все ваши примеры без скобочек) и ЦА ваших трудов.

                          Вот я спрашиваю, чем обусловлен такой стиль? А мой же вопрос обусловлен древними холиварами на тему «в c++ есть скобочки, а у нас нет — наш код компактней». Вот я и думаю — это всё идёт с тех времен, либо что-то ещё.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое