В своей предыдущей статье я исследовал структуру PyObject и её роль в качестве заголовка для всех объектов среды исполнения CPython. Эта структура играет важнейшую роль в обеспечении наследования и полиморфизма в системе объектов CPython. Но это лишь вершина айсберга.

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

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

Высокоуровневая структура динамической диспетчеризации в CPython

Когда мы пишем на Python код наподобие a + b, конкретное поведение операции + определяют типы a и b. Каждый тип в Python имеет собственную реализацию оператора + (если этот тип поддерживает +), и интерпретатор Python сам выбирает подходящую реализацию для вызова на основании типа операндов. Весь этот процесс в языках программирования называется динамической диспетчеризацией. На схеме ниже показано высокоуровневое описание того, как это работает в CPython:

High level flow of operator execution in the CPython VM
Высокоуровневый поток исполнения оператора в CPython VM. Реализация выбирается в зависимости от того, какой тип имеют a и b: int, float или сложный тип

Давайте вкратце рассмотрим части этой схемы:

  • Код на Python компилируется в байт-код, исполняемый стековой виртуальной машиной (VM) в CPython. Команда BINARY_OP отвечает за исполнение операции + с двумя операндами, a и b.

  • Сама VM не знает, как выполнять + с двумя объектами. Она делегирует эту задачу абстрактному интерфейсу объектов.

  • Абстрактный интерфейс объектов в CPython определяет интерфейс, поддерживающий стандартные операции уровня объектов в CPython. Это позволяет VM единым унифицированным образом исполнять все операторы, не зная подробностей реализации системы объектов. Абстрактный интерфейс диспетчеризирует исполнение конкретной реализации внутри типов при помощи таблицы поиска указателей функций в заголовке объекта (подробнее об этом позже).

В предыдущей статье мы вкратце исследовали часть этого потока, рассмотрев реализацию команды BINARY_OP в VM CPython. На этот раз наша цель — по-настоящему разобраться в том, как происходит динамическая диспетчеризация, поэтому мы будем двигаться внизу вверх.

Сначала мы рассмотрим, как разные типы реализуют различные операторы, затем взглянем на абстрактный интерфейс объектов и узнаем, как он вызывает эти конкретные реализации, а в конце расскажем, как VM CPython интегрируется с абстрактным интерфейсом объектов.


Анализ структуры PyTypeObject

Структура PyTypeObject — это второй строительный блок системы объектов CPython (первый — это PyObject). Она содержит информацию о типе среды исполнения про объект. Прежде чем рассматривать динамическую диспетчеризацию в CPython, нам сначала нужно понять, что находится внутри PyTypeObject.

Но сначала повторим пройденное и разберём определение PyObject; здесь на сцене появляется PyTypeObject:

Definition of PyObject struct
Определение структуры PyObject

Кроме того, каждое определение типа в CPython содержит PyObject в качестве первого поля в виде заголовка. Например, вот определение типа float:

Definition of float type in CPython
Определение типа float в CPython

Это значит, что каждый такой объект может быть приведён к типу PyObject (чтобы понять, как это сделать, см. предыдущую статью о PyObject), а поскольку PyObject содержит указатель на PyTypeObject, среда исполнения CPython имеет всю связанную с типами информацию об объекте, которая доступна ей постоянно.

Теперь давайте заглянем внутрь PyTypeObject. Это очень большой объект с десятками полей. На этом изображении показано его полное определение:

Definition of the PyTypeObject struct
Определение структуры PyTypeObject

Структура PyTypeObject хранит подробности о типе среды исполнения об объекте: имя типа, размер типа, функции для распределения и освобождения объекта этого типа.

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

Мы хотим понять, как CPython исполняет бинарный оператор сложения (+), поэтому внимательнее присмотримся к тому, что находится внутри PyNumberMethods. Его определение показано на изображении ниже:

Заглядываем внутрь структуры PyNumberMethods. В поле слева представлено развёрнутое определение PyTypeObject, в котором выделено поле PyNumberMethods. В поле справа показано частичное определение PyNumberMethods
Заглядываем внутрь структуры PyNumberMethods. В поле слева представлено развёрнутое определение PyTypeObject, в котором выделено поле PyNumberMethods. В поле справа показано частичное определение PyNumberMethods

Каждая реализация типа в CPython должна создать экземпляр структуры PyNumberMethods и заполнить его указателями на функции, которые она реализует для поддержки числовых операций. Если тип не поддерживает числовые операции, она может просто присвоить полю tp_as_number в PyTypeObject значениеNULL; это сообщит среде исполнения CPython, что объект не поддерживает эти операции.

Дальше в качестве конкретного примера рассмотрим то, как эти функции реализует тип float , а затем создаёт экземпляр PyTypeObject при создании нового объекта float.


Создание экземпляров типов Float при помощи PyNumberMethods

На изображении ниже показан код из Objects/floatobject.c, содержащий реализацию типа float в CPython.

How the float type implements numeric operators and populates the function pointer table in its instance of PyTypeObject, called PyFloat_Type
Как тип float реализует числовые операции и заполняет таблицу указателей функций в своём экземпляре of PyTypeObject под именем PyFloat_Type

Давайте разберём это:

  • В левом поле показаны функции, реализующие операции сложения, вычитания и умножения.

  • В среднем поле показан экземпляр структуры PyNumberMethods (с именем float_as_number) для типа данных float. Обратите внимание, что он содержит указатели функций для функций сложения, умножения и вычитания.

  • В правом поле показан экземпляр PyTypeObject для создания объектов типа float. Обратите внимание, что он содержит указатель на объект float_as_number.

Кроме того, указатель на float_as_number включён в каждый заголовок объекта float (например, как значение поля ob_type в PyObject). На рисунке ниже показана функция PyFloat_FromDouble, создающая новые объекты типа float и использующая float_as_number для инициализации заголовка объекта.

How an instance of the float type is created. Notice how it passes a pointer to PyFloat_Type in the call to _PyObject_Init to set its type.
Как создаётся экземпляр типа float. Обратите внимание, как он передаёт указатель на PyFloat_Type в вызове _PyObject_Init, чтобы задать его тип. Первый выделенный блок: интерпретатор хранит кэш неиспользуемых объектов float и по возможности использует их повторно. Второй выделенный блок: распределение памяти для объекта float. Третий выделенный блок: _PyObject_Init присваивает счётчику ссылок указанного объекта значение 1 и присваивает его полю ob_type значение указателя на тип передаваемого им объекта. В этом случае ему передаётся указатель на PyFloat_Type

Изображение выше достаточно подробное и понятное, поэтому не будем больше тратить на это время. Но это именно тот код, который исполняется при записи кода a = 3.14 на Python.

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


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

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

Абстрактный интерфейс объектов в CPython

CPython определяет абстрактный интерфейс объектов для унификации доступа к конкретным реализациям типов. Это обеспечивает чистоту кода VM, потому что она просто делегирует исполнение оператора этому интерфейсу.

Этот абстрактный интерфейс определяется в файле Include/abstract.h, а на изображении ниже показаны объявленные в нём числовые функции:

The abstract.h declares the abstract object interface, which includes functions for all the common object level operations in CPython. This figure shows a partial list of numeric operations as declared in abstract.h
Файл заголовка abstract.h объявляет абстрактный интерфейс объектов, который содержит функции для всех стандартных операций уровня объектов в CPython. На этом изображении показан частичный список числовых операций, объявленных в abstract.h

abstract.h — это файл заголовка, поэтому он только объявляет прототипы этих функций. Реализации этих функций находятся в файле Objects/abstract.c. Мы рассмотрим только реализацию функции PyNumber_Add в нём, которую VM вызывает для исполнения оператора +. На изображении ниже показан её код с объяснениями происходящего:

abstract.c contains implementations of the functions declared in abstrac.h. This figure shows implementation of the PyNumber_Add function
abstract.c содержит реализации функций, объявленных в in abstrac.h. На этом изображении показана реализация функции PyNumber_Add. Первый выделенный блок: сначала пробуем выполнить бинарную операцию сложения с аргументами v и w. Если внутренняя impl аргументов v и w поддерживает операцию сложения, возвращаем результат. Второй выделенный блок: если объекты не поддерживают бинарного сложения, тогда проверяем, являются ли они объектами типа «последовательность» (например, строкой, списком и так далее...). Если да, то вызываем для них функцию concat. Третий выделенный блок: в противном случае возвращаем TypeError

Операция + поддерживается двумя классами типов данных в Python: числовыми типами (intfloatcomplex и так далее) и типами «последовательность» (listtuple и так далее).

ФункцияPyNumber_Add сначала пытается вызвать для аргументов реализацию бинарного сложеняи. Если эти типы не поддерживают бинарного сложения, то она пытается проверить, являются ли эти они типами «последовательность», и если это так, то пытается вызвать для них функцию concat.

Давайте сосредоточимся на числовых типах. Для числовых типов функция PyNumber_Add вызывает макрос BINARY_OP1, который просто вызывает функцию binary_op1. На изображении ниже показана binary_op1:

The rest of the implementation of PyNumber_Add in abstract.c
Остальная часть реализации PyNumber_Add в abstract.c. Первый выделенный блок: slotv и slotw — это указатели на функции, которые реализуют бинарные операции, соответственно, в v и w. Второй выделенный блок: если v — числовой тип, выполняем поиск указателя функции в его таблице PyNumberMethods. Третий выделенный блок: если v и w имеют разные типы и w является числовым, то выполняем поиск указателя функции в таблице PyNumberMethods w. Однако также возможно, что и slotv, и slotw указывают на одну функцию. В таком случае достаточно только одной из них, так что сбрасываем slotw на NULL. Четвёртый выделенный блок: теперь, когда мы знаем, какие функции нужно вызвать для выполнения бинарной операции, их можно вызвать. Но это необходимо делать в следующем порядке: 1. если бинарную операцию реализует только v, то есть slotv ≠ NULL И slotw = NULL, то мы просто можем вызвать slotv. 2. Однако если бинарную операцию реализуют и v, и w, а w является подтипом v, то мы должны вызвать реализацию бинарной операции w. Это важно, потому что если вы захотите перегрузить оператор собственным поведением в классе, то это обеспечит его выполнение. 3. Наконец, если бинарную операцию реализует только w, то мы вызываем slotw

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


Мы узнали, как тип реализует различные операторы и как абстрактный интерфейс объектов упрощает динамическую диспетчеризацию этих реализаций. Теперь мы вернёмся к VM CPython и посмотрим, где она вызывает абстрактный интерфейс объектов для выполнения оператора.

Привязка выполнения оператора к абстрактному интерфейсу объектов в VM CPython

Это последняя часть, где VM CPython интегрирует выполнение оператора с абстрактным интерфейсом объектов. Частично мы говорили об этом в предыдущей статье, когда разбирались, как структура PyObject помогает симулировать полиморфизм. На этот раз мы разберём это полностью. Но давайте начнём сначала.

На изображении ниже показана простая функция на Python и её команды в байт-коде:

How the CPython stack based VM executes instructions
Как стековая VM CPython исполняет команды. Locals — список символов, локальный для текущего контекста исполнения. Проще говоря, там хранятся локальные переменные и параметры сложения

Мы рассмотрим команду байт-кода BINARY_OP. На изображении ниже показано, как она обрабатывается VM:

Implementation of the BINARY_OP instruction in the CPython VM
Реализация команды BINARY_OP в VM CPython. Для бинарной операции lhs и rhs получаются извлечением двух верхних значений из стека. BINARY_OP — это обобщённая команда, а конкретная исполняемая операция передаётся как аргумент команды, который доступен в переменной oparg. Каждой бинарной операции назначается числовой код, например, сложение — это 0, умножение — 5 и так далее. binary_ops — это массив указателей функций, индексируемый по опкоду бинарных операций. То есть при помощи значения oparg мы можем найти функцию, которую нужно вызывать для выполнения текущей бинарной операции. После завершения использования lhs и rhs можно выполнить декремент из счётчиков ссылок. Мы изменяем стек так, чтобы lhs и rhs удалились, а в верхнем значении сохранился результат бинарной операции.

