28.11.2018       Выпуск 258 (26.11.2018 - 02.12.2018)       Статьи

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

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

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

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

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

транспайлер

ов (первый переводит код на языке Python в код на

новом языке программирования 11l

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

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

Python11l
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 < cb in a<..<c
a <= b < cb in a..<c
a < b <= cb in a<..c
0 <= b <= 9b 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 автоматически:

Python11l
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 для этого используется префикс @:

Python11l
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 логика ещё проще для запоминания:

Python11l
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 рассылка




Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus