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

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

Прежде чем мы углубимся в производительность обеих структур данных, во-первых, нам нужно понять их внутреннюю работу (время, когда она компилируется в C) как библиотеки и их структуры данных, реализованные на C.

Когда программное обеспечение Python установлено на вашем компьютере, как минимум, у него есть интерпретатор и библиотека поддержки. Интерпретатор - это не что иное, как программное обеспечение, которое запускает наши сценарии Python. Что интересно, это можно реализовать на любом языке программирования! CPython - это интерпретатор по умолчанию для Python, написанный на языке программирования C.

Что за суета вокруг CPython, Cython, Jython, IronPython? Я рассею все твои сомнения. Пожалуйста, держитесь до последнего и будьте готовы к небольшим предварительным условиям, используемым в Python. Давайте сначала разберемся со всем этим подробно.

CPython

  • CPython - эталонная реализация языка программирования Python. Написано на C и Python. Разработан тем же автором, что и python, Гвидо ван Россумом в 1994 году.
  • CPython компилирует ваш код Python в байт-код (прозрачно), интерпретирует этот байт-код и выполняет его по ходу. Python использует CPython в качестве основы

Cython

  • CPython не переводит ваш код Python на C сам по себе. Вместо этого он запускает цикл интерпретатора. Итак, здесь вступает в игру Cython для перевода кода Python на C.
  • Cython добавляет несколько расширений к языку Python и позволяет компилировать код в расширения C, код, который подключается к интерпретатору CPython.

Jython, IronPython и PyPy - текущие «другие» реализации языка программирования Python; они реализованы в Java, C # и RPython (подмножество Python) соответственно. Jython компилирует ваш код Python в байт-код Java, поэтому ваш код Python может работать на JVM. IronPython позволяет запускать Python в Microsoft CLR. А PyPy, реализованный в (подмножестве) Python, позволяет запускать код Python быстрее, чем CPython.

Теперь давайте погрузимся во внутреннюю реализацию списков Python и массивов Numpy.

Список

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

Но вопрос в том, как это реализовано?

Списки CPython представляют собой массивы переменной длины, а не списки в стиле Lisp (используются связанные списки). Реализация списка в CPython использует непрерывный массив ссылок на другие объекты и сохраняет указатель на этот массив и длину массива в структуре заголовка списка. Это делает индексирование списка a [i] операцией, стоимость которой не зависит от размера списка или значения индекса.

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

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

Теперь давайте посмотрим, как работает numpy.

Numpy

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

Массив NumPy представляет собой многомерный массив объектов одного типа и в основном описывается метаданными (в частности, количеством измерений, формой и типом данных) и фактическими данными.

Данные хранятся в однородном и непрерывном блоке памяти по определенному адресу в системной памяти (оперативной памяти или RAM). Этот блок памяти называется буфером данных.

Это основное различие между массивом и чистой структурой Python, такой как список, где элементы разбросаны по системной памяти. Этот аспект является важной особенностью, которая делает массивы NumPy такими эффективными.

Вот краткий обзор того, как объекты массива numpy выглядят в C. Структура PyArrayObject имеет четыре элемента (*data, nd, *dimensions, *strides), которые необходимы для доступа к данным массива из кода C, пожалуйста, игнорируйте другие поля для краткости :

Почему NumPy эффективен?

  • Вычисления на массивах, написанных очень эффективно на низкоуровневом языке, таком как C (а большая часть NumPy фактически написана на C). Зная адрес блока памяти и тип данных, можно просто арифметически перебрать все элементы, например. Это потребовало бы значительных накладных расходов в питоне со списком.
  • Пространственная локальность в шаблонах доступа к памяти приводит к увеличению производительности, особенно за счет кеш-памяти ЦП. Действительно, кеш загружает байты частями из ОЗУ в регистры ЦП. Смежные элементы затем загружаются очень эффективно (место ссылки).
  • Наконец, тот факт, что элементы хранятся в памяти непрерывно, позволяет NumPy использовать преимущества векторизованных инструкций современных процессоров, таких как Intel SSE и AVX, AMD XOP и т. Д. Например, несколько последовательных чисел с плавающей запятой могут быть загружены в регистры 128, 256 или 512 бит для векторизованных арифметических вычислений, реализованных как инструкции ЦП.
  • Кроме того, NumPy можно связать с оптимизированными библиотеками линейной алгебры, такими как BLAS и LAPACK, через ATLAS или библиотеку Intel Math Kernel (MKL). Некоторые конкретные матричные вычисления также могут быть многопоточными, используя преимущества современных многоядерных процессоров.

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

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

Список Python поддерживает эффективную вставку, удаление, добавление и конкатенацию, а понимание списков Python упрощает их создание и управление.

Однако список Python имеет определенные ограничения:

  • Список Python не поддерживает «векторизованные» операции, такие как поэлементное сложение и умножение.
  • Список Python может содержать объекты разных типов, что означает, что Python должен хранить информацию о типе для каждого элемента и должен выполнять код диспетчеризации типов при работе с каждым элементом. что приводит к проверке типов на каждой итерации.

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

Сравнение производительности

В этом разделе мы собираемся протестировать список Python и массив Numpy с точки зрения потребления памяти и времени, затрачиваемого на выполнение. Сначала импортируйте необходимые модули и сравните производительность массива / списка для разных размеров. В этом примере мы собираемся взять соотношение между результатами времени / памяти, полученными списком python и массивом numpy, и посмотреть, какой из них работает лучше. Кроме того, соотношение будет принято как список python: numpy_array, поэтому результаты показывают, во сколько раз numpy лучше, чем список python.

Потребление памяти

Вот быстрый пример, показывающий потребление памяти для хранения массивов разного размера.

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

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

Сравнение времени

В приведенном ниже примере вычисляются квадраты каждого элемента списка / массива и измеряется время.

На приведенном выше графике для size=1 список python работает лучше, а во всех остальных случаях numpy работает лучше. Причина в том, что хотя список python работает быстро (оба используют C в качестве бэкэнда), это просто накладные расходы на проверку dtype для каждого элемента. В numpy при проверке типа он также должен проверять другую информацию заголовка, которую мы видим в его структуре C, т.е. размер, шаг и т. Д., Тогда как в python достаточно только dtype.

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

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

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

Спасибо за прочтение. Мир.

использованная литература