Pull to refresh

Comments 8

В коде уже используется декоратор @njit, он является алиасом для @jit(nopython=True)

Но вот и до извращенств на Python добрались - а ведь ЯП Python изначально проектировался так, чтобы программировать на нём было легко и очивидно.

Моё мнение остаётся прежним - разрабатывать прикладную логику надо на простых ЯП - коли нужна экстремально высокая производительность - нужно переходить на C++ и Rust (если нужна ещё и надёжность); ну, по крайней мере пока не вышла n-я версия Mojo.

Высокой машинной оптимизации поддаётся не так уж много алгоритмов, а ещё нужно думать как сводить свои задачи к этим алгоритмам - зачастую это всё отнимает слишком много времени. И если ваша задача не в том, чтобы разрабатывать библиотеку для эффективного массового применения в обработке BigData, и Вы не разрабатываете драйверы - то лучше держаться подальше от такой оптимизации.

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

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

В идеале это должен быть такой ЯП, который умный AI-Ассистент смог бы разобрать вплоть до логики - и уже, "поняв" логику - перестроить совсем по-другому - опираясь на знания об оптимизации. То есть - это как дать готовый алгоритм сильному системному программисту - и попросить его оптимизировать под железо (в котором он хорошо разбирается) - и тут важно, чтобы алгоритм был так представлен, чтобы он достаточно легко в нём разобрался (а заодно и снабдить его готовыми проверочными всеохватывающими тестами).

И такой ЯП должен быть как можно более декларативным - т.е. именно описывать логику, как можно менее сужая возможные сценарии её решения. Тогда тут будет и машинная оптимизация, и распараллеливания, и кеширования, и, даже, динамическое перекомпилирование в процессе эксплуатации (по статистике) или переключение между несколькими готовыми вариантами, в зависимости от изменяемых условий выполнения. Ну, и конечно же, автодекомозиция от относительно сложных алгоритмов (а в таком ЯП не должно быть очень сложных описаний алгоритмов в принципе - это главный его постулат, т.е. это не С++ и не Rust, думаю даже ещё проще, чем Python в общем случае, со своими расширениями, должно быть) к простым.

Оптимизировать простые куда легче сложных. Вот только в реальных прикладных задачах крайне редко появляются простые алгоритмы (обычно как раз там, где их оптимизация в общей картине не даёт существенного прорыва в производительности)

def remove_noise_numba_3(arr, noise_level): 
  noise_level = arr.dtype.type(noise_level) 
  for i in range(arr.shape[0]): 
    for j in range(arr.shape[1]): 
      arr[i, j] = ( arr[i, j] if arr[i, j] >= noise_level 
      else 0 )

Вот так, пример это могло бы выглядеть на продвинутом ЯП

DEF remove_noise_numba_3(arr, noise_level) -> (e <- arr) -> MATCH |-> e < noise_level -> 0 OTHERWISE -> e

или короче

DEF remove_noise_numba_3(arr, noise_level) -> (e <- arr) -> SIEVE e < noise_level

или так

DEF PRC(src, edgval) : COMMAND WHEN src IS ETERABLE, src.ElementType IS EQUATABLE edgval.Type -> (arr) -> SIEVE it < edgval

VAL noise_level = 1000 VAL arr2 <- arr -> PRC noise_level

Забыл добавить, что в моём примере на декларативном ЯП ни функция ни команда по умолчанию не меняет исходные данные (в данном случае коллекцию / матрицу) - а возвращает обработанный новый результат. Но:

Во-первых - это всё декларативное описание - а уже как переиспользовать память и не плодить дублей - решать должен компилятор

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

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

Но команда REPLACE может подменять место размещения результата, скажем, стоящей далее функции: вот так

arr <- REPLACE remove_noise_numba_3(arr,noise_level) 

Можно явно указать необходимость перезаписи исходной коллекции arr (ну если она это позволяет) - без выделения отдельного блока памяти, с сохранением всех ссылок на данные arr.

Первый код на декларативном ЯП съехал в одну строчку (хотя я не сторонник влияния форматирования на алгоритм как в Python и даже такой код в одну строку будет идентично рабочим) - хотя подразумевалось следующее (для ясности понимания):

DEF remove_noise_numba_3(arr, noise_level) -> 
  (e <- arr) -> MATCH 
    |-> e < noise_level -> 0 
    OTHERWISE -> e

То есть тут каждая строка отделяемая "|->" это отдельная ветвь обработки (как их обрабатывать решает команда MATCH и компилятор). Команда "OTHERWISE" возвращает данные, если ни одна ветвь MATCH не подошла.

Такая запись не гарантирует, что сразу несколько ветвей не будут выполнены!!! Поэтому, формально, их порядок следования не важен (будь их тут много)! Опят же - если нужна строгость - это уже должно отдельно указываться, чтобы только одна ветвь была выполнена - тогда порядок следования ветвей будет важен.

Каждый оператор "->" передаёт текущую порцию обрабатываемых данных (а что это за порция определяется вышестоящим контекстом) на следующую команду или к каким-то данным, которые подменяют собой текущую порцию и так далее до возвращения результата. Вообще переходы от команды к команде в первую очередь являются просто трансляцией потока данных и (e <- arr) открывает этот поток данных, как и просто (arr) - без скобок - это уже был бы поток из одного элемента без развёртывания содержимого arr. Функция так же возвращает поток данных (как и команда).

Типы данных могут быть введены по необходимости - или алгоритм может оставаться абстрактным (тогда типы данных и их совместимость будут определяться по месту вызова)

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

Интересный вопрос как NumPy данные представляет. В статье много про предсказание ветвления и совсем мало про книг процессора.

Ниже ссылка на презентацию Скотта Мейера, где он о важности работы вещей процессора говорит. Meyers: Cpu Caches and Why You Care

как выше в комментариях сказали, если нужен высокопроизводительный код-берите соответствующие языки, вам же легче будет :)

Описанное изменение работает и без использования numba. Тем не менее спасибо за статью!

%timeit image2 = image.copy()
11.3 ms ± 417 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit image2 = image.copy(); image2[image2 < 1000] = 0
92.3 ms ± 4.44 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit image2 = image.copy(); image2 *= (image2 >= 1000)
22.7 ms ± 783 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Sign up to leave a comment.