Информация об изменениях

Сообщение Re[10]: Производительность .Net на вычислительных задачах от 26.10.2020 5:04

Изменено 26.10.2020 5:11 Sinclair

Re[10]: Производительность .Net на вычислительных задачах
Здравствуйте, vdimas, Вы писали:

V>В любом случае зависит от конкретных вычислений. Например, при многостадийной обработке стерео-аудио (поток пар float32) выгодно первую операцию (из конвейера их) спланировать так, чтобы разделить каналы в промежуточных данных, дальнейшая обработка этих данных по незавсимым каналам будет происходить максимально эффективно, последняя операция должна опять собирать два потока в один.

Эмм, это если у нас разные формулы для левого и правого каналов, да? А зачем такое бывает нужно?
V>Плюс к этому, исходные и конечные данные могут представлять из себя стрим float32, а вычисления имеет смысл делать во float64, это касается и промежуточных результатов.
Эмм, VCVTPS2PD / VCVTDQ2PD?

V>Для вычислений с плавающей точкой есть еще одна характеристика (в сравнении с целочисленными вычислениями) — это трафик подгрузки данных, т.е. эффективность такой операции. В плавающей точке даже константы не идут как аргументы операции, они идут как обычные внешние данные.

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

V>Конкретно в sauvola я внимательно не втыкал, да и смысл втыкать конкретно в него? Речь же идёт о произвольном алгоритме?

Ну вот я и не могу себе представить такой алгоритм.
V>Причём, твой подход может быть интересен и для 1D, потому что в рекурсивной фильтрации, например, на каждом шаге вычисляются элементы x0*a0, x1*a1, .., y0*b0, y(-1)*b1, ...
V>Т.е., возможность относительной адресации — она полезна не только для 2D алгоритмов.
Ну да. Там даже проще всё. Но всё ещё не вижу смысла складывать что-то в промежуточную память.

V>Так шта, вот тебе идея — допилить свою реализацию до 1D.

Да, на конференции задавали такой вопрос.
V>Я с фильтрацией периодически вожусь и погонял бы её и на этот счёт.
V>Потому что, несколько раз подходил к дотнету ранее, но он не использует векторные инструкции при компиляции вычислений с плавающей точкой (ранее точно не использовал, я проверял раз в несколько лет), поэтому я так и не занимался обработкой звука всерьёз на C#.
Да, автовекторизации как не было, так и нет.
V>Но если ты уже так далеко продвинулся в генерации векторных вычислений на C#, в оптимизации доступа к массивам и прочим таким вещам — то тебе там будет сделать совсем немного.
Я хочу сначала довести linq2d до более-менее приемлемого состояния, чтобы не стыдно было хотя бы пакет выложить.
Пока что я этого не делаю, т.к. есть риск изменений, ломающих обратную совместимость.

Ну, и скорость тоже — не сделано как минимум две вещи:
1. Разворачивание циклов. На первый взгляд, ICC-шный код отличается как раз тем, что он "бежит" не одним SIMD-регистром, а сразу двумя. Да ещё и перемешивает операции загрузки с операциями вычисления.
2. Оптимизация рекуррентных обращений. Как раз то, что потребуется для 1d.
V>И что-то мне подсказывает, что эту работу можно будет слать разработчикам дотнета... Ну или оформлять в виде общедоступного nuget-пакета.
V>Разумеется, там еще проверять это всё на прочность и прочую дуракоустойчивость... возможно, еще допиливать, но если уже столько труда вложено — имеет смысл не бросать эту разработку.
Да тут не так много усилий — немножко по вечерам.

S>>а потом ещё и выносим load(t)/load(a) в другой цикл, чтобы избежать кэширования

V>Угу.
V>Для 1d там же потребуется "прокрутить" данные в регистрах, т.е. сдвинуть их на один отсчёт.
Да, в том-то и дело. В 2d, например, обращение к готовому результату "на строчку выше" гарантированно безопасно, т.к. там расчёт уже закончен. А в 1d безопасны только обращения на расстояние, больше, чем длина регистра.
А вот обращения типа r[-1] надо прокручивать. Как раз эта штука у меня не сделана.

