NLinker

Ответ Ане про ФП

Aug 3rd, 2025
147
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!

Привет, Аня.

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

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

Теперь о факторах, влияющих на скорость исполнения:

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

  • Питонячьи функции reduce, map, filter, если их применять над списками, немного медленнее, чем цикл for: https://stackoverflow.com/questions/22108488/are-list-comprehensions-and-functional-functions-faster-than-for-loops

  • Реализация самой функции reduce нативная, однако лямбда или функция, которая туда передаётся будучи реализованной в Питоне, "съедает" всё преимущество, потому что в Питоне сам вызов функции (особенно виртуального метода) обходится дорого, "спасибо" динамической типизации и динамической диспетчеризации.

  • Однако если использовать фреймворки типа JAX, то реализация через функции высшего порядка даёт большую свободу самому фреймворку оптимизировать исполнение, например:

    
    # Define a function to minimize (e.g., Rosenbrock function)

@jit
def rosenbrock(x):
    return sum(100.0 * (x[1:] - x[:-1] 2.0) 2.0 + (1 - x[:-1]) ** 2.0)

@jit
def gradient_descent_step(x, learning_rate):
return x - learning_rate * grad(rosenbrock)(x)


Здесь функция высшего порядка `grad` принимает функцию `rosenbrock` и под капотом JAX раскладывает функцию rosenbrock на кусочки и векторизует так, чтобы вычисление градиентов было наиболее эффективным. Явная реализация через циклы была бы, во-первых, значительно сложнее, и во-вторых, чтобы обогнать эту реализацию, нужно было бы долго гонять код под профайлером. Резюмируя этот пункт циклы смешивают итерацию с обработкой последовательности и жёстко предписывают конкретные шаги, что "связывает руки" среде или компилятору.

- Учитывая пункт выше иногда компилятор тупит и делает код медленнее чем можно, и мы знаем лучший и более быстрый способ, который и можем реализовать в цикле. Конкретные варианты кода лучше испытывать с помощью измерений времени (см библиотеку timeit)

- Сам вопрос о тюнинге Питона звучит для меня странно, Питон мы используем не для того, чтобы бороться за перфоманс. Если возник вопрос про перфоманс, надо просто брать rust + pyo3 и решать этот вопрос раз и навсегда))

Плюс добавлю, что скорость не является единственным критерием, раз уж программы пишутся для людей, то читаемость имхо гораздо более важна, и циклы безусловно проще пишутся и читаются. Пайплайны на функциях высшего порядка легко пишутся и читаются только после определённого опыта и тренировки. Я такой опыт имею и в принципе нормально отношусь к сложной композиции функций (в Haskell такие вообще выглядят красиво), а вот коллег подобный стиль может раздражать. Приведу ещё фрагмент из книжки по C++ с подобным интересным рассуждением (https://djvu.online/file/pFSJe4xDZjGk0). Встречаются задачи, которые через цикл решаются элементарно, а через пайплайны на ФВП довольно заморочно. Так что везде нужно искать баланс.
Add Comment
Please, Sign In to add comment