Русский
Русский
English
Статистика
Реклама

Интерпретатор

Перевод Sparkplug неоптимизирующий компилятор JavaScript в подробностях

09.06.2021 20:16:20 | Автор: admin

Создать компилятор JS с высокой производительностью означает сделать больше, чем разработать сильно оптимизированный компилятор, например TurboFan, особенно это касается коротких сессий, к примеру, загрузки сайта или инструментов командной строки, когда большая часть работы выполняется до того, как оптимизирующий компилятор получит хотя бы шанс на оптимизацию, не говоря уже о том, чтобы располагать временем на оптимизацию. Как решить эту проблему? К старту курса о Frontend-разработке делимся переводом статьи о Sparkplug свече зажигания под капотом Chrome 91.


Вот почему с 2016 года мы ушли от синтетических бенчмарков, таких как Octane, к измерению реальной производительности и почему старательно работали над производительностью JS вне оптимизирующих компиляторов. Для нас это означало работу над парсером, стримингом [этой поясняющей ссылки в оригинале нет], объектной моделью, конкурентностью, кешированием скомпилированного кода...

Впрочем, повернувшись лицом к улучшению производительности фактического, начального выполнения JS, мы столкнулись с ограничениями процесса оптимизации интерпретатора. Интерпретатор V8 сам по себе быстрый и сильно оптимизированный, но интерпретаторам как таковым свойственны накладные расходы, избавиться от которых мы не можем: например на декодирование байт-кода или диспетчеризацию неотъемлемые части функциональности интерпретатора.

С нашей текущей моделью с двумя компиляторами мы не можем быстро перейти к оптимизированному коду, но после определённого момента ускориться можно, только удаляя снижающие пик производительности проходы оптимизации. Что ещё хуже, мы действительно не можем начать оптимизацию раньше, поскольку ещё нет стабильной обратной связи о форме объекта.

Выход из положения Sparkplug: новый неоптимизирующий компилятор JavaScript, который мы выпустили вместе с V8 9.1, он работает между интерпретатором Ignition и компилятором TurboFan.

Новый процесс компиляцииНовый процесс компиляции

Быстрый компилятор

Sparkplug создан компилировать быстро. Очень быстро. Настолько, что мы всегда можем компилировать, когда захотим, повышая уровень кода SparkPlug намного агрессивнее кода TurboFan, [подробнее здесь, этой ссылки в оригинале нет].

Есть пара трюков, делающих Sparkplug быстрым. Первый трюк это читы. Компилируемые им функции уже скомпилированы в байт-код, и компилятор байт-кода уже проделал большую часть тяжёлой работы, такой как разрешение переменных, анализ, не указывают ли скобки на стрелочную функцию, раскрытие выражений деструктуризации в полный код и т. д. Sparkplug компилирует, исходя из байт-кода, поэтому ему не нужно учитывать ничего из перечисленного.

Второй трюк в отличие от большинства компиляторов Sparkplug не генерирует промежуточное представление (IR). Вместо этого Sparkplug компилирует прямо в машинный код за один линейный проход по байт-коду и выдаёт соответствующий выполнению байт-кода машинный код. На самом деле компилятор это switch внутри цикла for, который занят диспетчеризацией, чтобы за каждой инструкцией закрепить генерирующие машинный код функции.

// The Sparkplug compiler (abridged).for (; !iterator.done(); iterator.Advance()) {  VisitSingleBytecode();}

Недочёт промежуточного представления (IR) заключается в том, что компилятор ограничен возможностями оптимизации. Поскольку промежуточной архитектурно независимой стадии нет, этот факт также означает, что всю реализацию нужно переносить на каждую поддерживаемую архитектуру отдельно. Но, оказывается, ни то, ни другое не является проблемой: портировать код довольно легко, вместе с тем Sparkplug не нужно выполнять тяжёлую оптимизацию.

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

Совместимые с интерпретатором фреймы

Добавление нового компилятора в зрелую виртуальную машину JS пугающая задача. Кроме стандартного выполнения в V8 мы должны поддерживать отладчик, профилирование центрального процессора с обходом стека, а значит, трассировки стека для исключений, интеграцию в стратегию динамического повышения уровня функции, замену на стеке, чтобы оптимизировать код горячих циклов. Работы много.

Sparkplug это рука мастера, которая ловкими, красивыми движениями упрощает большинство этих задач, то есть он поддерживает совместимые с интерпретатором стековые фреймы. Давайте вернёмся немного назад. При выполнении кода во стековых фреймах хранится состояние функции. Это касается каждого вызова какой-то новой функции: для её локальных переменных создаётся стековый фрейм. Этот стек определяется указателем на фрейм в начале и указателем на стек в конце.

Стековый фрейм с указателями стека и фреймаСтековый фрейм с указателями стека и фрейма

Сейчас около половины читателей закричит: "Диаграмма не имеет смысла, стек направлен в другую сторону!" Ничего страшного, я сделал кнопку: думаю, стек направлен вниз.

Когда функция вызвана, адрес возврата кладётся на стек и удаляется со стека функцией, когда она возвращается, чтобы знать, куда вернуться. Затем, когда функция создаёт новый фрейм, она сохраняет указатель на старый фрейм на стеке и устанавливает новый указатель в начало собственного стекового фрейма. Таким образом, на стеке образуется цепочка указателей на фреймы, каждый из которых отмечает начало фрейма, указывающего на предыдущий:

Стековые фреймы для нескольких вызововСтековые фреймы для нескольких вызовов

Строго говоря, это только соглашение, согласно которому создаётся код, но не требование. Хотя оно довольно универсально; не работает оно только в двух случаях: когда стековые фреймы полностью игнорируются или когда вместо него можно использовать таблицы со стороны отладчика.

Это основной макет стека для всех типов функций; затем идёт соглашение о передаче аргументов и о том, как в своём фрейме функция хранит значения. В V8 мы имеем соглашение для фреймов JS, что аргументы до вызова функции (включая приёмник) добавляются на стек в обратном порядке, а также о том, что первые несколько слотов стека это вызываемая функция, контекст, с которым она вызывается, и количество передаваемых аргументов. Вот наш стандартный фрейм JS.

Это соглашение о вызове JS, общее для оптимизированных и интерпретируемых фреймов, именно оно позволяет нам, например, при профилировании кода на панели производительности отладчика проходить по стеку с минимальными накладными расходами.

В случае Ignition соглашение становится более явным. Ignition интерпретатор на основе регистров, это означает, что есть виртуальные регистры (не путайте их с машинными!), которые хранят текущее состояние интерпретатора. включая локальные переменные (объявления var, let, const) и временные значения. Эти регистры содержатся в стековом фрейме интерпретатора, вместе с указателем на выполняемый массив байт-кода и смещением текущего байт-кода в массиве.

Sparkplug намеренно создаёт и поддерживает соответствующий фрейму интерпретатора макет фрейма. Всякий раз, когда интерпретатор сохраняет значение регистра, SparkPlug также сохраняет его. Делает он это по нескольким причинам:

  1. Это упрощает компиляцию Sparkplug; новый компилятор может просто отражать поведение интерпретатора без необходимости сохранять какое-либо отображение из регистров интерпретатора в состояние Sparkplug.

  2. Поскольку компилятор байт-кода выполнил тяжёлую работу по распределению регистров, такой подход ускоряет компиляцию.

  3. Это делает интеграцию с остальной частью системы почти тривиальной; отладчик, профайлер, раскручивание стека исключений, вывод трассировки все эти операции идут по стеку, чтобы узнать, каков текущий стек выполняемых функций, и все эти операции продолжают работать со Sparkplug почти без изменений, потому всё, что касается их, они получают из фрейма интерпретатора.

  4. Тривиальной становится и замена на стеке (OSR). Замена на стеке это когда выполняемая функция заменяется в процессе выполнения; сейчас это происходит, когда интерпретированная функция находится в горячем цикле (в это время она поднимается до оптимизированного кода этого цикла) и где оптимизированный код деоптимизируется (когда он опускается и продолжает выполнение функции в интерпретаторе), любая работающая в интерпретаторе логика замены на стеке будет работать и для Sparkplug. Даже лучше: мы можем взаимозаменять код интерпретатора и SparkPlug почти без накладных расходов на переход фреймов.

Мы немного изменили стековый фрейм интерпретатора: во время выполнения кода Sparkplug не поддерживается актуальная позиция смещения. Вместо этого мы храним двустороннее отображение из диапазона адресов кода Sparkplug к соответствующему смещению. Для декодирования такое сопоставление относительно просто, поскольку код Sparklpug получается линейным проходом через байт-код. Всякий раз, когда стековый фрейм хочет узнать "смещение байт-кода" для фрейма Sparkplug, мы смотрим на текущую выполняемую инструкцию в отображении и возвращаем связанное смещение байт-кода. Аналогично, когда Sparkplug нужно узнать OSR из интерпретатора, мы смотрим на байт-код в смещении и перемещаемся к соответствующей инструкции Sparkplug.

Вы можете заметить, что теперь у нас есть неиспользуемый слот фрейма, где должно быть смещение байт-кода; избавиться от него мы не можем, поскольку хотим сохранить оставшуюся часть стека неизменной. Мы перепрофилируем этот слот стека, чтобы вместо него кешировать "вектор обратной связи" для текущей выполняющейся функции; это вектор, хранящий данные о форме объекта, и он должен быть загружен для большинства операций. Всё, что нам нужно делать, соблюдать осторожность с OSR, чтобы гарантировать, что мы подставляем либо правильное смещение байт-кода, либо правильный вектор обратной связи для этого слота. В итоге стековый фрейм Sparkplug выглядит так:

Полагаемся на встроенный код

На самом деле Sparkplug генерирует очень мало собственного кода. У JS сложная семантика, так что для выполнения даже самых простых операций требуется много кода. Принудительная повторная генерация такого кода при каждой компиляции оказалась бы плохим решениям по нескольким причинам:

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

  2. Этот подход увеличил бы потребление памяти кодом Sparkplug.

  3. Пришлось бы переписывать кодогенерацию для большого количества функциональности JS, что, вероятно, означало бы и больше ошибок, и большую поверхность атаки.

Поэтому, чтобы сделать грязную работу, вместо повторной генерации кода Sparkplug вызывает встроенные функции, небольшие сниппеты машинного кода, встроенного в двоичный файл. Они либо совпадают с теми, что использует интерпретатор, либо по крайней мере большая часть кода Sparkplug общая с кодом обработчиков интерпретатора. В действительности код Sparkplug это вызовы встроенных двоичных сниппетов и поток управления.

Вы можете подумать: "Ну и какой тогда смысл во всём этом? Разве Sparkplug не выполняет ту же работу, что и интерпретатор?" и во многом будете правы. Во многих отношениях Sparkplug является "просто" сериализацией выполнения интерпретатора, вызывая те же встроенные двоичные сниппеты и поддерживая тот же стековый фрейм. Тем не менее оно того стоит, потому что Sparkplug удаляет (или, точнее, предварительно компилирует) те самые неустранимые накладные расходы интерпретатора, такие как декодирование операндов и диспетчеризация следующего байт-кода.

Оказалось, что интерпретация эффективнее множества оптимизаций уровня центрального процессора: статические операнды динамически читаются из памяти интерпретатором, вынуждая процессор делать предположения о том, какими могут быть значения. Диспетчеризация к следующей инструкции байт-кода для сохранения производительности требует успешного прогнозирования ветви выполнения, и, даже если предположения и прогнозы верны, по-прежнему нужно выполнять декодирование и диспетчеризацию кода, а также занимать драгоценное пространство различных буферов и кешей. ЦП сам по себе эффективный интерпретатор, хотя он применяется к машинному коду. С этой точки зрения Sparkplug транспилятор из байт-кода Ignition в байт-код центрального процессора, перемещающий выполнение в "эмуляторе" к "нативному" выполнению.

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

Так как же Sparkplug работает на практике? Мы выполнили несколько бенчмарков Chrome на наших ботах для замера производительности со Sparkplug и без него. Спойлер: мы очень довольны.

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

Speedometer

Speedometer это тест, который пытается эмулировать реально работающий фреймворк, создавая веб-приложение для отслеживания списка задач с использованием нескольких популярных фреймворков и проводя стресс-тестирование производительности этого приложения добавлением и удалением задач. Мы обнаружили, что это отличное отражение поведения загрузки и взаимодействия в реальном мире, и мы неоднократно обнаруживали, что улучшения в спидометре отражаются на наших реальных показателях. Со Sparkplug оценка Speedometer улучшается на 510 %, в зависимости от бота.

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

Обзор бенчмарка

Speedometer отличный ориентир, но он не показывает всей картины. Также у нас есть набор бенчмарков просмотра веб-страниц записи набора реальных веб-сайтов, их мы можем воспроизводить, а также скрипт небольших взаимодействий, с их помощью мы можем получить более реалистичное представление о том, как различные метрики ведут себя в реальном мире.

На этих тестах мы решили посмотреть метрику V8 Main-Thread Thread, измеряющую общее проведённое в V8 время (включая компиляцию и выполнение), в основном потоке (то есть исключая стриминговый парсинг или фоновую оптимизацию). Это лучший способ увидеть, насколько оправдан Sparkplug, без учёта других источников шума бенчмарка.

Результаты различны и сильно зависят от машины и веб-сайта, но в целом выглядят прекрасно: видно улучшение около 515 %.

Медианное улучшение времени работы V8 в основном потоке на наших бенчмарках для просмотра с 10 повторениями. Полосы на диаграмме указывают на диапазон между квартилямиМедианное улучшение времени работы V8 в основном потоке на наших бенчмарках для просмотра с 10 повторениями. Полосы на диаграмме указывают на диапазон между квартилями

Таким образом, V8 имеет новый сверхбыстрый неоптимизирующий компилятор, повышающий производительность V8 в реальных бенчмарках на 515 %. Он уже доступен в V8 v9.1 (укажите опцию --sparkplug), и мы выпустим его вместе с Chrome 91.

Вот что важно заметить: выделившись в отдельную область разработки, фронтенд не ограничивается одним только JavaScript, например есть множество тонкостей в том, каким образом браузер работает с CSS. Вместе с тем оптимизации на уровне браузера не означают, что можно больше не писать аккуратный, компактный и элегантный код. Скорее они означают, что теперь разработчики будут свободнее чувствовать себя, когда захотят написать сложную или тяжёлую функциональность. Если фронтенд вам интересен, вы можете обратить внимание на наш курс Frontend-разработчик, где получите комплексную подготовку, чтобы в дальнейшем писать и поддерживать приложения различного масштаба и уровня сложности.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Устройство CPython. Доклад Яндекса

22.07.2020 10:16:05 | Автор: admin
Мы публикуем конспект вступительной лекции видеокурса Бэкенд-разработка на Python. В ней Егор Овчаренко egorovcharenko, тимлид в Яндекс.Такси, рассказал о внутреннем устройстве интерпретатора CPython.


Если кратко, какой у нас будет план? Сначала мы поговорим о том, почему будем изучать именно Python. Затем посмотрим, как работает интерпретатор CPython более глубоко, как он управляет памятью, как устроена система типов в Python, на словари, генераторы и исключения. Я думаю, это займет примерно час.


Почему Python?



* insights.stackoverflow.com/survey/2019
** очень субъективно
*** интерпретация исследования
**** интерпретация исследования


Давайте начнем. Почему Python? На слайде есть сравнение нескольких языков, которые сейчас используются в бэкенд-разработке. Но если кратко, в чем преимущество Python? На нем можно быстро писать код. Это, конечно, очень субъективно люди, которые круто пишут на C++ или Go, могут с этим поспорить. Но в среднем писать на Python быстрее.

В чем минусы? Первый и, наверное, основной минус Python медленнее. Он может быть медленнее других языков в 30 раз, вот исследование на эту тему. Но его скорость зависит от задачи. Есть два класса задач:

CPU bound, задачи, зависящие от процессора, ограниченные по CPU.

I/O bound, задачи, ограниченные вводом-выводом: или по сети, или в базах данных.

Если вы решаете задачу CPU bound, то да, Python окажется медленнее. Если I/O bound, а это большой класс задач, то для понимания скорости выполнения вам надо запускать бенчмарки. И, возможно, сравнивая Python с другими языками, вы даже не заметите разницы в производительности.

Кроме того, Python обладает динамической типизацией: интерпретатор в момент компиляции не проверяет типы. В версии 3.5 появились type hints, позволяющие статически указывать типы, но они не очень строгие. То есть некоторые ошибки вы будете отлавливать уже в продакшене, а не на этапе компиляции. У других популярных языков для бэкенда Java, C#, C++, Go типизация статическая: если вы в коде передаете не тот объект, который нужно, компилятор вам об этом сообщит.

Если чуть более приземленно, как используется Python в продуктовой разработке Такси? Мы движемся в сторону микросервисной архитектуры. У нас уже 160 микросервисов, именно продуктовых 35, 15 из них на Python, 20 на плюсах. То есть мы сейчас пишем или только на Python, или на плюсах.

Как мы выбираем язык? Первое требования по нагрузке, то есть смотрим, потянет Python или нет. Если он тянет, тогда мы смотрим на компетенцию разработчиков команд.

Сейчас хочется поговорить про интерпретатор. Как работает CPython?

Устройство интерпретатора


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

1. Оптимизация под высокую нагрузку. Представьте, что у вас есть сервис на Python. Он работает, нагрузка невысокая. Но однажды вам приходит задача написать ручку, готовую к высокой нагрузке. От этого не уйти, не переписывать же весь сервис на C++. Итак, вам нужно оптимизировать сервис под высокую нагрузку. Понимание того, как работает интерпретатор, может в этом помочь.

2. Отладка сложных случаев. Допустим, сервис работает, но в нем начинает утекать память. У нас в Яндекс.Такси такой случай был буквально недавно. Каждый час сервис выедал 8 ГБ памяти и падал. Надо разбираться. Дело в языке, в Python. Требуется знание, как работает управление памятью в Python.

3. Это пригодится, если вы будете писать сложные библиотеки или сложный код.

4. И вообще считается хорошим тоном знать инструмент, с которым вы работаете, на более глубоком уровне, а не просто как пользователь. В Яндексе это ценится.

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



Давайте кратко вспомним, какие бывают виды трансляторов. У нас бывают компиляторы, а бывают интерпретаторы. Компилятор, как вы, наверное, знаете, это штука, которая переводит ваш исходный код сразу в машинный. Интерпретатор скорее переводит сначала в байт-код, а потом уже его исполняет. Python интерпретируемый язык.

Байт-код некий промежуточный код, который получается из исходного. Он не привязан к платформе и выполняется на некой виртуальной машине. Почему виртуальная? Это не настоящая машина, а некая абстракция.



Каких типов бывают виртуальные машины? Регистровые и стековые. Но здесь надо запомнить не это, а то, что Python стековая машина. Дальше мы посмотрим, как работает стек.

И еще одна оговорка: здесь мы будем говорить только о CPython. CPython референсная имплементация Python, написанная, как можно догадаться, на C. Используется как синоним: когда мы говорим о Python, мы обычно говорим о CPython.

Но также есть другие интерпретаторы. Есть PyPy, который использует JIT-компиляцию и ускоряет где-то в пять раз. Используется редко. Я, честно говоря, не встречал. Есть JPython, есть IronPython, который переводит байт-код для Java Virtual Machine и для дотнетовской машины. Это out of scope сегодняшней лекции честно говоря, я с ним не сталкивался. Поэтому давайте посмотрим на CPython.



Посмотрим, что происходит. У вас есть исходник, строчка, вы хотите ее выполнить. Что делает интерпретатор? Строка это просто набор символов. Чтобы сделать с ним нечто осмысленное, сначала код переводится в лексемы. Лексема некий сгруппированный набор символов, идентификатор, число или какая-то итерация. Собственно, интерпретатор переводит код в лексемы.



Дальше из этих лексем строится Abstract Syntax Tree, AST. Тоже пока не заморачивайтесь, это просто некие деревья, в узлах которых у вас операции. Допустим, в нашем случае есть BinOp, бинарная операция. Операция возведение в степень, операнды: число, которое возводим, и степень, в которую возводим.

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

Здесь посмотрим поподробнее. Байт-код это, как говорит нам это название, код, состоящий из байтов. А в Python начиная с 3.6 байт-код это два байта.



Первый байт это сам оператор, называется opcode. Второй байт это аргумент oparg. Он выглядит как у нас сверху. То есть какая-то последовательность байт. Но в Python есть модуль dis, от слова Disassembler, с помощью которого мы можем посмотреть более человекочитаемое представление.

Как оно выглядит? Есть номер строчки исходника самая левая единичка. Вторая колонка это адрес. Как я говорил, байт-код в Python 3.6 занимает два байта, поэтому у нас все адреса четные и мы видим 0, 2, 4

Load.name, Load.const это уже сами опции кода, то есть коды тех операций, которые Python должен выполнить. 0, 0, 1, 1 это oparg, то есть аргументы этих операций. Дальше посмотрим, как они выполняются.

(...) Давайте посмотрим, как в Python происходит выполнение байт-кода, какие для этого есть структуры.



Если не знаете С, не страшно. Сноски даны для общего понимания.

В Python есть две структуры, которые нам помогают выполнять байт-код. Первая CodeObject, вы видите ее краткое содержание. На самом деле структура больше. Это код без контекста. Значит, эта структура содержит, собственно, байт-код, который мы только что видели. Она содержит в себе названия переменных, использующиеся в этой функции, если функция содержит ссылки на константы, названия констант, что-то еще.



Следующая структура FrameObject. Это уже контекст выполнения, та структура, которая уже содержит в себе значение переменных; ссылки на глобальные переменные; стек выполнения, о котором поговорим чуть позже, и много другой информации. Допустим, номер выполнения инструкции.

Как пример: если вы хотите вызвать функцию несколько раз, то CodeObject у вас будет один и тот же, а FrameObject на каждый вызов будет создаваться новый. У него будут свои аргументы, свой стек. Так они взаимосвязаны.



Что такое основной цикл интерпретатора, как выполняется байт-код? Вы видели, у нас был список этих opcode с oparg. Как это все выполняется? В Python, как в любом интерпретаторе, есть цикл, который выполняет этот байт-код. То есть на вход в него поступает фрейм, и Python просто по порядку идет по байт-коду, смотрит, что это за oparg, и переходит к его обработчику с помощью огромного switch. Здесь для примера приведен только один opcode. Для примера, у нас здесь есть binary subtract, бинарное вычитание, допустим, A-B у нас выполнится в этом месте.

Давайте расскажу, как работает binary subtract. Очень просто, это один из самых простых кодов. Функция TOP берет из стека самое верхнее значение, берет тоже с самого верхнего, не просто удаляет его из стека, и потом вызывается функция PyNumber_Subtract. Результат: слэш функция SET_TOP помещается обратно на стек. Если про стек не понятно, дальше будет пример.



Очень кратко о GIL. GIL это мьютекс, который есть в Python на уровне процесса и который в основной цикл интерпретатора делает take этого мьютекса. И только после этого начинает выполнять байт-код. Это сделано для того, чтобы в один момент времени только один поток выполнял байт-код, чтобы защитить внутреннее устройство интерпретатора.

Допустим, забегая чуть дальше, что у всех объектов в Python есть количество ссылок на них. И если два потока будут менять это количество ссылок, то интерпретатор сломается. Поэтому есть GIL.

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



Краткий пример. Вы можете спокойно исследовать этот фрейм из Python. Есть модуль sys, у которого есть функция подчеркивания get_frame. Вы можете получить фрейм и посмотреть, какие переменные есть. Есть инструкция. Это больше для обучения, в реальной жизни я его не применял.

Давайте для понимания попробуем посмотреть, как работает стек виртуальной машины Python. У нас есть некий код, довольно простой, который непонятно что делает.



Слева код. Желтым выделена часть, которую мы сейчас рассматриваем. Во второй колонке у нас байт-код этого кусочка. В третьей колонке фреймы со стеками. То есть стек выполнения у каждого FrameObject свой.

Что делает Python? Идет просто по порядку, по байт-коду, по средней колонке, выполняет и работает со стеком.



У нас выполнился первый opcode, который называется LOAD_CONST. Он загружает константу. Мы пропустили часть, там создается CodeObject, и у нас где-то в константах был некий CodeObject. Python загрузил его на стек с помощью LOAD_CONST. У нас теперь на стеке в этом фрейме есть объект CodeObject. Можем идти дальше.



Потом Python выполняет opcode MAKE_FUNCTION. MAKE_FUNCTION, очевидно, делает функцию. Он ожидает, что на стеке у вас был CodeObject. Он производит некие действия, создает функцию и кладет функцию обратно на стек. Теперь у вас FUNCTION вместо CodeObject, который был на стеке фрейма. И теперь эту функцию нужно поместить в перемененную to_power, чтобы вы могли к ней обращаться.



Выполняется opcode STORE_NAME, он помещается в переменную to_power. На стеке у нас была функция, теперь это переменная to_power, вы можете к ней обращаться.

Дальше мы хотим напечатать 10 + значение этой функции.



Что делает Python? Это преобразовалось в байт-код. Первый opcode у нас LOAD_CONST. Мы загружаем десятку на стек. На стеке появилась десятка. Теперь надо выполнить to_power.



Функция выполняется следующим образом. Если она с позиционными аргументами остальное пока не будем смотреть, то сначала Python кладет саму функцию на стек. Потом он кладет все аргументы и вызывает CALL_FUNCTION с аргументом количества аргументов функции.



Загрузили на стек первый аргумент, это функция.



Загрузили на стек еще два аргумента 30 и 2. Теперь у нас на стеке функция и два аргумента. Верхушка стека у нас сверху. CALL_FUNCTION у нас ожидает. Мы говорим: CALL_FUNCTION (2), то есть у нас функция с двумя аргументами. CALL_FUNCTION ожидает, что у нас на стеке будет два аргумента, после них функция. У нас так и есть: 2, 30 и FUNCTION.

Выполняется opcode.



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

Стек у фрейма свой. Создался новый фрейм под свою функцию. Он пока пустой.



Дальше происходит выполнение. Тут уже попроще. Нам нужно возвести A в степень power. Мы загружаем на стек значение переменной A 30. Значение переменной power 2.



И выполняется opcode BINARY_POWER.



У нас возводится одно число в степень другого и кладется на стек обратно. Получилось 900 на стеке функции.

Следующий opcode RETURN_VALUE возвратит значение со стека в предыдущий фрейм.



Так происходит выполнение. Функция завершилась, фрейм, скорее всего, очистится, если у него нет ссылок, и на фрейме предыдущей функции будет два числа.



Дальше все примерно так же. Происходит сложение.



(...) Давайте поговорим про типы и PyObject.

Типизация




Объект сишная структура, в которой есть два основных поля: первое количество ссылок на этот объект, второе тип объекта, естественно, ссылка на тип объекта.

Другие объекты наследуются от PyObject путем включения его в себя. То есть если мы посмотрим на float, число с плавающей точкой, структурка там PyFloatObject, то у него есть HEAD, который является структурой PyObject, и, дополнительно, данные, то есть double ob_fval, где хранится значение самого этого float.



А это уже тип объекта. Мы только что смотрели в PyObject тип, это структура, которая обозначает тип. По сути это тоже сишная структура, которая содержит в себе указатели на функции, реализующие поведение этого объекта. То есть там очень большая структура. У нее указаны функции, которые вызываются, если вы, допустим, хотите сложить два объекта этого типа. Или хотите вычесть, вызвать этот объект или создать его. Все, что вы можете сделать с типами, должно быть указано в этой структуре.



Для примера посмотрим на int, целые числа в Python. Тоже очень сокращенная версия. Что нам может быть интересно? У int есть tp_name. Видно, что есть tp_hash, мы можем получить hash int. Если мы вызовем hash от int, вызовется эта функция. tp_call у нас ноль, не определен, это значит, что мы не можем вызвать int. tp_str приведение к строке не определено. В Python есть функция str, которая может привести к строке.

На слайд это не попало, но вы все уже знаете, что int все-таки можно напечатать. Почему здесь ноль? Потому что есть еще tp_repr, в Python две функции проведения строки: str и repr. Более подробное приведение к строке. Оно на самом деле определено, просто на слайд не попало, и вызовется оно, если вы, собственно, будете приводить к строке.

В самом конце мы видим tp_new функцию, которая вызывается при создании этого объекта. tp_init у нас ноль. Все мы знаем, что int не изменяемый тип, immutable. После создания его изменять, инициализировать смысла нет, поэтому там нолик.



Для примера также посмотрим на Bool. Как кто-то, может быть, из вас знает, Bool в Python на самом деле наследуется от int. То есть вы можете Bool складывать, делить друг с другом. Этого делать, конечно, нельзя, но можно.

Мы видим, что есть tp_base указатель на базовый объект. Все помимо tp_base единственные вещи, которые были переопределены. То есть у него свое имя, своя функция представления, где как раз пишется не число, а true или false. Представление в виде Number, там переопределяются некоторые логические функции. Docstring своя и создание свое. Все остальное идет от int.



Очень кратко расскажу про списки. В Python список это динамический массив. А динамический массив это массив, который работает так: вы инициализируете область памяти заранее с какой-то размерностью. Туда добавляете элементы. Как только количество элементов превысит этот размер, вы его расширяете с неким запасом, то есть не на единичку, а на какое-то значение больше единички, чтобы была хорошая asin-точка.

В Python размер растет как 0, 4, 8, 16, 25, то есть по какой-то формуле, которая позволяет нам вставку сделать ассимптотически за константу. И можно посмотреть, есть выдержка из сишной функции вставки в список. То есть мы делаем resize. Если у нас не resize, мы выкидываем ошибку и присваиваем элемент. В Python это обычный динамический массив, реализованный на C.

(...) Давайте кратко поговорим про словари. Они в Python везде.

Словари


Мы все знаем, в объектах весь состав классов содержимтся в словарях. Очень многие вещи на них основаны. Словари в Python в хеш-таблице.



Если кратко, как работает хеш-таблица? Есть некие ключи, guido, timmy, barry. Мы хотим их положить в словарь, прогоняем каждый ключ через хеш-функцию. Получается хеш. Мы по этому хешу находим бакет. Бакет это просто номер в массиве элементов. Происходит конечное деление по модулю. Мы кладем этот элемент в этот бакет. Если бакет пустой, мы кладем туда элемент. Если бакет не пустой и там уже есть некий элемент, это коллизия и у нас выбирается следующий бакет, смотрится, свободный он или нет. И так до тех пор, пока мы не найдем свободный bucket.

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

Поэтому в Python эмпирически принято так, что одна треть элементов всегда свободна. Если количество элементов в массиве больше двух третей, то массив расширяется. Это не очень хорошо, потому что у нас одна треть этого массива тратится впустую, ничего полезного не хранится.


Ссылка со слайда

Поэтому с версии 3.6 в Python сделали такую штуку. Слева можно посмотреть, как было раньше. У нас есть разреженный массив, где хранятся эти три элемента. С 3.6 они решили сделать такой разреженный массив обычным массивом, но при этом хранить индексы элементов бакета в отдельном массиве indices.

Если мы посмотрим массив индексов, то в первом бакете у нас None, во втором лежит элемент с индексом 1 из этого массива и т. д.

Это позволило, во-первых, сократить использование памяти, во-вторых, мы еще бесплатно получили из коробки упорядоченный массив. То есть мы добавляем элементы в этот массив, условно, обычным сишным append, и массив автоматически упорядочен.

Есть интересная оптимизация, которую Python использует. Чтобы работали эти хеш-таблицы, нам нужно иметь операцию сравнения элементов. Представьте, мы поместили в хеш-таблицу элемент, а потом хотим взять элемент. Берем хеш, идем в бакет. Видим: бакет полный, там что-то есть. Но тот ли это элемент, который нам нужен? Может быть, когда он помещался, возникла коллизия и элемент на самом деле поместился в другой bucket. Поэтому мы должны сравнивать ключи. Если ключ не тот, мы применяем тот же механизм поиска следующего бакета, который используется при разрешении коллизий. И идем дальше.


Ссылка со слайда

Поэтому нам нужно иметь функцию сравнения ключей. В общем случае функция сравнения объектов может быть очень дорогой. Поэтому используется такая оптимизация. Сначала мы сравниваем айдишники элементов. ID в CPython это, как вы знаете, положение в памяти.

Если айдишники совпадают, значит, это одинаковые объекты и, естественно, они равны. Тогда мы возвращаем True. Если нет, смотрим хеши. Хеш должен быть довольно быстрой операцией, если мы не переопределили как-то. Мы берем хеши от этих двух объектов, сравниваем. Если их хеши не равны, то объекты точно не равны, поэтому мы возвращаем False.

И только в очень маловероятном случае если у нас хеши равны, но мы не знаем, тот ли это объект, только тогда мы сравниваем сами объекты.

Небольшая интересная штука: нельзя ничего вставлять в ключи во время итерации. Это ошибка.



Под капотом в словаре есть такая переменная version, в которой хранится версия словаря. Когда вы изменяете словарь, version меняется, Python это понимает и кидает вам ошибку.



Для чего словари могут использоваться на более практическом примере? У нас в Такси есть заказы, а у заказов статусы, которые могут меняться. При изменении статуса вы должны выполнять некие действия: отправить смски, записывать заказы.

Эта логика у нас написана на Python. Чтобы не писать огромный if вида if статус заказа такой-то, сделай то-то, есть некий dict, в котором ключ это статус заказа. А к VALUE есть tuple, в котором содержатся все обработчики, которые надо выполнить при переходе в данный статус. Это распространенная практика, фактически замена сишного switch.



Еще несколько вещей по типам. Расскажу про immutable. Это неизменяемые типы данных, а mutable соответственно, изменяемые типы: дикты, классы, инстансы классов, листы и, может, что-то еще. Практически все остальное строки, обычные числа они immutable. Для чего нужны mutable-типы? Первое: они позволяют проще понимать код. То есть если вы в коде видите, что что-то tuple, вы понимаете, что дальше он не изменяется, и вам это позволяет проще читать код? понимать, что будет дальше. В tuple ds не можете набрать элементы. Вы это будете понимать, и это поможет при чтении вам и всем людям, которые будут читать код за вами.

Поэтому есть правило: если вы что-то не будете менять, лучше используйте неизменяемые типы. Также это приводит к ускорению работы. Есть две константы, которые как раз использует tuple: pit_tuple, tap_tuple, max и СС. В чем смысл? Для всех tuple размера до 20 используется определенный способ выделения памяти, который ускоряет это выделение. И таких объектов каждого типа может быть до двух тысяч, очень много. Это гораздо быстрее, чем листы, поэтому если будете использовать tuple, у вас все будет быстрее.

Также есть проверки во время выполнения. Очевидно, если вы пытаетесь в объект что-то запиндить, а он не поддерживает эту функцию, то будет ошибка, некое понимание того, что вы сделали что-то не то. Ключами в дикте могут быть только объекты, у которых есть хеш, которые не изменяются во время жизни. Этому определению удовлетворяют только immutable-объекты. Только они могут быть ключами дикта.



Как это выглядит в C? Пример. Слева tuple, справа обычный list. Тут, естественно, видны не все отличия, а только те, которые хотел показать. В list в поле tp_hash у нас NotImplemented, то есть хеша у list нет. В tuple некая функция, которая вам действительно вернет хеш. Это как раз то, почему tuple в том числе может быть ключом дикта, а list не может.

Следующее, что выделено, это функция присваивания элемента, sq_ass_item. В list она есть, в tuple нолик, то есть вы в tuple, естественно, ничего не можете присвоить.



Еще одна вещь. Python ничего не копирует, пока мы его не попросим. Об этом тоже надо помнить. Если вы что-то хотите скопировать, используйте, допустим, модуль copy, у которого есть функция copy.deepcopy. В чем отличие? copy копирует объект, если это контейнерный объект, типа списка на одном уровне. Все ссылки, которые были в этом объекте, вставляются в новый объект. А deepcopy рекурсивно копирует все объекты внутри этого контейнера и дальше.

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

(...) Дальше поговорим про менеджмент памяти.

Менеджмент памяти




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

На самом деле во время выполнения Python создает очень много маленьких объектов. И если бы мы для их выделения использовали стандартную сишную функцию malloc, то очень быстро бы уткнулись в то, что у нас память фрагментированная и, соответственно, выделение памяти работает медленно.



Из этого вытекает необходимость использовать свой менеджер памяти. Вкратце, как он работает? Python выделяет себе блоки памяти, которые называются арена, по 256 килобайт. Внутри он себе нарезает пулы по четыре килобайта, это размер страницы памяти. Внутри пулов у нас живут блоки разного размера, от 16 до 512 байт.

Когда мы пытаемся выделить объекту меньше 512 байт, Python выбирает каким-то своим способом блок, который подходит для этого объекта и размещает объект в этом блоке.

Если объект деаллоцируется, удаляется, то этот блок помечается как свободный. Но операционной системе он не отдается, и мы при следующей алокации можем в тот же блок записать этот объект. Это очень сильно ускоряет выделение памяти.



Освобождение памяти. Раньше мы видели структуру PyObject. У нее есть этот refcnt счетчик ссылок. Работает очень просто. Когда вы берете референс на этот объект, Python увеличивает счетчик ссылок. Как только у вас объект, референс пропадает на него, вы деалоцируете счетчик ссылок.

То, что выделено желтым. Если refcnt не равен нулю, значит, мы там что-то делаем. Если refcnt равен нулю, то мы сразу же деаллоцируем объект. Не ждем никакого сборщика мусора, ничего, а прямо в этот момент очищаем память.

Если вы встретите метод del, он просто удаляет привязку переменной к объекту. А метод __del__, который вы можете определить в классе, вызывается, когда объект уже действительно удаляется из памяти. Вы вызовете del у объекта, но при этом, если у него еще есть ссылки, объект никуда не удалится. И его Finalizer, __del__, не вызовется. Хотя они называются очень похоже.

Краткая демка о том, как можно посмотреть количество ссылок. Есть наш любимый модуль sys, в котором есть функция getrefcount. Вы можете посмотреть количество ссылок на объект.