V>А ты не экспериментировал еще с имеющимися соотв. ср-вами дотнета, там же есть векторные операции:

V>https://docs.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.vector256?view=netcore-3.1
V>Что оно порождает?
Так я его и использую. У меня же нет прямого доступа к бинарю — я порождаю MSIL, сдобренный интринсиками. А дальше — уже JIT.
V>Т.е. что насчёт детектирования текущей железки и порожения кода под неё конкретную?
Детектирование делается очень тупо незатейливо. При инициализации, библиотека строит список векторных операций. Например, https://github.com/evilguest/linq2d/blob/master/Linq2d/CodeGen/VectorData.cs#L291-L465
В зависимости от того, какие фичи обнаружены в рантайме, добавляются разные операции.

А потом, при компиляции ядра фильтра, мы просто идём вверх по дереву и смотрим, можно ли векторизовать скалярные операции.
Причём пока что сделано очень, очень тупо — из всех композитных операций рассматриваются только загрузка-с-конвертацией.
Например, если мы, допустим, складываем short и int, то в AST у нас лежит Add(Convert(Load(s), typeof(int)), Load(i)).
Когда векторизатор анализирует это выражение, он видит "о, я могу загрузить вектор из 16 шортов, ура", но "ой, загрузить вектор из 16 интов мне некуда".
А вот дальше он смотрит на 8-элементные вектора, а там есть LoadAndConvert из short* в Vector256<int>.

V>Ну да, для фильтрации, корелляции, свёртки и ИИ основная операция — поэлементное умножение векторов с накоплением суммы, S=x0*a0+x1*a1+...

S>>Короче, нужны примеры кода, в которых сохранение промежуточного результата в память что-то даст.
V>Поток пар отсчётов стерео {float32, float32}.
V>(один из самых популярных "сырых" звуковых форматов на сегодня, т.е. то, что дают кодеки, в каком формате происходит захват и воспроизведение аудио из/в железо в современных аудио-АПИ)
И что с ним нужно сделать?
Re[10]: Производительность .Net на вычислительных задачах
Здравствуйте, vdimas, Вы писали:

V>В любом случае зависит от конкретных вычислений. Например, при многостадийной обработке стерео-аудио (поток пар float32) выгодно первую операцию (из конвейера их) спланировать так, чтобы разделить каналы в промежуточных данных, дальнейшая обработка этих данных по незавсимым каналам будет происходить максимально эффективно, последняя операция должна опять собирать два потока в один.

Эмм, это если у нас разные формулы для левого и правого каналов, да? А зачем такое бывает нужно?
V>Плюс к этому, исходные и конечные данные могут представлять из себя стрим float32, а вычисления имеет смысл делать во float64, это касается и промежуточных результатов.
Эмм, VCVTPS2PD / VCVTDQ2PD?

V>Для вычислений с плавающей точкой есть еще одна характеристика (в сравнении с целочисленными вычислениями) — это трафик подгрузки данных, т.е. эффективность такой операции. В плавающей точке даже константы не идут как аргументы операции, они идут как обычные внешние данные.

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

V>Конкретно в sauvola я внимательно не втыкал, да и смысл втыкать конкретно в него? Речь же идёт о произвольном алгоритме?

Ну вот я и не могу себе представить такой алгоритм.
V>Причём, твой подход может быть интересен и для 1D, потому что в рекурсивной фильтрации, например, на каждом шаге вычисляются элементы x0*a0, x1*a1, .., y0*b0, y(-1)*b1, ...
V>Т.е., возможность относительной адресации — она полезна не только для 2D алгоритмов.
Ну да. Там даже проще всё. Но всё ещё не вижу смысла складывать что-то в промежуточную память.

V>Так шта, вот тебе идея — допилить свою реализацию до 1D.

Да, на конференции задавали такой вопрос.
V>Я с фильтрацией периодически вожусь и погонял бы её и на этот счёт.
V>Потому что, несколько раз подходил к дотнету ранее, но он не использует векторные инструкции при компиляции вычислений с плавающей точкой (ранее точно не использовал, я проверял раз в несколько лет), поэтому я так и не занимался обработкой звука всерьёз на C#.
Да, автовекторизации как не было, так и нет.
V>Но если ты уже так далеко продвинулся в генерации векторных вычислений на C#, в оптимизации доступа к массивам и прочим таким вещам — то тебе там будет сделать совсем немного.
Я хочу сначала довести linq2d до более-менее приемлемого состояния, чтобы не стыдно было хотя бы пакет выложить.
Пока что я этого не делаю, т.к. есть риск изменений, ломающих обратную совместимость.

