Re[18]: Производительность .Net на вычислительных задачах
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.11.20 03:55
Оценка:
Здравствуйте, vdimas, Вы писали:
V>До какой-то степени можно, конечно.
V>Это как минимизация выражений, распространение констант и т.д.
V>В общем, всё то, что входит в стадию бета-редукции у современных оптимизирующих компиляторов, с учётом предметной области вычислений.
Если мы уже перешли к линейным фильтрам, то они компонуемы просто по определению. Любая комбинация линейных фильтров является линейным фильтром.

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

Ок, я понял. Это если честно считать модель преобразования с начала и до конца.

S>>если мы всё равно пропускаем через эту модель единичный сигнал, а на выход записываем коэффициенты в "финальной" дискретизации и разрядности.


V>Коэффициенты IR имеют понятный для любого хоббиста физический смысл — это просто отсчёты выходного сигнала.

V>(т.е., "планка входа" в тему минимальная)
Это как раз понятно.

S>>Ну, по сравнению с арифметикой даже они гораздо дороже. Как сделать табличную аппроксимацию в SIMD?

V>Через a*x+b
Не-не-не-не-не, Дэвид Блейн. ax+b — это линейное преобразование. Таблица — это когда у меня значения выхода произвольные.
То есть я беру, скажем, логарифм по некоторому основанию от x, и рассчитываю его для всех 65536 возможных значений x, складывая в таблицу.
А потом вычисляю logtable[x] для каждого x.
Вот как развлекались любители математики 12 лет тому: http://jrfonseca.blogspot.com/2008/09/fast-sse2-pow-tables-or-polynomials.html
Сейчас, вроде бы, можно использовать gather инструкции из AVX2; но я с ними не разобрался — непонятно, как там задаётся базовый адрес и масштаб. И там есть ограничения на типы данных — можно тащить либо 32, либо 64 бита.
То есть работать с 16-битным звуком напрямую через них не выйдет.

V>В 2 раза развёртки циклов я не видел ни разу в ассемблерном виде в релизе.

V>От 16 до 128 с шагом в степень двойки видел.
А чего далеко ходить? Вот, берём простейший код поэлементного сложения массивов. Максимально дружественный к векторизации.
Гляньте — какой имеем loop unroll factor: https://godbolt.org/z/919Mf9 ?

Покажите мне код, который разворачивается в SIMD с фактором хотя бы в 16, не говоря уже о 128.

V>О чём говорил я — циклы разворачивают для экономии на обслуживании переменной цикла и соотв. ветвления.

На фоне SIMD-операций экономия на обслуживании ветвления незначительна. Ну, по крайней мере, выглядит таковой с точки зрения компилятора.
То есть у нас и так бранч занимает, скажем, x% от стоимости вычислительной части цикла. Развёртка вдвое позволяет снизить расход до x/2 %. В 16 раз — до x/16.
Разница между 1/4 и 1/16 = 3/16, или чуть меньше 20%. Если исходный x был равен 20%, то общий выигрыш в производительности при переходе от unroll=4 к 16 будет 4%. И при этом он вообще будет заметен только в тех случаях, когда длина входных данных у нас значительно больше 16ти размера SIMD-вектора. Таким образом, unroll в 128 может быть полезен только при очень, очень дешёвом теле цикла.

V>От алгоритма ж зависит.

V>Если в алгоритме смещение x +/- 100, то на 100 точках в начале и в конце строки будет выход за границы изображения.
В корректном алгоритме мы никогда не обращаемся за пределы массива. Если есть смещение — то, значит, перед обращением к a[i,j] выполняется какой-то код по коррекции индексов.
Возьмём алгоритм без смещения. Тупой цикл for(var i=0;i<a.Height;i++) for(var j=0;j<a.Width;j++) s+=a[i,j]. Тут четыре бранч-инструкции, из которых две — пользовательские, и две — в проверке диапазона.
Статистика вот такая:
1. Выход из цикла по i проверяется h раз, и только в 1 случае берётся другая ветка.
2. Выход из цикла по j проверяется h*w раз, и только в h случаев берётся другая ветка
3. Проверка диапазона по i выполняется h*w раз, и никогда не срабатывает
4. Проверка диапазона по j выполняется h*w раз, и никогда не срабатывает

Казалось бы — 3 и 4 вообще не должны давать вклад в производительность; к моменту, когда дело доходит до них, очередной элемент массива уже в регистре и прибавляется к сумме. Проверка очевидного не должна занимать больше времени, чем ожидание памяти. 1 и 2 на массиве размером 1000*1000 должны давать сбой предсказания в 0.1% случаев каждая; так что тоже должны давать пренебрежимо мало вклада.
Ан нет — устранение проверок диапазона из этого цикла даст чуть ли не 25% ускорения; развёртка цикла даёт ещё бонус.

Из этого делаем вывод — оптимизатор внутри процессора есть, но он недостаточно умный, чтобы скомпенсировать даже простейшие недостатки скормленного ему кода.

V>Такая адресация делается на циклической линии задержки, длина которой не может быть меньше макс. амплитуды смещения по индексу, и тогда индекс любого отсчёта при смещении назад находится как (cursor + length — offset) % length. Например, было бы прикольно эту операцию сразу выполнить как "встроенный кирпичик" системы. На пальцах (для линейной аппроксимации) 3.5 отсчёта назад — это x[cursor-3]*0.5+x[cursor-4]*0.5, 4.3 отсчёта назад — это x[cursor-4]*0.7+x[cursor-5]*0.3, где индекс условного cursor-offset вычисляется на циклической линии задержки как показал выше.

А зачем нам нецелые смещения?
Выражение 0.2*x[-3.5]+0.1*x[-4.3] сводится к 0.1*x[-3]+0.17*x[-4]+0.03*x[-5], и у нас опять всё целое.
Если нас интересует период эха, то как бы 44кГц уже дают точность в 22.6 микросекунд — вряд ли слушатель заметит, что эхо приходит не ровно через 2.5 секунды, а через 2.500023.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.