Я не буду тратить время на объяснение всего этого, потому что мы рассмотрели это в предыдущей статье. Однако если уж мы коснулись самого выполнения бинарной операции, давайте взглянем на код, потому что именно там VM делегирует задачи abstract.c.

В приведённом выше коде мы видим следующую строку:

res = binary_ops[oparg](lhs, rhs);

Этот код выполняет поиск указателя функции в таблице binary_ops, используя в качестве индекса опкод бинарного оператора, и вызывает эту функцию. Давайте взглянем на эту таблицу, определяемую в файле ceval.c (в котором реализована основная часть кода исполнения VM).

The binary_ops table in CPython VM which indexes the operator functions as defined in abstract.c using the binary operators as the index
Таблица binary_ops в VM CPython, которая индексирует функции операторов, определённые в abstract.c, используя в качестве индекса бинарные операторы

Каждый указатель функции в таблице binary_ops указывает на функцию, которая реализована в Objects/abstract.c. В предыдущем разделе мы уже видели определение PyNumber_Add в abstract.c, и поняли, как он выполняет динамическую диспетчеризацию оператора в зависимости от типов операндов.

Именно так VM делегирует выполнение бинарных операторов реализации абстрактного интерфейса, которая в конечном итоге производит динамическую диспетчеризацию при помощи поиска указателя функции в таблицах, находящихся в заголовках объектов.


Подведём итог

Вот и всё! Именно это происходит на самом деле, когда вы исполняете a + b в коде на Python. Давайте вкратце подытожим:

  • Каждый тип реализует функции для поддерживаемых им операторов и заполняет таблицу указателей функций в его заголовке (то есть поле PyTypeObject внутри PyObject).

  • В зависимости оператора VM CPython вызывает функцию в абстрактном интерфейсе объектов.

  • Абстрактный интерфейс объектов (при вызове из VM) выполняет поиск в таблице указателей функций в заголовке объектов операндов и вызывает соответствующую функцию.


В заключение

