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

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

Python 11l
s[-1]
s[-2]
s[:-1]
s[1:]
s[:1:]
s[1:2]
s[::2]
s[3:10:2]
s[3:10:]
s.last
s[(len)-2]
s[0..<(len)-1]
s[1..]
s[0..<1]
s[1..<2]
s[(0..).step(2)]
s[(3..<10).step(2)]
s[3..<10]
Явное указание для индексирования от конца массива 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.

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


На первый взгляд выдающаяся черта языка 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++

Комментарии (28)


  1. Tanner
    28.11.2018 07:10

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

    А имеет смысл попробовать ускорить достаточно сложный проект? Этот, например?


    1. alextretyak Автор
      28.11.2018 07:22

      На данном этапе, увы, нет.
      В этом проекте слишком много чего импортируется и слишком много возможностей Python используется.
      [[[А декораторы поддерживать вообще не планируется.]]]


      1. Tanner
        28.11.2018 07:25

        Жаль.


        1. alextretyak Автор
          28.11.2018 07:52

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

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


          1. gnomeby
            28.11.2018 10:26

            Это с вашей точки зрения. А с точки опытного веб-разработчика, декораторы — отличный способ не сильно увеличивая код определить правила входа в конечные точки веб-приложения (CSRF проверки, разрешенные http методы, правила кеширования и пр.).


            1. alextretyak Автор
              28.11.2018 12:26

              Очень бы хотелось увидеть пример хорошего использования декораторов.
              Вы не могли бы дать ссылочку на код [если он в open source, разумеется]?
              Буду крайне признателен.


              1. gnomeby
                28.11.2018 12:33

                Вот прямо на главной:
                flask.pocoo.org

                Вот посложнее:
                docs.python.org/3/library/contextlib.html


          1. asm0dey
            29.11.2018 22:00

            скажем так — я не понимаю как сделать flexget без декораторов…


  1. Tyiler
    28.11.2018 07:47

    Прочитал мельком.
    Вопрос возник: если так все хорошо получилось преобразовать, то почему бы сразу не сделать C++ из Python, без прослойки 11l (думаю, что никому не нужной — еще один язык? — нее!)


    1. alextretyak Автор
      28.11.2018 08:03

      Ну, начиналось всё с преобразователя 11l > C++. О компиляции/ускорении кода на Python я тогда вообще не думал.
      И лишь спустя полгода была начата работа над Python > 11l.
      Почему не над Python > C++?
      Ну, во-первых, 11l гораздо ближе к Python, чем C++ к Python.
      Во-вторых, не буду скрывать, проект Python > 11l задумывался с целью популяризации языка 11l.


      1. Tyiler
        28.11.2018 08:16

        почитал прошлые ваши статьи про 11l.
        там вам пытались вправить мозги. я тоже самое скажу — забейте на 11l.
        Вот сделать переводчик (Транспайлер) быстрый — Python -> C++, а потом еще и вызов компиляции из коробки для него сразу, — было бы, наверно, полезно.


        1. gnomeby
          28.11.2018 10:21

          Таких проектов уже много и так.


          1. alextretyak Автор
            28.11.2018 13:30

            А можете привести самые удачные?
            Просто я толковых проектов не нашёл (помимо упомянутых в статье Nuitka и Shed Skin, я попробовал py14, Pythran и Py2C).


            1. gnomeby
              28.11.2018 15:19

              Да, по сути эти проекты и так в вашей статье. Но самый удачный проект — Cython. Он конечно вводит статическую типизацию, но одновременно и самый удобный в применении и в него можно при желании скомпилить многое оптимизируя кусками, без сильных переделок, а это самое важное. Это гораздо важнее возможности транслировать любой код на питоне в Си код.


              1. alextretyak Автор
                28.11.2018 16:09

                Главная проблема Cython [и главное препятствие на пути его широкого распространения] с моей т.з. — аннотации типов в нём задаются по-своему, отлично от Python Type Hints (доступных начиная с Python 3.5).

                А также, насколько я понял, нет достаточно удобного отладчика для Cython (кроме DDD ничего не нашёл). [Для 11l это не такая острая проблема, так как можно отлаживать Python-код перед отдачей его транспайлеру Python > 11l, а с Cython так не получится, так как код на нём написанный не совместим с Python.]


                1. gnomeby
                  28.11.2018 16:46

                  Главное препятствие на пути широкого распространения любой альтернативки CPython — то что это никому не нужно. Кроме специализированных случаев: pypy, numpy, cython, когда люди готовы специально потратить время на адаптацию алгоритмов. Никто не любит ограничений любых либ и трансляторов, ибо питон любят в том числе за синтаксис, иначе проще взять Go/Swift/Rust и пилить более быстрое ПО.

                  Если ваш транслятор хорош и действительно транслирует обычный питон-код, тогда его элементы можно включить в сам CPython и получить ускорение. И вот от этого никто бы не отказался. Но то-то мне подсказывает, что вы также будете предоставлять ограниченный питон.


        1. Tyiler
          28.11.2018 14:18
          +1

          в продолжение.
          я бы стал делать через LLVM, те попробовал бы сделать байт код из Python кода понятный LLVM.
          может быть уже есть такие решения, посмотреть как сделаны, проверить скорость — и если есть куда расти, то…


  1. monester
    28.11.2018 10:06

    А как у вас с безопасностью? Что будет есть обратиться за приделы списка элементов?


    1. alextretyak Автор
      28.11.2018 10:49

      Также как в Python: обращение за пределы массива бросает исключение IndexError.


  1. gnomeby
    28.11.2018 10:31
    +1

    Автор вы молодец, никого не слушайте и язык не бросайте. Самое главное — это самому понять почему этот язык никому не нужен, именно это и будет самым большим опытом в этом приключении. Ну и конечно навыки трансляции бесценны.


    1. gnomeby
      28.11.2018 15:34

      Ну и вдобавок:
      * Напишите простенький веб-фреймворк на языке и на нём же запустите собственный сайт.
      * Для коннекторов к базе используйте линковку Си драйвера любимой базы.
      * Не забудьте про шаблонизатор
      * Не забудьте про кеш
      * И вам понадобится какой-нибудь FastCgi протокол

      Вот когда вы это всё сделаете, может быть вы что-то ещё поймёте.


  1. Tihon_V
    28.11.2018 10:39

    А где можно посмотреть список/план фич?
    IMHO: Стоит сранить с grumpy (golang).


  1. Zanak
    28.11.2018 11:12

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


  1. Ktulhy
    28.11.2018 14:28

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


    1. alextretyak Автор
      28.11.2018 14:52

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


  1. Pappageno
    29.11.2018 06:30

    while (true)
    {
        switch (instr[i])
        {
        case '[':
            nesting_level++;
            break;
        case ']':
            if (--nesting_level == 0)
                goto break_;
            break;
        }
        i++;
        ...
    }
    break_:


    А вы специально используете эти скобочки, чтобы визуально увеличить кол-во C++-кода? В контексте питона(и 11l) вы не можете сказать «мне удобнее/привычней» выделять блоки «скобкой на новой строке», а не отступом.


    1. alextretyak Автор
      29.11.2018 06:40

      Эмм… Не понял, если честно, ваш вопрос. Вы имеете в виду, почему не так:

      while (true) {
          switch (instr[i]) {
      ...
      

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

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


      1. Pappageno
        30.11.2018 07:43

        Эмм… Не понял, если честно, ваш вопрос. Вы имеете в виду, почему не так:

        Да.

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

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

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