Расскажу подробнее. Делается объект. От него берется количество ссылок. Интересная деталь: переменная A указывает на TaxiOrder. Вы берете количество ссылок, у вас напечатается 2. Казалось бы, почему? У нас же одна ссылка на объект. Но когда вы вызываете getrefcount, этот объект бандится на аргумент внутри функции. Поэтому у вас уже есть две ссылки на этот объект: первая переменная, вторая аргумент функции. Поэтому печатается 2.

Дальше тривиально. Мы присваиваем еще одну переменную объекту, получаем 3. Потом удаляем эту привязку, получаем 2. Потом удаляем все ссылки на этот объект, и при этом вызывается финализатор, который напечатает нашу строчку.



(...) Есть еще одна интересная особенность CPython, на которую нельзя закладываться и нигде в доках про это, кажется, не сказано. Целые числа используются часто. Было бы расточительно их каждый раз создавать заново. Поэтому самые частоиспользуемые числа, разработчики Python выбрали диапазон от 5 до 255, они Singleton. То есть они созданы один раз, лежат где-то в интерпретаторе, и когда вы пытаетесь их получить, то получаете ссылку на один и тот же объект. Мы взяли A и B, единички, напечатали их, сравнили их адреса. Получили True. И у нас, допустим, 105 ссылок на этот объект, просто потому что сейчас получилось столько.

Если возьмем какое-то число больше допустим, 1408, у нас эти объекты не равны и ссылок на них, соответственно, две. А фактически одна.



Мы чуть-чуть поговорили про выделение памяти, про освобождение. Теперь поговорим про сборщик мусора. Для чего он нужен? Казалось бы, у нас есть число ссылок. Как только никто на объект не ссылается, мы можем его удалять. Но у нас могут быть циклические ссылки. Объект может ссылаться, допустим, сам на себя. Или, как в примере, может быть два объекта, каждый ссылается на соседа. Это называется цикл. И тогд эти объекты никогда могут не отдать ссылку на другой объект. Но при этом они, допустим, недостижимы из другой части программы. Нам надо их удалить, потому что они недоступны, бесполезны, но ссылки у них есть. Ровно для этого существует модуль garbage collector. Он детектит циклы и удаляет эти объекты.

Как он работает? Сначала кратко расскажу про поколения, а потом про алгоритм.



Для оптимизации скорости garbage collector в Python он generational, то есть работает с помощью поколений. Есть три поколения. Зачем они нужны? Понятно, что те объекты, которые создались совсем недавно, с большей вероятностью нам не нужны, чем долгоживущие объекты. Допустим, в ходе функций у вас что-то создается. Скорее всего, при выходе из функции оно будет не нужно. То же самое с циклами, с временными переменными. Все эти объекты надо чистить чаще, чем те, которые живут давно.

Поэтому в нулевое поколение помещаются все новые объекты. Периодически происходит очистка этого поколения. В Python есть три параметра. На каждое поколение свой параметр. Вы можете их получить, заимпортить garbage collector, вызвать функцию get_threshold, и получить эти пороговые значения.

По дефолту там 700, 10, 10. Что такое 700? Это количество созданий объектов минус количество удалений. Как только оно превышает 700, запускается сборка мусора в новом поколении. А 10, 10 это количество сборок мусора в предыдущем поколении, после которого нам надо запустить сборку мусора в текущем поколении.

То есть когда мы нулевое поколение очистим 10 раз, то запустим сборку в первом поколении. Очистив первое поколение 10 раз, запустим сборку во втором поколении. Соответственно, объекты перемещаются из поколения в поколение. Если выживают перемещаются в первое поколение. Если выжили при сборке мусора в первом поколении перемещаются во второе. Из второго поколения уже никуда не перемещаются, остаются там навсегда.



Как работает сборка мусора в Python? Допустим, мы запускаем сборку мусора в поколении 0. У нас есть некие объекты, у них циклы. Есть группа объектов слева, которые друг на друга ссылаются, и группа справа, тоже ссылается друг на друга. Важная деталь на них также есть ссылка из поколения 1. Как Python детектит циклы? Сначала у каждого объекта создается временная переменная и в нее записывается количество ссылок на этот объект. На слайде это отражено. У нас на объект сверху две ссылки. А вот на объект из поколения 1 кто-то ссылается снаружи. Python это запоминает. Потом (важно!) он проходит по каждому объекту внутри поколения и удаляет, декрементирует счетчик на число ссылок внутри этого поколения.



Вот что получилось. У объектов, которые ссылаются только друг на друга внутри поколения, эта переменная автоматически по построению стала равной нулю. Единичка только у объектов, на которые есть ссылки снаружи.

Что делает потом Python? Он, поскольку здесь единичка, понимает, что на эти объекты есть ссылка снаружи. И мы не можем удалить ни этот объект, ни этот, потому что иначе у нас получится невалидная ситуация. Поэтому Python переносит эти объекты в поколение 1, а все, что осталось в поколении 0, он удаляет, очищает. Про garbage collector все.



(...) Идем дальше. Очень кратко расскажу про генераторы.

Генераторы




Здесь, к сожалению, введения в генераторы не будет, но давайте попробую рассказать, что такое генератор. Это некая, условно говоря, функция, которая запоминает контекст своего выполнения с помощью слова yield. На этом месте она возвращает значение и запоминает контекст. Вы можете к ней потом вновь обращаться и получать значение, которое оно выдает.

Что с генераторами можно делать? Можно делать yield генератора, это вам вернет значения, запомнит контекст. Можно делать return для генератора. В этом случае у вас кинется эксепшен StopIteration, value внутри которого будет содержать значение, в данном случае Y.

Менее известный факт: вы можете отправить генератору некие значения. То есть вы вызываете у генератора метод send, и Z см. пример будет значением выражения yield, которое вызовет генератор. Если вы хотите генератором управлять, вы можете туда передавать значения.

Также вы можете кидать туда исключения. То же самое: берете generator object, делаете throw. Кидаете туда ошибку. У вас на месте последнего yield зарейзится ошибка. И close вы можете закрывать генератор. Тогда рейзится эксепшен GeneratorExit, и ожидается, что генератор больше ничего yieldить не будет.



Здесь я просто хотел рассказать про то, как это устроено в CPython. У вас в генераторе на самом деле хранится фрейм выполнения. И как мы помним, FrameObject содержит весь контекст. Из этого, кажется, понятно, как контекст сохраняется. То есть у вас в генераторе просто есть фрейм.



Когда вы выполняете функцию генератора, как Python понимает, что вам нужно не выполнить ее, а создать генератор? В CodeObject, который мы смотрели, есть флаги. И когда вы вызываете функцию, Python чекает ее флаги. Если есть флаг CO_GENERATOR, он понимает, что функцию не надо выполнять, а надо только создать генератор. И он его создает. Функция PyGen_NewWithQualName.



Как происходит выполнение? Из GENERATOR_FUNCTION генератор сначала вызывает GENERATOR_Object. Потом вы GENERATOR_Object можете уже с помощью next вызывать, получать следующее значение. Как происходит вызов next? Из генератора берется его фрейм, он запоминается в переменную F. И отправляется в основной цикл интерпретатора EvalFrameEx. У вас происходит выполнение, как в случае обычной функции. Мапкод YIELD_VALUE используется, чтобы вернуть, поставить на паузу выполнение генератора. Он запоминает весь контекст во фрейме и прекращает выполнение. Это была предпоследняя тема.

(...) Кратко вспомним, что такое исключения и как они используются в Python.

Исключения




Исключения способ обработки ошибочных ситуаций. У нас есть блок try. Мы можем в try записать те вещи, которые могут возбуждать исключения. Допустим, с помощью слова raise мы можем зарейзить ошибку. С помощью except можем ловить определенные типы исключений, в данном случае SomeError. С помощью except мы без выражения ловим все исключения вообще. Блок else используется реже, но он есть и выполнится, только если ни одного исключения не было возбуждено. Блок finally выполнится в любом случае.

Как работают исключения в CPython? Кроме стека выполнения, у каждого фрейма есть еще стек блоков. Лучше рассказать на примере.





Стек блоков это стек, в котором пишутся блоки. У каждого блока есть тип, Handler, обработчик. Handler это адрес байт-кода, на который надо перейти, чтобы обработать этот блок. Как все работает? Допустим, у нас есть некий код. Мы сделали блок try, у нас есть блок except, в котором мы ловим исключения RuntimeError, и блок finally, который должен быть в любом случае.

Это все вырождается вот в такой байт-код. В самом начале байт-кода на блоке try мы видим два два opcode SETUP_FINALLY с аргументами to 40 и to 12. Это адреса обработчиков. Когда выполняется SETUP_FINALLY, в стек блоков помещается блок, в котором написано: чтобы обработать меня, перейди в одном случае на 40-й адрес, в другом на 12-й.

12 ниже по стеку это except, строчка, где есть else RuntimeError. Значит, когда у нас будет исключение, мы будем смотреть стек блоков в поиске блока с типом SETUP_FINALLY. Найдем блок, в котором есть переход на адрес 12, перейдем туда. И там у нас происходит сравнение исключения с типом: мы проверяем, равен ли тип исключения RuntimeError или нет. Если равен мы выполняем его, если нет прыгаем куда-то в другое место.

FINALLY следующий блок в стеке блоков. Он у нас выполнится, если у нас будет еще какой-то exception. Тогда дальше пойдет поиск по этому стеку блоков, и мы дойдем до следующего блока SETUP_FINALLY. Там будет обработчик, который сообщает нам, например, адрес 40. Мы прыгаем на адрес 40 по коду видно, что это блок finally.