Это было краткое знакомство со ВСЕМ кодом CPython, который задействуется при исполнении чего-то простого, например, a + b в коде на Python. Хотя информации довольно много, её не так сложно понять, если вы разбираетесь в указателях функций.

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

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


  1. x_arrange
    13.12.2023 17:23

    так сколько строк-то?)


    1. Lazytech
      13.12.2023 17:23

      так сколько строк-то?)

      Если вкратце, точно неизвестно, сколько; известно только, что много.

      https://news.ycombinator.com/item?id=38600216
      whizzter 2 days ago
      If you read the article you can see that some codepaths can invoke Malloc with all the follow-on effects like Kernel boundary crossings that this implies, it's thus quite random.

      https://news.ycombinator.com/item?id=38634480
      whizzter 7 hours
      Depends, I look at it from a performance standpoint when starting to count lines/instructions, not just directly executed code but also how feasible it would be to translate the thing to a JIT for example, the amount is large enough that going to a JIT would yield little (this is why there has been so many Python JIT's that has failed to gain enough performance and hence traction) before mayor architectural fixes are made.
      Not only is there branches to a ton of special things but also macros that hides even more lines (IncRef/DecRef probably has a lot of magic behind there).


    1. idimus
      13.12.2023 17:23

      Можно скачать сорцы питона, собрать их с lcov и посмотреть, что будет для разных типов. Потом написать скрипт, который пройдет по репорту и сделает операцию +(желательно разные репорты для разных типов).

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

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


  1. eri
    13.12.2023 17:23

    А если скомпилировать через cython ? В этом случае правда код не прогоняется через виртуальную машину.


    1. Kreastr
      13.12.2023 17:23

      Если ничего не делать - ничего не изменится. Чтобы код в cython стал занимать 1 строку всем переменным (a, b и результат) должны быть определены типы (например через cdef)


  1. dyadyaSerezha
    13.12.2023 17:23

    1) Я пропустил или рассказан шаг приведения типов, как например для случая int + float или float + int?

    2) В некоторых местах кривой или неверный перевод;

    "Структура PyTypeObject хранит подробности о типе среды исполнения об объекте" - это как понять по-русски?


    1. lamerok
      13.12.2023 17:23

      1} В разделе Создание экземпляров типов Float при помощи PyNumberMethods

      Вы пропустили, внутри самой функции сложения float_add() вызывается CONVERT_TO_DOUBLE()


      1. dyadyaSerezha
        13.12.2023 17:23

        То есть, при 1 + 1 числа переводятся в double?


    1. Lazytech
      13.12.2023 17:23

      "Структура PyTypeObject хранит подробности о типе среды исполнения об объекте" - это как понять по-русски?

      Забавно, гуглопереводчик справился лучше:

      Структура PyTypeObject хранит сведения о типе объекта во время выполнения, такие как имя типа, размер типа, функции для выделения и освобождения объекта этого типа.


      1. rukhi7
        13.12.2023 17:23

        Забавно, гуглопереводчик справился лучше:

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


        1. Lazytech
          13.12.2023 17:23

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


        1. aspect04tenor
          13.12.2023 17:23

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


          1. rukhi7
            13.12.2023 17:23

            да я тут да, не сконцентрировался, написал что-то не очень подумав

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


  1. PocoLoco
    13.12.2023 17:23

    Полезная информация. Ещё хотелось бы узнать сколько полупроводников задействовано в сим происходящем.


    1. AcckiyGerman
      13.12.2023 17:23

      Зависит от камня вестимо.


      1. Breathe_the_pressure
        13.12.2023 17:23

        Меня также интересует количество электронов задействованных в этом бардаке? Электрон сейчас дорог знаете ли.


        1. domix32
          13.12.2023 17:23

          Так он один на всех.


        1. Serge78rus
          13.12.2023 17:23

          Электрон сейчас дорог знаете ли.

          Зато дырки пока что бесплатны. Для снижения издержек надо больше использовать полупроводники p-типа )


          1. Breathe_the_pressure
            13.12.2023 17:23

            Дороже всего нам обходятся только бесплатные дырки. ;)


            1. Boilerplate
              13.12.2023 17:23

              Борзый какой, пойдем за орбиталь выйдем


  1. chnav
    13.12.2023 17:23

    "И эти люди запрещают мне ковыряться в носу"

    Сдаётся мне что даже Visual Basic был эффективнее с точки зрения производительности. В тезис про упрощение жизни програмиста я тоже не особо верю т.к. изучить весь зоопарк библиотек Питона практически нереально, а без доступа 24/7 к StackOverflow и Гуглу вообще невозможно.


    1. firehacker
      13.12.2023 17:23

      Почему «даже»?

      Вопреки гуляющим в сети бредням, что VB интерпретируемый язык, по умолчанию при генерации исполняемых файлов среда генерировала IL-код в том формате, в каком происходило взаимодействие между фронтендом и бэкендом компилятора Microsoft Visual C/C++ — и этот IL скармливался бэкенду.

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

      Если не быть чудаком и использовать типизированные переменные скалярных типов, а не тип Variant — код получался настолько же хорошим, как аналогичный сильный код, с той оговоркой, что в код вставлялись проверки, чтобы не выстрелить себе в ногу. Да и то, эти проверки были весьма легковесными — например, проверка на переполнение после операции сложения делалась одной единственной инструкцией JO/JNO, проверяющей OF. Но и подобный проверки во многом можно было отключить в настройках оптимизации.

      Не по умолчанию — это компиляция в P-code. Так назывался байт-код собственной виртуальной машины VB/VBA. Этот вариант всегда использования в режиме отладки, потому что позволяло чрезвычайно бонато и гибко править код по живому в процессе запуска в режиме отладки. В этом случае операции проводились короткими обработчиками соответствующих P-codeных инструкций, которые были написаны на ассемблере и вылизаны, насколько это было возможно.

      Операции над типом Variant в любом случае выполнялись при помощи API-функций ил OLEAUT32.DLL.


      1. chnav
        13.12.2023 17:23

        Спасибо. Я хотел сравнить с интерпретатором, неловко получилось ))


        1. firehacker
          13.12.2023 17:23

          Если продолжать: VB (и в равной степени VBA) это, пожалуй, уникальная среда, где интерпретирование кода (парсинг, разбор) происходило в момент набора кода в редакторе, и с тех пор, как программист нажал Enter (или другим способом перешёл на другую строку), строка кода под капотом вообще больше не существует как текст.

          Лишь для целей отрисовки на экране избранные строки реконструировались обратно в цветной текст из исходно не текстового представления.

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


    1. Dima_Sharihin
      13.12.2023 17:23

      то ли дело luajit

      local function f(a, b) return a + b end
      local s = 0
      for i = 1, 512 do s = f(s, i) end
      print(s)
      

      luajit -jdump 1.lua
      ---- TRACE 1 mcode 100
      000e7f9c sub sp, sp, #8
      000e7fa0 str r9, [sp, #8]
      000e7fa4 movw r0, #23000
      000e7fa8 movt r0, #46810
      000e7fac ldr r10, [r9, #16]
      000e7fb0 ldrd r4, [r9]
      000e7fb4 cmn r5, #9
      000e7fb8 blne 0x000e0018 ->0
      000e7fbc ldrd r6, [r9, #8]
      000e7fc0 cmn r7, #14
      000e7fc4 blne 0x000e0018 ->0
      000e7fc8 cmp r4, r0
      000e7fcc blne 0x000e0018 ->0
      000e7fd0 adds r11, r6, r10
      000e7fd4 blvs 0x000e0018 ->0
      000e7fd8 add r10, r10, #1
      000e7fdc cmp r10, #512
      000e7fe0 blgt 0x000e001c ->1
      ->LOOP:
      000e7fe4 mov r9, r11
      000e7fe8 adds r11, r10, r9
      000e7fec blvs 0x000e0020 ->2
      000e7ff0 add r10, r10, #1
      000e7ff4 cmp r10, #512
      000e7ff8 ble 0x000e7fe4 ->LOOP
      000e7ffc bl 0x000e0024 ->3
      ---- TRACE 1 stop -> loop

      Аж несколько кодов на ассемблере. Ну, это, конечно, лукавая цифра


      1. beeruser
        13.12.2023 17:23

        BLVS не пометили. Это обработка переполнения после сложения.


    1. 1755
      13.12.2023 17:23

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


  1. Borjomy
    13.12.2023 17:23

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

    С учетом того, сколько процессорного времени=энергии тратится на такую элементарную операцию, питон никакая не зелёная технология. А черная


    1. Breathe_the_pressure
      13.12.2023 17:23

      Теперь понятно почему им так в Германии заинтересовались.


    1. N-Cube
      13.12.2023 17:23

      Так нет смысла оптимизировать сложение двух целочисленных значений. А для сложения миллиарда значений на си придется или специализированные библиотеки применять (и писать избыточный код), или писать кучу кода для использования симд, векторизации и многотопочного выполнения, а на питоне достаточно декларацию numba добавить и/или объявить массивы как объекты numpy. Практика показывает, что код на питоне почти всегда быстрее сишного (потому что все названные оптимизации на си практически никто не делает). При желании, код на питоне и ассемблерный обгоняет, периодически разработчики публикуют примеры.


      1. Ritan
        13.12.2023 17:23

        Да что уж там мелочиться. Код на питоне и CPU на котором работает обгоняет. Просто бежит впереди декодера и подкладывает библиотеки


        1. N-Cube
          13.12.2023 17:23

          Идиотские шутки оставьте для более подходящего места, а по теме - numba на лету из кода питона создает очень неплохой ассемблерный код с учетом векторизации и прочего, см. https://numba.pydata.org Написанный человеком ассемблерный код зачастую медленнее, тесты в сети есть.


          1. idimus
            13.12.2023 17:23

            Питон быстрее c/c++ потому что numba для генерации ассемблера использует LLVM! Не то что C/C++, RUST которые тоже умеют в LLVM... Ой.

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


            1. N-Cube
              13.12.2023 17:23

              Вот только программисты на С/С++ ассемблерный код почти никогда не смотрят, потому что уверены, что и так быстрее некуда. А там бывают очень странные фокусы, зависящие от компилятора. Как правило, даже без numba просто на numpy программа на питоне заметно быстрее типовой на си на сложной обработке больших данных.


              1. idimus
                13.12.2023 17:23

                Это правда. Но где то же должен быть водороздел, между крутышами и не очень.

                Язык глвбокт вторичен в данном случае. Abyway типовой python медленее типового c/c++ по причине разных задач и приоритетов.

                А кому надо и htttp://godbolt.org могут открыть и зарплату свое распальцевать.


                1. N-Cube
                  13.12.2023 17:23

                  Типовый питон подразумевает использование numpy, scipy и прочих вычислительных библиотек, плюс dask, joblib и прочих для параллельного и кластерного выполнения и так далее. Также в одну строчку numba подключается и компилирует код. Так что программист одного уровня на питоне пишет код куда компактнее и оптимальнее, нежели на си. Чтобы научиться грамотно использовать в питоне joblib, надо потратить пару дней, а вот на OpenMP в си нужно пару лет. Более того, компилятор от эппл OpenMP не поддерживает вовсе, OpenMP библиотека в Conda собрана gcc 8 и с более новым не собирается в принципе, а в Homebrew собрана без поддержки фортрана и надо ее пересобирать, но с новыми gcc не собирается (а старые не поддерживают актуальный фортран), и так далее. С openMPI все еще хуже, потому что реализаций много, в том числе проприетарных, и на каждом университетском unix кластере оказывается своя со своими «особенностями» (разнящимися от версии к версии и на разных платформах). На моей практике, хуже научного софта на Си только софт на матлабе - и по качеству кода и по быстродействию, а на питоне и код прочитать можно и работает быстрее.


                  1. idimus
                    13.12.2023 17:23

                    Типовый питон это не только научный питон, питон аналитический или ml. Часто это in-house тестирование, разработка вебсайтов и прототипипования. Часто в случае с последним, если взлетает, берут и переписывают на более быстрый язык.

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

                    Проблемы с библиотеками чаще всего решаются тем, что у тебя функционал переходит в стандарт, либо in-house решения аналогичные по функционалу.

                    Все эти быстрые и замечательные библиотеки питона, в основной под капотом c/c++, а в некоторых случаях даже fortran. Так что конфликт языков в данном случае, больше конфликт инфраструктуры языка. И да распарсить json нифига не юзер френдли для c++ или c, но и далеко не такая большая проблема. Зато можно получить результаты которые нужны именно тебе, и это будет быстрее и куда контроллируемее, что в случае с c/c++ очень ценно. Да и с точки бизнеса тоже.


        1. lorc
          13.12.2023 17:23

          Ваш сарказм необоснован. Большая часть программ в основном висит заблокированная на IO: ожидает пакет из сети, чтения из файла, ввода от юзера. Что Си что Пайтон имеют ждать одинаково эффективно :) И ждать будут одинаковое количество времени.

          Да, есть класс вычислительных задач где скорость вычисления a + b имеет значение. Но для таких задач никто не будет писать a + b ни в си, ни на пайтоне. А будут использовать специализированные библиотеки, которые умеют складывать числа сильно быстрее, чем наивный код на си или пайтоне. О чем собственно@N-Cubeи написал. Непонятно за что его минусуют.


          1. Daddy_Cool
            13.12.2023 17:23

            Вствлю пять копеек.
            Нам надо было написать некую хитрую обработку изображений.
            Первая версия была на Питоне - почему? Потому что был рядом человек который умел работать с изображениями только на Питоне. И да, один файл обрабатывался пять часов. Мы убедились в работоспособности идеи и переписали на Си. Файл стал обрабатыватсья три минуты.


            1. squaremirrow
              13.12.2023 17:23

              Такая разница в производительности может говорить лишь о том, что человек не умеет писать на питоне производительный код, т.е. не использует библиотеки типа opencv, numpy, scipy, numba.

              Я могу поверить разнице времени в несколько раз между реализацией на Питоне и Си, но разница ровно в 100 раз могла возникнуть только из-за плохого кода на Питоне.


              1. Borjomy
                13.12.2023 17:23

                Так проблема в том, что 99% программистов на Питоне не умеют писать производительный код. Они не для этого его учили, а для того, чтобы было "как проще".


                1. N-Cube
                  13.12.2023 17:23

                  Программисты на си ровно так же не умеют. Скажем, могут написать многомерный фильтр Гаусса в виде многомерной итерации по по всей окрестности каждого элемента - практически в любом научном софте найдете (см. GMT, GMTSAR и еще неисчислимое множество известного софта). Зато в питоне авторы библиотеки scipy знают, что это можно сделать в тысячи и миллионы раз быстрее с помощью набора одномерных фильтров. Идем дальше - тот же фильтр Гаусса для больших сигма сишные программисты никогда на моей памяти даже не пытаются оптимизировать как последовательность фильтров, ну и так далее. В итоге, фильтрацию терабайтного растра на ноутбуке на питоне можно выполнить за минуту, а на си - за неделю. В теории, можно и на си грамотно написать, но я за четверть века не встречал ни одной адекватной реализации простейшего фильтра Гаусса на си, не говоря уж о чем-то посложнее.


                  1. Vladimirsencov
                    13.12.2023 17:23

                    Тут не проблема производительности платформы. А просто нежелание или отсутствие необходимости делать оптимизацию.


                    1. N-Cube
                      13.12.2023 17:23

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


            1. MountainGoat
              13.12.2023 17:23

              Вы загрузили изображение в трёхмерный массив (третье - по цветам) из нативных lists и нативных числовых типов? Не надо так. Даже в стандартной библиотеке Питона есть модуль array для таких случаев.


              1. Daddy_Cool
                13.12.2023 17:23

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


                1. MountainGoat
                  13.12.2023 17:23

                  Тогда поясню. В отличие от других языков, в Питоне "примитивные типы" - числа и массивы - нифига не примитивные. Они набиты проверками на ошибки, что очень полезно когда они есть в единичном экземпляре. Но когда начинаются хоть какие-то вопросы к производительности или потреблению памяти, то их использовать нельзя, а нужно из отдельной библиотеки типа Numpy вытащить настоящие примитивы - те, которые как и в Си, весят 4-8 байтов на штуку, при переполнении тихо врут а не дают эксепшн, и прочие радости.

                  Те, кто этому не научился, раскладывают данные в стандартные массивы, и смотрят как 200кб у них в памяти занимает 20Мб. А виноват Питон, конечно.


                  1. Daddy_Cool
                    13.12.2023 17:23

                    Ну вот, началось в колхозе утро... А я то надеялся, что Питон это что-то типа современного Бейсика и его все учат потому что просто. Даже реклама постоянно попадается: "Почему лучше учить сложный C++, чем простые Python/Java" ))).


                    1. anka007
                      13.12.2023 17:23

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


                    1. N-Cube
                      13.12.2023 17:23

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


      1. kom09
        13.12.2023 17:23

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


        1. N-Cube
          13.12.2023 17:23

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


  1. Keeper10
    13.12.2023 17:23

    А в попугаях на ассемблере будет ещё больше!


  1. nameisBegemot
    13.12.2023 17:23

    Питон элементарный язык, говорили они


    1. vesper-bot
      13.12.2023 17:23

      А это цена за элементарность, а также за динамическое типизирование.


    1. 1755
      13.12.2023 17:23

      Элементарный в использовании не означает элементарный в реализации.


  1. ArtTwink
    13.12.2023 17:23

    В подписе к первой картинке:

    Реализация выбирается в зависимости от того, какой тип имеют a и b: int, float или сложный тип

    А на картинке написано “complex”. Это действительно есть такой тип, как «сложный», или это всё-таки комплексное число?


    1. vesper-bot
      13.12.2023 17:23

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


  1. IGR2014
    13.12.2023 17:23

    Сколько строк на C нужно, чтобы выполнить a + b в Python?

    Много, учитывая объём кода самого интерпретатора Python))