Профилирование C-кода, numpy, Cython, флаг --native
Python — это только верхушка айсберга. Под ним скрывается океан C-кода.
Многие Python-библиотеки используют C-код для ускорения:
Без флага --native вы видите только Python-функции. С --native — полный стек включая C.
# Без нативного стека (по умолчанию)
py-spy record -o profile.svg --pid 12345
# С нативным стеком
py-spy record -o profile.svg --native --pid 12345import numpy as np
def compute():
arr = np.random.rand(1000, 1000)
result = np.linalg.inv(arr) # Вызов C-кода
return resultПрофиль:
┌─────────────────────────────────────────┐
│ compute (95%) │
├─────────────────────────────────────────┤
│ np.linalg.inv (90%) │ ← «Чёрный ящик»
│ np.random.rand (5%) │
└─────────────────────────────────────────┘
Вы видите np.linalg.inv, но не знаете, что внутри.
Профиль:
┌─────────────────────────────────────────┐
│ compute (95%) │
├─────────────────────────────────────────┤
│ np.linalg.inv (90%) │
│ ┌──────────────────────────────────┐ │
│ │ dgetrf_ (LAPACK, 60%) │ │ ← C-функция!
│ │ dgetri_ (LAPACK, 30%) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
Теперь видно, что время тратится в LAPACK функциях dgetrf_ и dgetri_.
import numpy as np
import time
def matrix_operations():
"""Тяжёлые вычисления с матрицами"""
results = []
for _ in range(100):
# Создание матрицы
arr = np.random.rand(500, 500)
# Инверсия — дорогая операция
inv = np.linalg.inv(arr)
# Умножение
product = np.dot(arr, inv)
# Сумма
results.append(np.sum(product))
return results
if __name__ == "__main__":
start = time.time()
matrix_operations()
print(f"Время: {time.time() - start:.2f}с")# Без нативного стека
py-spy record -o numpy_basic.svg -- python numpy_script.py
# С нативным стеком
py-spy record -o numpy_native.svg --native -- python numpy_script.pyБез --native:
┌─────────────────────────────────────────┐
│ matrix_operations (98%) │
├─────────────────────────────────────────┤
│ np.linalg.inv (70%) │
│ np.dot (15%) │
│ np.random.rand (10%) │
│ np.sum (3%) │
└─────────────────────────────────────────┘
С --native:
┌─────────────────────────────────────────┐
│ matrix_operations (98%) │
├─────────────────────────────────────────┤
│ np.linalg.inv (70%) │
│ ┌──────────────────────────────────┐ │
│ │ dgetrf_ (45%) - LU разложение │ │
│ │ dgetri_ (25%) - инверсия │ │
│ └──────────────────────────────────┘ │
│ np.dot (15%) │
│ ┌──────────────────────────────────┐ │
│ │ dgemm_ (12%) - умножение │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
Вывод: Основная нагрузка — LAPACK функции. Это нормально для линейной алгебры.
Cython компилирует Python в C. Без --native вы видите только Python-обёртки.
# cython: boundscheck=False, wraparound=False
import numpy as np
def cython_sum(int n):
cdef int i
cdef double total = 0.0
for i in range(n):
total += i * i
return total┌─────────────────────────────────────────┐
│ cython_sum (80%) │ ← Видите только Python-обёртку
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ cython_sum (80%) │
│ ┌──────────────────────────────────┐ │
│ │ __pyx_pf_7example_cython_sum │ │ ← Скомпилированная функция
│ │ (75%) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
import hashlib
def hash_many(items):
"""Хэширование множества элементов"""
results = []
for item in items:
h = hashlib.sha256(item.encode()).hexdigest()
results.append(h)
return results
items = [f"data_{i}" for i in range(100_000)]
hash_many(items)┌─────────────────────────────────────────┐
│ hash_many (95%) │
├─────────────────────────────────────────┤
│ hashlib.sha256 (90%) │
│ ┌──────────────────────────────────┐ │
│ │ SHA256_Update (OpenSSL, 70%) │ │
│ │ SHA256_Final (OpenSSL, 20%) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
Видно, что время тратится в OpenSSL функциях.
Для чтения C-стека нужны отладочные символы. Они есть в:
Проверка:
# Если py-spy выдаёт предупреждение о missing symbols:
# Установите отладочные символы
# Ubuntu/Debian
sudo apt-get install python3-dbg
# macOS (через Homebrew)
brew install python --with-debug--native показывает C-стек внутри процесса Python. Отдельные процессы (subprocess) не показываются.
Чтение C-стека добавляет небольшой оверхед. Для продакшена используйте с осторожностью.
Вы видите имена C-функций, но не ассемблерный код. Для ассемблера используйте perf (Linux) или Instruments (macOS).
| Без --native | С --native |
|---|---|
| Только Python-фреймы | Python + C-фреймы |
| Быстрее (<5% оверхед) | Медленнее (~10% оверхед) |
| Достаточно для чистого Python | Нужно для numpy, Cython, cryptography |
| Меньше файл | Больше файл |
Рекомендация:
--native--native--nativeОбработка DataFrame занимает 30 секунд. Где узкое место?
import pandas as pd
import numpy as np
def process_data():
df = pd.DataFrame({
'A': np.random.rand(1_000_000),
'B': np.random.rand(1_000_000),
'C': np.random.rand(1_000_000)
})
# Группировка и агрегация
result = df.groupby(df['A'] > 0.5).agg({
'B': ['sum', 'mean'],
'C': ['sum', 'mean']
})
return result
process_data()py-spy record -o pandas.svg --native -- python pandas_script.py┌─────────────────────────────────────────┐
│ process_data (95%) │
├─────────────────────────────────────────┤
│ DataFrame.groupby (60%) │
│ ┌──────────────────────────────────┐ │
│ │ groupby_apply (40%) │ │
│ │ take_2d_multi (20%) │ │
│ └──────────────────────────────────┘ │
│ DataFrame.agg (30%) │
└─────────────────────────────────────────┘
Вывод: groupby занимает 60%. Возможно, стоит использовать более эффективный метод группировки или индексацию.
Парсинг большого XML файла занимает 2 минуты.
from lxml import etree
def parse_large_xml(filename):
tree = etree.parse(filename)
root = tree.getroot()
# XPath запросы
results = []
for elem in root.xpath('//item[@active="true"]'):
results.append(elem.text)
return results
parse_large_xml('large.xml')py-spy record -o lxml.svg --native -- python parse_xml.py┌─────────────────────────────────────────┐
│ parse_large_xml (90%) │
├─────────────────────────────────────────┤
│ etree.parse (50%) │
│ ┌──────────────────────────────────┐ │
│ │ xmlReadFile (libxml2, 45%) │ │
│ └──────────────────────────────────┘ │
│ root.xpath (40%) │
│ ┌──────────────────────────────────┐ │
│ │ xmlXPathEval (libxml2, 35%) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
Вывод: Время тратится в libxml2. Возможные оптимизации:
iterparse)lxml.etree.XMLParser с опциями| Функция | Библиотека | Что делает |
|---|---|---|
dgemm_, dgetrf_ | LAPACK | Линейная алгебра |
SHA256_Update | OpenSSL | Хэширование |
xmlReadFile, xmlXPathEval | libxml2 | Парсинг XML |
PyEval_EvalFrameEx | CPython | Выполнение байт-кода |
malloc, free | libc | Выделение памяти |
--native--nativeКлючевая идея: --native открывает «чёрный ящик» C-расширений. Вы видите не только Python-обёртки, но и реальные C-функции, которые выполняют работу.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.