В CPython это работает очень просто. У нас все функции, которые могут возбуждать исключения, возвращают код значения. Если все хорошо, возвращается 0. Если это ошибка, возвращается -1 или NULL, в зависимости от типа функции.

Возьмем такую врезку на C. Мы видим, как происходит деление. И есть проверка, что если B равно нулю, а делить на ноль не хочется, то мы запоминаем исключение и возвращаем NULL. Значит, произошла ошибка. Следовательно, все остальные функции, которые находятся выше по стеку вызовов, тоже должны будут выкинуть NULL. Мы это увидим в основном цикле интерпретатора и прыгнем сюда.



Это раскрутка стека. Тут всё, как я говорил: мы просматриваем весь стек блоков и проверяем, что его тип равен SETUP_FINALLY. Если это так прыгаем по Handler, очень просто. На этом, собственно, и всё.

Ссылки


Интепретатор в целом:
docs.python.org/3/reference/executionmodel.html
github.com/python/cpython
leanpub.com/insidethepythonvirtualmachine/read

Управление памятью:
arctrix.com/nas/python/gc
rushter.com/blog/python-memory-managment
instagram-engineering.com/dismissing-python-garbage-collection-at-instagram-4dca40b29172
stackify.com/python-garbage-collection

Исключения:
bugs.python.org/issue17611
Подробнее..

Umka. Жизнь статической типизации в скриптовом языке

21.06.2020 14:07:34 | Автор: admin


В своё время посты на Хабре и Reddit о статически типизированном скриптовом языке Umka вызвали весьма активную дискуссию.

Прошедшие полтора месяца позволили мне избавиться от некоторых заблуждений, развить язык и дать чуть более вразумительные ответы на вопросы публики. Как и следовало ожидать, наиболее серьёзное испытание выпало на долю самой концепции статической типизации. Она осталась в основе языка, но потребовала компромиссов в частности, для корректной сборки мусора.

Появились первые замеры быстродействия интерпретатора в сравнении с Wren и Python их результаты внушают оптимизм. Наконец, родился более реалистичный пример использования Umka по его основному назначению, т. е. как встраиваемого языка.

Информация о типах во время исполнения программы (RTTI). Проект начинался с радикального отказа от хранения типов данных при исполнении программы. Вся информация о типах терялась после компиляции в байт-код виртуальной машины. В принципе, статическая типизация позволяет это сделать, а заодно избавляет от странных трюков вроде упаковки данных в NaN, к которой, например, прибегают создатели JavaScript и Wren ради увеличения быстродействия. Однако обнаружились два случая, в которых пришлось использовать RTTI:

  • Приведение интерфейсного типа данных к конкретному прямой аналог утверждения типа (type assertion) в Go, а также, отчасти, оператора dynamic_cast в C++. Оно требуется и при сборке мусора, содержащегося в данных, приведённых к интерфейсному типу.
  • Сборка мусора, связанного с динамическими структурами данных вроде списков и деревьев.

Быстродействие. Изначально Umka никак не предназначался для установления рекордов быстродействия. Безразличие публики к медлительности Python наводило на мысль, что скорость вовсе не то качество, которого в первую очередь ожидают от скриптового языка. Однако успех LuaJIT и активная реклама Wren заставили задуматься. После этого меня уже не удивляло, что и ранние публикации про Umka вызвали вопросы о быстродействии, хотя мне по-прежнему интересно, от кого в первую очередь исходит спрос на скорость. От разработчиков игр?

Пока полный набор тестов не готов, я могу поделиться лишь предварительными результатами замеров. В численных задачах (например, задаче многих тел) Umka надёжно опережает Python, а если в задаче активно используется цикл for, то Umka даёт выигрыш даже по сравнению с Wren, который позиционируется автором чуть ли не как самый быстрый скриптовый язык после LuaJIT. Наглядным примером служит перемножение больших матриц:


Умножение матриц 400 x 400 (AMD A4-3300M @ 1.9 GHz, Windows 7)

Очевидно, в пользу Umka здесь сыграла поддержка традиционных статических массивов и более низкоуровневая организация цикла for, не содержащая вызовов методов.

Задачи с интенсивной сборкой мусора (например, создание и обход двоичных деревьев) вызывают много сомнений по поводу эквивалентности сравниваемых алгоритмов. Например, известная реализация двоичных деревьев на Python возвращает содержимое узлов россыпью и выглядит так, будто в принципе допускает размещение всего дерева на стеке вообще без использования кучи и сборки мусора. Однако она, по-видимому, требует динамической типизации и не может быть точно воспроизведена на Umka. Если же потребовать возвращать узлы в виде структур, как в Umka (а за неимением структур приходится требовать объекты), то быстродействие Python сразу же падает в 3-4 раза. Вариант на Umka вдвое отстаёт от первой реализации и вдвое опережает вторую. Какое сравнение корректнее не знаю.

Взаимодействие с внешним кодом. Коль скоро язык рассматривается как встраиваемый, понадобился более или менее реалистичный пример взаимодействия кода на C и Umka. В нём средствами игровой библиотеки raylib формируется трёхмерная сцена, а наполнение сцены определяется внешним скриптом на Umka. В примере можно найти и вызов функций Umka из кода на C, и вызов функций C из Umka. Статическая типизация языка Umka позволила естественным образом формировать на нём структуры данных, непосредственно воспринимаемые библиотекой raylib.


Пример трёхмерной сцены, содержимое которой задаётся скриптом на Umka

Обобщённые типы и функции (generics). Как только читатель улавливает сходство Umka с Go, пускай даже синтаксическое следует вопрос о поддержке generic'ов. Работа в этом направлении пока не вышла из стадии обзора подходов. Конечно, хотелось бы воспользоваться предложениями разработчиков Go, однако сосуществование в их головах интерфейсов и контрактов всегда отпугивало, как странное дублирование понятий. К удивлению и радости, в только что вышедшей новой редакции черновика контракты исчезли по тем же причинам, о которых размышлял и я. Пока generic'ов в Umka нет, остаётся пользоваться, как и в Go, пустыми интерфейсами interface{}.

Документация. Полная спецификация Umka ещё в работе, но уже написана грамматика и расширен обзорный тур по основным возможностям языка.
Подробнее..

Простой интерпретатор Lisp на Umka

26.09.2020 22:13:23 | Автор: admin

Разработка моего статически типизированного скриптового языка Umka вошла в ту стадию, когда потребовалась проверка языковых возможностей на более сложных примерах, чем скрипты в пару десятков строк. Для этого я решил реализовать на своём языке интерпретатор Lisp. На это меня вдохновил педагогический эксперимент Роба Пайка, одного из создателей языка Go. Недавно Пайк опубликовал маленький интерпретатор Lisp на Go. Особенно впечатлило замечание Пайка, что описание интерпретатора заключено на одной странице 13 древнего руководства по Lisp 1.5. Учитывая синтаксическое родство Umka и Go, было трудно не поддаться соблазну построить такой интерпретатор на Umka, но не буквальным переносом кода Пайка, а полностью заново, от основ. Надеюсь, знатоки Lisp и функциональных языков простят мне наивное изумление от соприкосновения с прекрасным.

На непосвящённых Lisp может произвести впечатление, близкое к шоку. Где граница между кодом и данными? Где циклы? Где стек? Единственной структурой данных является дерево. Оно же может представлять список. Оно же становится абстрактным синтаксическим деревом при разборе программы. Оно же заменяет собой стек при вычислении выражений. Любое дерево можно попытаться исполнить как код или использовать как данные. Вместо циклов рекурсия. В ядре языка нет даже арифметики. И тем не менее, это полный по Тьюрингу язык, который можно бесконечно расширять и посыпать синтаксическим сахаром.

Определение минимального интерпретатора Lisp действительно занимает меньше страницы. Конечно, с некоторой натяжкой: в нём используются функции, определённые на нескольких предыдущих страницах. Кажется, создатель Lisp Джон Маккарти из азарта старался превзойти сам себя в лаконизме и в итоге опубликовал микроруководство по Lisp, содержащее определение языка вместе с исходником интерпретатора в общей сложности две журнальные страницы. Правда, добавил в заголовок: "Not the whole truth".

Ядро языка (здесь мы говорим о самых старых и простых диалектах) требует наличия пяти элементарных функций, четырёх ключевых слов и двух констант, которые не могут быть выражены средствами самого языка.

Базовые конструкции языка для тех, кто с ними не знаком
  • (car x) выделение головы списка x

  • (cdr x) выделение хвоста списка x

  • (cons x y) соединение списков x и y

  • (atom x) проверка x на атомарность

  • (eq x y) проверка атомарных элементов x и y на равенство

  • (cond (a x) (b y)) выбор значения x или y по условию a или b

  • (quote x) указание использовать x как есть, без вычисления

  • ((lambda (x) a) y) вызов безымянной функции с телом a, формальным параметром x и фактическим параметром y

  • ((label ff (lambda (x) a)) y) присвоение безымянной функции имени ff

  • t истина

  • nil ложь или пустое выражение

С помощью этих конструкций можно определять и вызывать рекурсивные функции, так что в одном выражении будет заключена целая программа. Если к этому добавить ещё и арифметические функции, то можно, например, рассчитать факториал 6:

((label fac (lambda (n) (cond ((eq n 0) 1) ((quote t) (mul n (fac (sub n 1))))))) 6)

В микроруководстве Маккарти этими средствами выражен весь интерпретатор Lisp, за исключением лексического и синтаксического разбора. В руководстве Lisp 1.5 на той самой странице 13 приведён почти такой же интерпретатор, но в более человекочитаемом псевдокоде. Его я и взял за основу своего маленького проекта. Потребовалось лишь добавить разбор текста программы, некое подобие REPL и импровизированную арифметику. Роб Пайк, видимо, поступил так же, но отказался от конструкции label в пользу defn, которая позволила ему не определять функцию заново всякий раз, когда требуется её вызвать. В ядре Lisp такой возможности не предусмотрено.