Ну, и скорость тоже — не сделано как минимум две вещи:
1. Разворачивание циклов. На первый взгляд, ICC-шный код отличается как раз тем, что он "бежит" не одним SIMD-регистром, а сразу двумя. Да ещё и перемешивает операции загрузки с операциями вычисления.
2. Оптимизация рекуррентных обращений. Как раз то, что потребуется для 1d.
V>И что-то мне подсказывает, что эту работу можно будет слать разработчикам дотнета... Ну или оформлять в виде общедоступного nuget-пакета.
V>Разумеется, там еще проверять это всё на прочность и прочую дуракоустойчивость... возможно, еще допиливать, но если уже столько труда вложено — имеет смысл не бросать эту разработку.
Да тут не так много усилий — немножко по вечерам.

S>>а потом ещё и выносим load(t)/load(a) в другой цикл, чтобы избежать кэширования

V>Угу.
V>Для 1d там же потребуется "прокрутить" данные в регистрах, т.е. сдвинуть их на один отсчёт.
Да, в том-то и дело. В 2d, например, обращение к готовому результату "на строчку выше" гарантированно безопасно, т.к. там расчёт уже закончен. А в 1d безопасны только обращения на расстояние, больше, чем длина регистра.
А вот обращения типа r[-1] надо прокручивать. Как раз эта штука у меня не сделана.

V>А ты не экспериментировал еще с имеющимися соотв. ср-вами дотнета, там же есть векторные операции:

V>https://docs.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.vector256?view=netcore-3.1
V>Что оно порождает?
Так я его и использую. У меня же нет прямого доступа к бинарю — я порождаю MSIL, сдобренный интринсиками. А дальше — уже JIT.
V>Т.е. что насчёт детектирования текущей железки и порожения кода под неё конкретную?
Детектирование делается очень тупо незатейливо. При инициализации, библиотека строит список векторных операций. Например, https://github.com/evilguest/linq2d/blob/master/Linq2d/CodeGen/VectorData.cs#L291-L465
В зависимости от того, какие фичи обнаружены в рантайме, добавляются разные операции.

А потом, при компиляции ядра фильтра, мы просто идём вверх по дереву и смотрим, можно ли векторизовать скалярные операции.
Причём пока что сделано очень, очень тупо — из всех композитных операций рассматриваются только загрузка-с-конвертацией.
Например, если мы, допустим, складываем short и int, то в AST у нас лежит Add(Convert(Load(s), typeof(int)), Load(i)).
Когда векторизатор анализирует это выражение, он видит "о, я могу загрузить вектор из 16 шортов, ура", но "ой, загрузить вектор из 16 интов мне некуда".
А вот дальше он смотрит на 8-элементные вектора, а там есть LoadAndConvert из short* в Vector256<int>. Отлично, у нас уже есть готовый Vector256<int>, смотрим второй аргумент — тоже есть конверсия из int* в Vector2556<int>.
Едем дальше — есть ли операция для сложения таких векторов? Есть. Всё, успех — у нас готово векторное ядро:
Avx2.Store(
  result+x, 
  Avx2.Add(
    Avx2.LoadVector256Int32(s+x), 
    Avx2.LoadVector256(i+x))
  )


V>Ну да, для фильтрации, корелляции, свёртки и ИИ основная операция — поэлементное умножение векторов с накоплением суммы, S=x0*a0+x1*a1+...

S>>Короче, нужны примеры кода, в которых сохранение промежуточного результата в память что-то даст.
V>Поток пар отсчётов стерео {float32, float32}.
V>(один из самых популярных "сырых" звуковых форматов на сегодня, т.е. то, что дают кодеки, в каком формате происходит захват и воспроизведение аудио из/в железо в современных аудио-АПИ)
И что с ним нужно сделать?