Сравнение производительности: benchmark UDF-стратегий
Benchmark: 100 миллионов строк, операция upper()
Мы протестировали все четыре подхода на одном датасете (100M строк, операция upper() на строковом столбце) на кластере с 4 executors × 4 cores:
Как читать график: длина бара показывает относительную скорость, а не абсолютное время. Built-in (emerald) — самый быстрый, Python UDF (red) — самый медленный. Обратите внимание на масштаб: Python UDF в 100 раз медленнее built-in, поэтому его бар практически невидим.
Почему такая разница?
| Подход | Overhead источник | Execution model | Catalyst | CodeGen |
|---|---|---|---|---|
| Built-in | Нет | JVM Tungsten native | Полная оптимизация | Whole-Stage CodeGen |
| Scala UDF | Boxing/unboxing типов | JVM native call | Чёрный ящик | Нет |
| Pandas UDF | Arrow batch transfer | JVM → Arrow → Python → Arrow → JVM | Чёрный ящик | Нет |
| Python UDF | Per-row serialization | JVM → pickle → socket → Python → socket → JVM | Чёрный ящик | Нет |
Ключевой вывод: overhead растёт с каждым уровнем абстракции. Built-in работает “внутри” JVM. Scala UDF — “рядом” с JVM. Pandas UDF передаёт батчи через Arrow. Python UDF передаёт каждую строку через socket.
Benchmark: сложные операции
Для более сложных операций (regex matching, математические вычисления, ML-инференс) разрыв сокращается, потому что доля полезной работы растёт:
Regex extraction + string manipulation (100M строк):
Для сложных операций Python UDF “всего” в ~30x медленнее (вместо 100x), а Pandas UDF — в ~3x. Это потому, что Python-код занимает большую долю общего времени.
Матрица принятия решений
| Тип операции | Рекомендация | Почему |
|---|---|---|
| Строковые операции (upper, trim, replace) | Built-in | upper(), trim(), regexp_replace() |
| Условная логика (if/else) | Built-in | when().otherwise(), case в SQL |
| Работа с датами (разница, форматирование) | Built-in | datediff(), date_format(), months_between() |
| Математические формулы | Built-in | pow(), sqrt(), log(), арифметические операции |
| Работа с массивами/map | Built-in | array_contains(), explode(), map_keys() |
| Кастомная валидация (ИНН, ОГРН) | Scala UDF | Сложная логика без Python-зависимостей |
| Интеграция с Java-библиотекой | Scala UDF | Прямой доступ к JVM |
| ML-инференс (scikit-learn, PyTorch) | Pandas UDF (Iterator) | Загрузка модели один раз + vectorized predict |
| Сложные pandas-операции над группами | Pandas UDF (Grouped Map) | applyInPandas() для полного контроля |
| NumPy-вычисления (FFT, линейная алгебра) | Pandas UDF (Scalar) | Vectorized C-операции |
| Единственная оставшаяся опция | Python UDF | Когда ничего другого не подходит |
Чек-лист перед созданием UDF
Прежде чем писать UDF, пройдите этот чек-лист:
- Проверьте встроенные функции: Spark SQL Functions — 300+ функций
- Попробуйте
when/otherwise: большинство условной логики выражается через них - Рассмотрите higher-order functions:
transform(),filter(),aggregate()для работы с массивами - Рассмотрите SQL expression: иногда SQL
CASE WHENпроще, чем цепочкаwhen().otherwise() - Если UDF неизбежен: Scala UDF > Pandas UDF > Python UDF
Анти-паттерн: “мы Python-команда, поэтому используем Python UDF”
# Типичное обоснование: "наша команда не знает Scala,
# и нам проще писать Python UDF"
# ПРОБЛЕМА: Python UDF для простой конкатенации
@udf(returnType=StringType())
def full_name(first, last):
return f"{first} {last}" if first and last else None
# Результат: 100x overhead на каждую строку
# РЕШЕНИЕ: встроенная concat()
from pyspark.sql.functions import concat_ws
df.withColumn("full_name", concat_ws(" ", col("first_name"), col("last_name")))
“Мы Python-команда” — не повод использовать Python UDF. Встроенные функции вызываются из PySpark точно так же, как из Scala. Разница не в языке вызова, а в том, где выполняется логика — на JVM (встроенная) или в Python-процессе (UDF).
Резюме модуля
Иерархия производительности UDF в Spark:
1x Built-in (Catalyst + CodeGen + JVM) → ВСЕГДА ПЕРВЫЙ ВЫБОР
~2x Scala UDF (JVM, нет CodeGen) → Сложная логика без Python
~5x Pandas UDF (Arrow батчи) → ML, pandas, NumPy
~100x Python UDF (per-row сериализация) → ПОСЛЕДНИЙ ВАРИАНТ
Подробнее о каждом подходе — в предыдущих уроках модуля (cross-ref: 01-builtin-vs-udfs, 02-python-udf-overhead, 03-pandas-udfs-arrow, 04-scala-udfs).
Что дальше?
В следующем модуле мы переходим к оптимизации хранения данных — форматы файлов (Parquet, ORC, Avro), Z-ordering, bloom filters и решение проблемы маленьких файлов.