Поскольку весь интерпретатор пронизан рекурсией и обработкой деревьев, он послужил отличным тестом многих возможностей языка Umka от формирования стековых кадров до сборки мусора. Думаю, Umka хорошо справился с испытанием. Исправлять пришлось лишь два-три мелких бага, связанных с экспортом имён и опережающим описанием типов. Весь код интерпретатора занял меньше 400 строк.

Желающим поиграть с моим интерпретатором и передать по эстафете вдохновение от Роба Пайка рекомендую взять папку с примерами из ветки master, а не из последнего выпуска.

Подробнее..

Перевод Debugging в Julia два способа

08.12.2020 14:22:23 | Автор: admin


скришнот из metal slug 3


2020 год это определенно год странностей. Мой код тоже часто включает в себя некоторые странные ошибки. И в данном посте я хочу показать вам несколько методов отладки кода на языке julia.


Я ни с какой стороны не профессионал в этом деле, да и это справедливо для всего, о чем я пишу в блоге, так что просто имейте это в виду Ну, на самом деле некоторые из вас платят за мою работу, так что технически я могу назвать себя профессиональным блогером, не так ли?


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


Я предполагаю, что у вас есть некоторые базовые знания о Джулии. Следующие посты могут дать вам основы, если вы заинтересованы:



Кроме того, нужно знание базового синтаксиса.


Пример кода


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


В качестве примера возьмем задачку ProjectEuler problem #21. Можете попробовать решить сами. Тут будет начало реализации возможной наивной версии.


Задача заключается в следующем: мы ищем дружественные числа меньше 10 000. Дружественное число определяется как элемент дружественной пары
Пара двух целых чисел (a,b) дружна, если d(a) = b и d(b) = a, где d сумма делителей, так что d(4) = 1+2 = 3.


Дана дружная пара a = 220 и b = 284.
Мы могли бы начать с функции, которая просто берет пару и решает, является ли она дружественной.


function is_amicable(a, b)    sum_divisors(a) == b && sum_divisors(b) == aend

Джулия всегда возвращает выходные данные последнего выполненного выражения в функции. Это означает, что ключевое слово return не обязательно.

Затем нам понадобится функция sum_divisors


function sum_divisors(a)    result = 0    for i = 1:a        if a % i == 0            result += i        end    end    return resultend

которая вызывается так


julia> is_amicable(220, 284)false

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


Отладка с помощью Debugger.jl в REPL


Этот пост показывает вам два различных варианта отладки, и первый вариант может быть выполнен в REPL или в вашей IDE, то есть VSCode.


В этом разделе я объясню, как работать с отладчиком на REPL. (Debugger.jl)


julia> ] add Debuggerjulia> using Debugger

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


julia> @enter is_amicable(220, 284)In is_amicable(a, b) at REPL[7]:1 1  function is_amicable(a, b)>2      sum_divisors(a) == b && sum_divisors(b) == a 3  endAbout to run: (sum_divisors)(220)1|debug> 

Я набил @enter is_amicable(220, 284), чтобы получить этот вывод. Кстати, я только что скопировал две функции, которые я определил ранее, в REPL. С другой стороны, Вы можете создать для этого файл amicable.jl и использовать Revise и include (см. REPL and Revise.jl).


В случае файла номера строк, вероятно, более полезны.


Я вернусь через секунду...


julia> using Revisejulia> includet("amicable.jl")julia> using Debuggerjulia> @enter is_amicable(220, 284)In is_amicable(a, b) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:1 1  function is_amicable(a, b)>2      sum_divisors(a) == b && sum_divisors(b) == a 3  endAbout to run: (sum_divisors)(220)1|debug> 

Готово. Хорошо, теперь как уже упоминалось, в конце мы собираемся запустить sum_divisors(220).


Последняя строка 1|debug> дает нам возможность исследовать дальше, прыгая по коду, в том числе и низкоуровневому, и много чего еще всякого интересного.
Можно посмотреть полный список команд: Debugger.jl commands


Вы также можете ввести ? в режиме отладчика и нажать клавишу enter, чтобы увидеть список команд

Давайте начнем с n шаг к следующей строке.


1|debug> nIn is_amicable(a, b) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:1 1  function is_amicable(a, b)>2      sum_divisors(a) == b && sum_divisors(b) == a 3  endAbout to run: return false

Значит sum_divisors(220) != 284. Мы, вероятно, хотим перейти к вызову sum_divisors(220).


Мы всегда можем выпрыгнуть из сеанса отладки с помощью q, а затем начать все сначала
Начнем снова с @enter is_amicable(220, 284) и используем s для шага в функцию


1|debug> sIn sum_divisors(a) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:5  5  function sum_divisors(a)> 6      result = 0  7      for i = 1:a  8          if a % i == 0  9              result += i 10          endAbout to run: 01|debug> 

А дальше продолжаем использовать n, но вы, вероятно, можете себе представить, что это займет некоторое время.


Какие еще инструменты у нас есть, чтобы проверить, что происходит?


Некоторые из вас могут подумать: Хорошо, мы должны, по крайней мере, выяснить, что мы возвращаем, и мы можем просто вызвать sum_divisors(220). Это, вероятно, правильно, но не показывает возможности отладчика. Давайте представим, что мы имеем доступ только к режиму отладчика и не можем просто вызвать функцию.


В общем, этот способ узнавания нового, скрывая то, что мы уже знаем, довольно эффективен.


Я думаю, что пришло время, чтобы представить силу точек останова.


Вместо того, чтобы ползти по программе строка за строкой, часто разумно перейти к определенной точке, запустив код до тех пор, пока эта точка не будет достигнута.


Вы можете сделать это с помощью bp add, а затем указать файл, номер строки и возможное условие. Вы можете увидеть все параметры с помощью ? в режиме отладки.


В наших интересах будет поставить bp add 12. После этого мы можем использовать команду c, которая расшифровывается как continue (до точки останова).


1|debug> cHit breakpoint:In sum_divisors(a) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:5  8          if a % i == 0  9              result += i 10          end 11      end>12      return result 13  endAbout to run: return 504

Итак, теперь мы знаем, что оно возвращает 504 вместо 284. Теперь мы можем использовать `, чтобы перейти в режим Джулии. (Я знаю, что это вроде как запрещено нашими правилами, но время от времени это имеет смысл, и мы видим, что мы находимся в 1|julia>, а не в julia>, так что я думаю, что все в порядке...)


504-284 это не самый сложный расчет, но мы можем использовать julia, чтобы сделать это за нас, не выходя полностью из режима отладки, используя:


1|debug> `1|julia> 504-284220

Похоже, мы нашли ошибку. Мы добавляем само число к результату, но оно на самом деле не считается за множитель.


А это значит, что мы можем сделать:


function sum_divisors(a)    result = 0    #for i = 1:a    for i = 1:a-1        if a % i == 0            result += i        end    end    return resultend

чтобы избежать эту проблему.


Да, я знаю, что мы можем избежать большего количества чисел, чтобы быть быстрее

Мы можем выйти из режима вычислений с помощью backspace, а затем q, чтобы выйти из режима отладки. Запускаем


julia> is_amicable(220, 284)true

и видим, что мы выковыряли этот баг.


Давайте запустим его в последний раз в сеансе отладки и посмотрим на переменные. Снова перейдем к точке останова c и запустим


1|debug> w add i1] i: 2191|debug> w add a1] i: 2192] a: 220

Теперь мы видим переменные. Если мы снова нажмем c, то снова перейдем к точке разрыва (для очередного вычисления sum_divisors(284) == 220).
Мы можем снова использовать букву w, чтобы увидеть список переменных в области видимости:


1|debug> w1] i: 2832] a: 284

Есть еще несколько способов поиграть, например, шагнуть в код, показать низкоуровневый код и многое другое. Этого должно быть достаточно для знакомства.
В следующем разделе я хочу привести вам тот же пример с помощью редактора кода visual studio с расширением julialang.


Использование VSCode


Я думаю, что большинство разработчиков Julia используют VSCode IDE и, по крайней мере, иногда, vim, emacs или еще что-то такое неудобное Ладно, это, наверное, просто слишком неудобно для меня


Определенно пришло время переключиться на VSCode с Atom/Juno, поскольку расширение Julia теперь разработано для VSCode вместо Atom.


Поскольку это IDE, имеет смысл иметь более визуальный отладчик, чем тот, который описан в предыдущем разделе.



Он довольно прост в навигации, и по умолчанию вы получаете больше выходных данных.


Чтобы начать сеанс отладки, вы нажимаете на кнопку с ошибкой и воспроизводите знак слева, пока у вас открыт файл julia.
Я добавил последнюю строку is_amicable(220, 284), так как VSCode просто запускает программу.


Вы можете добавить точку останова, щелкнув слева от номера каждой строки.


Я сделал снимок экрана после того, как сделал эти шаги, и последним шагом было нажатие на кнопку отладки.


Через несколько секунд сеанс отладки приостанавливается по мере достижения точки останова. С левой стороны можно увидеть локальные переменные в этой позиции. Это этап после того, как я исправил ошибку, так что вы можете видеть, что возвращается правильный результат "284". Однако вы также получаете значение для a и i.


Короче, все то же, что мы делали раньше с нашими переменными, но там нам пришлось вручную добавлять их.


Теперь мы также можем вручную добавлять выражения для наблюдения. Это можно сделать в части Watch ниже Variables, которая находится за пределами скриншота. Довольно приятно иметь возможность добавлять точки останова одним щелчком мыши, а также иметь локальные переменные, показанные слева по умолчанию.


Вы можете спросить себя: Ну, на самом деле это не два способа отладки, не так ли? Это примерно одно и то же, только с другим графическим интерфейсом.


Это правда! Вот почему я сейчас перехожу к следующему разделу поста


Infiltrator.jl для скорости


Существует одна огромная проблема с отладчиком Julia, которая решается по-разному различными пакетами. Проблема в том, что отладчик работает в интерпретируемом режиме, что делает его очень медленным. Если вы отлаживали код C++, то знаете, что отладчик там тоже работает медленнее, чем выполнение, но для Джулии это, на мой взгляд, огромная проблема.


Можно перейти в скомпилированный режим с отладчиком, но это экспериментально, и, по крайней мере, для меня он никогда не останавливался на точке останова.


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


Это также один из тех проектов с менее чем 100 звездами. Я хочу подтолкнуть его к этой вехе, так что если вам нравится то, что вы видите в этом разделе, пожалуйста, щелкните им звездочку.


Infiltrator.jl идет совершенно другим путем. Прежде всего, вам нужно немного изменить свой код. Он предоставляет макрос @infiltrate. О боже, как я люблю это название


Макрос примерно такой же, как и предыдущая точка останова. Всякий раз, когда достигается нужная строка, открывается новый вид режима REPL. Это немного усложняет переключение между режимом отладки и обычным режимом запуска, так как вам нужно добавить или удалить макросы @infiltrate, но я думаю, что это нормально.


Я снова продемонстрирую это на примере разобранном выше. Подобного рода использование было в debugging ConstraintSolver.jl.


Я скопировал код сверху и просто добавил using Infiltrator и @infiltrate.


using Infiltratorfunction is_amicable(a, b)    sum_divisors(a) == b && sum_divisors(b) == aendfunction sum_divisors(a)    result = 0    for i = 1:a-1        if a % i == 0            result += i        end    end    @infiltrate    return resultendis_amicable(220, 284)

При запуске кода с include("amicable.jl") получаем:


Hit `@infiltrate` in sum_divisors(::Int64) at amicable.jl:14:debug> 

Это означает, что мы знаем, какая точка останова была достигнута, и видим тип переменной, которую мы назвали sum_divisors. Однако в отличие от Debugger.jl мы не видим кода.


Вы можете снова увидеть раздел справки с помощью ?


debug> ?  Code entered is evaluated in the current function's module. Note that you cannot change local  variables.  The following commands are special cased:    - `@trace`: Print the current stack trace.    - `@locals`: Print local variables.    - `@stop`: Stop infiltrating at this `@infiltrate` spot.  Exit this REPL mode with `Ctrl-D`, and clear the effect of `@stop` with `Infiltrator.clear_stop()`.

Существует не так уж много команд, поэтому мы можем просто попробовать их одну за другой:


debug> @trace[1] sum_divisors(::Int64) at amicable.jl:14[2] is_amicable(::Int64, ::Int64) at amicable.jl:4[3] top-level scope at amicable.jl:18[4] include(::String) at client.jl:457

Таким образом, мы пришли из is_amicable и можем видеть типы, а также имя файла и номер строки, что полезно при использовании multiple dispatch.


debug> @locals- result::Int64 = 284- a::Int64 = 220

мы можем видеть локальные переменные, которые похожи на те, которые мы видели в представлении переменных VSCode.


Кроме того, мы можем просто вычислять выражения прям в этом режиме. Для Infiltrator.jl нет необходимости использовать `, чтобы переключиться на вычисления.


debug> a == 220true

Вы можете использовать @stop, чтобы больше не останавливаться на этой вехе, и Infiltrator.clear_stop(), чтобы очистить эти остановки.


Давайте не будем использовать @stop сейчас, а вместо этого перейдем к следующей точке @infiltrate с помощью CTRL-D:


Hit `@infiltrate` in sum_divisors(::Int64) at amicable.jl:14:debug> 

таким образом, мы находимся в той же точке останова, но со вторым вызовом. К сожалению, невозможно использовать клавишу со стрелкой вверх, чтобы перейти через историю команд, которые мы использовали, так что нам нужно снова ввести @locals, если мы хотим их увидеть.


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


Давайте рассмотрим сравнение двух различных способов в следующем разделе.


Выводы


Мы посмотрели на Debugger. jl, который дает вам всю информацию, которая может понадобиться в вашем REPL.
Поэтому он не зависит от редактора.


Следующий инструмент, который я упомянул, был дебагер в VSCode который является в основном просто графическим интерфейсом для Debugger.jl. Вероятно, его удобнее использовать людям, которые любят работать с IDE. Хотя в Debugger.jl могут быть некоторые опции, которые недоступны в графическом интерфейсе, как это часто бывает.


Оба этих инструмента дают то преимущество, что вы можете шаг за шагом переходить через свой код и исследовать все, что захотите. Вы можете взглянуть на низкоуровневый код (по крайней мере, в Debugger.jl). Это, вероятно, то, что каждый ожидает сделать с отладчиком. Проблема просто в том, что он слишком медленный во многих случаях использования, например, когда вы хотите отладить свой собственный пакет с 1000 строками кода.


В таком случае Infiltrator.jl это путь, по крайней мере для меня, и пока скомпилированный режим Debugger.jl работает недостаточно хорошо. У него есть и другие недостатки, так как не бывает чтоб все и сразу, но я думаю, что он часто превосходит использование println, поскольку можно распечатать все, что в данный момент интересует в данной точке останова, и увидеть все локальные переменные за один раз.


Спасибо за то, что дочитали и особая благодарность моим 10 покровителям!


Я буду держать вас в курсе Twitter OpenSourcES.

Подробнее..

Почему за интерпретируемыми языками будущее

31.05.2021 00:23:47 | Автор: admin

Зададимся вопросом, что такое прогресс? Википедия говорит нам следующее:

Прогресс, проградация(лат.pro движение вперёд, успех;grad шагать, ступать;gradatio постепенное повышение) направление развития от низшего к высшему, положительная динамика, поступательное движение вперед, повышение уровня организации, усложнение способа организации, характеризуется увеличением внутренних связей. Противоположность регресс,деградация

Люди, общество, социум испокон веков стремилось к познанию, открытию, изучению. Вы когда- нибудь задавались вопросом, почему? Почему мы прогрессируем? Что это, код, заложенный в нас природой на генном уровне или чей-то промысел? От каменного оружия до нанотехнологий, от лечения травами, до пересадки сердца, постепенно, со временем, но мы улучшаем и облегчаем нашу жизнь! Вот, два ключевых слова, улучшаем и облегчаем, именно эти два слова, являются нашим двигателем, двигателем развития и прогресса. О чем вы подумаете, если я вам скажу, что война, это тоже необратимая часть прогресса? История, является отражением нашего с вами развития, наших поступков и достижений, от простейшего организма, до организма со сложнейшей структурой. Мы делаем выводы, мы учимся, словно нейросеть. Допускали ли вы, что человек это и есть искусственный интеллект. Забавно, но человек, подобно Богу, стремится создать существо, по образу и подобию своему.

В начале было слово, в нашем же случае, был машинный язык. На протяжении 1950-х годов разработка программ для машин увеличивалась, код усложнялся, структура становилась сложнее. Как итог, люди пришли к созданию первого высокоуровневого языка программирования Фортран, и уже в след за ним начали появляться и другие языки, началось улучшение и развитие инструмента взаимодействия с машиной, причина, следствие. Добавим немного Википедии:

Эти языки более удобны для человека, с помощью них можно писать программы до нескольких тысяч строк длиной. Условными словами можно было, как привычно человеку, гораздо более просто выразить сложную программную операцию из битов.

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

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

"Всё гениальное просто, и всё простое гениально. Маленькому человечку нравится скрывать свою ничтожность за сложными вещами." Йозеф Геббельс

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

Да, интерпретируемые языки, такие как Python, Ruby, являются "медленными", в сравнении с такими компилируемыми языками, как Java, С++. Скорость работы программ, написанных на них, позволяет нам управлять ракетами, и мы безусловно, должны чтить и уважать их, ибо благодаря им мы имеем, что имеем. Но дело не только в скорости работы программ, но и в простоте написания и скорости разработки. Instagram, Hulu, Twich, Github, Airbnb, Shopify, Reddit, Spotify, все это написано на интерпретируемом языке и я сомневаюсь, что кто-то станет писать Instagram на C++/C, только потому что он быстрее Python, также, верно и обратное утверждение, на Python ни кто не будет писать операционную систему или драйвера. Однако, человеческая природа хочет облегчать и совершенствовать различного рода процессы. Это заложено в нас, идти в перед, развиваться и я не на миг не сомневаюсь, что лет через 100, языки программирования будут подобно человеческому, скорость которых будет сравнима скорости машинному языку. И все это, благодаря человечеству, который подобно организму, развивается, обучается и улучшается.

Так почему же я считаю, что за интерпретируемыми языками будущее? Потому что, они несут философию простоты, понимания и человечности, уже сейчас мы можем наблюдать, как они развивают наш мир и наше общество. И именно эти языки будут прародителями последующих.

"Я хочу, чтобы компьютер был моим слугой, а не господином, поэтому я должен уметь быстро и эффективно объяснить ему, что делать. " Мацумото Юкихиро

"В первую очередь вы пишете код, который будет коммуницировать с другими разработчиками, и в меньшей степени, навязывать свою волю компьютеру." Гвидо ван Россум

"Скорость работы больше не является главной проблемой. Как быстро вы зайдёте на рынок, да." Дин Хуэй

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru