Как избежать арифметики с плавающей точкой - PullRequest
3 голосов
/ 18 сентября 2009

Я написал небольшой программный синтезатор для iPhone.
Для дальнейшей настройки производительности я измерил свое приложение с Shark и обнаружил, что теряю много времени в преобразованиях float / SInt16.
Поэтому я переписал некоторые части, чтобы обойти преобразования, предварительно рассчитав таблицы поиска, которые возвращают «готовые к использованию» образцы SInt16. Пока все отлично работает.
В настоящее время я пытаюсь переписать некоторые фильтры и мою реализацию конверта ADSR, чтобы использовать только целочисленную арифметику, но я мог бы использовать некоторые советы, как выполнять умножения / деления без плавающих чисел.
Я использую канонический формат iPhone :

  • LPCM
  • 16-разрядные целочисленные выборки

Каковы хорошие подходы для применения амплитуды к моему окончательному образцу без использования поплавка?

Edit:
Единственное, что я до сих пор понял, это то, что я могу делить на степени 2, сдвигая вправо мою текущую выборку.

inBuffer[frame] = wavetable[i % cycleLengthInSamples] >> 4;

Но я не могу придумать какого-либо элегантного способа создания гладкого конверта ADSR с этим.

Edit2: Спасибо за все ваши отличные ответы!
Мой текущий подход:

  • привести все мои значения конверта ADSR в положительный диапазон SInt16
  • умножить на текущее значение из волновой таблицы (сохранить промежуточные значения как SInt32)
  • сдвинуть результат на 16 вправо

это похоже на работу:)

Ответы [ 4 ]

4 голосов
/ 18 сентября 2009

Фиксированная точка хороша, так как в этом случае вы используете 16 бит. Самый простой способ - умножить на 10 в зависимости от необходимой точности. Если вы можете использовать 32-битные целочисленные значения в качестве промежуточного, вы сможете получить приличную точность. В конце вы можете преобразовать обратно в 16-битное int, округляя или усекая по своему усмотрению.

Edit: Вы хотите сдвинуть влево, чтобы увеличить значения. Сохраняйте результат смещения в типе с большей точностью (32 или 64 бита в зависимости от того, что вам нужно). простое переключение не будет работать, если вы используете подписанные типы

Будьте внимательны, если вы умножаете или делите два числа с фиксированной точкой. Умножение получается (a * n) * (b n), и вы получите b n ^ 2 вместо a b n. Деление - это (a n) / (b n), которое (a / b) вместо ((a n) / b). Вот почему я предложил использовать степени 10, чтобы легко находить ваши ошибки, если вы не знакомы с фиксированной точкой.

Когда вы закончите свои вычисления, вы вернетесь назад вправо, чтобы вернуться к 16-битному целому. Если вы хотите получить фантазию, вы также можете выполнить округление перед сменой.

Я предлагаю вам немного почитать, если вы действительно заинтересованы в реализации эффективной фиксированной точки. http://www.digitalsignallabs.com/fp.pdf

3 голосов
/ 19 сентября 2009

Ответы на этот вопрос довольно полны с точки зрения реализации. Вот немного больше объяснения, чем я видел там:

Один из подходов состоит в том, чтобы ввести все ваши числа в диапазон, скажем, [-1.0,1.0). Затем вы отображаете эти числа в диапазоне [-2 ^ 15, (2 ^ 15) -1]. Например,

Half = round(0.5*32768); //16384
Third = round((1.0/3.0)*32768); //10923

Когда вы умножаете эти два числа, вы получаете

Temp = Half*Third; //178962432
Result = Temp/32768; //5461 = round(1.0/6.0)*32768

В последней строке делится на 32768 - это точка Patros , рассчитанная для умножений, нуждающихся в дополнительном шаге масштабирования. Это имеет больше смысла, если вы напишите масштабирование 2 ^ N явно:

x1 = x1Float*(2^15);
x2 = x2Float*(2^15);
Temp = x1Float*x2Float*(2^15)*(2^15);
Result = Temp/(2^15); //get back to 2^N scaling

Так что это арифметика. Для реализации обратите внимание, что умножение двух 16-разрядных целых чисел требует 32-разрядного результата, поэтому Temp должен быть 32-разрядным. Кроме того, 32768 не может быть представлено в 16-битной переменной, поэтому следует помнить, что компилятор будет выполнять 32-битные операции немедленно. И, как вы уже заметили, вы можете перейти к умножению / делению на степени 2, чтобы вы могли написать

N = 15;
SInt16 x1 = round(x1Float * (1 << N));
SInt16 x2 = round(x2Float * (1 << N));
SInt32 Temp = x1*x2;
Result = (SInt16)(Temp >> N);
FloatResult = ((double)Result)/(1 << N);

Но предположим, что [-1,1) не правильный диапазон? Если вы предпочитаете ограничить свои числа, скажем, [-4.0,4.0), вы можете использовать N = 13. Тогда у вас есть 1 знаковый бит, два бита перед двоичной точкой и 13 после. Они называются 1.15 и 3.13 дробными типами с фиксированной точкой соответственно. Вы торгуете точностью во фракции для запаса.

Сложение и вычитание дробных типов работает нормально, если вы смотрите на насыщенность. Для разделения, как сказал Патрос, масштабирование фактически сводится на нет. Так что вы должны сделать

Quotient = (x1/x2) << N;

или, чтобы сохранить точность

Quotient = (SInt16)(((SInt32)x1 << N)/x2); //x1 << N needs wide storage

Умножение и деление на целые числа работает нормально. Например, чтобы разделить на 6, вы можете просто написать

Quotient = x1/6; //equivalent to x1Float*(2^15)/6, stays scaled

А в случае деления на степень 2,

Quotient = x1 >> 3; //divides by 8, can't do x1 << -3 as Patros pointed out

Однако сложение и вычитание целых чисел не работает наивно. Сначала вы должны проверить, подходит ли целое число вашему типу x.y, сделать эквивалентный дробный тип и продолжить.

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

1 голос
/ 22 сентября 2009

В общем, скажем, вы будете использовать подписанное представление с фиксированной запятой 16.16. Таким образом, 32-битное целое будет иметь 16-битную целую часть со знаком и 16-битную дробную часть. Тогда я не знаю, какой язык используется при разработке iPhone (возможно, Objective-C?), Но этот пример находится на C:

#include <stdint.h>

typedef fixed16q16_t int32_t ;
#define FIXED16Q16_SCALE 1 << 16 ;

fixed16q16_t mult16q16( fixed16q16_t a, fixed16q16_t b )
{
    return (a * b) / FIXED16Q16_SCALE ;
}

fixed16q16_t div16q16( fixed16q16_t a, fixed16q16_t b )
{
    return (a * FIXED16Q16_SCALE) / b ;
}

Обратите внимание, что приведенное выше является упрощенной реализацией и не обеспечивает защиты от арифметического переполнения. Например, в div16q16 () я делаю несколько перед делением, чтобы сохранить точность, но в зависимости от операндов операция может переполниться. Вы можете использовать 64-битное промежуточное звено, чтобы преодолеть это. Также деление всегда округляется, потому что оно использует целочисленное деление. Это дает лучшую производительность, но может повлиять на точность итеративных вычислений. Исправления просты, но добавляют к накладным расходам.

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

В языке OO fixed16q16_t, естественно, будет кандидатом в класс с перегрузкой операторов, поэтому вы можете использовать его как обычный арифметический тип.

Может оказаться полезным конвертировать типы:

double fixed16q16_to_double( fixed16q16_t fix )
{
    return (double)fix / FIXED16Q16_SCALE ;
}

int fixed16q16_to_int( fixed16q16_t fix )
{
    // Note this rounds to nearest rather than truncates
    return ((fix + FIXED16Q16_SCALE/2)) / FIXED16Q16_SCALE ;
}

fixed16q16_t int_to_fixed16q16( int i )
{
    return i * FIXED16Q16_SCALE ;
}

fixed16q16_t double_to_fixed16q16( double d )
{
    return (int)(d * FIXED16Q16_SCALE) ;
}

То есть основы, можно получить более сложные и добавить триггеры и другие математические функции.

Исправлены операции сложения и вычитания со встроенными операторами + и - и их вариантами.

1 голос
/ 18 сентября 2009

Посмотрите на эту страницу, которая описывает алгоритмы быстрого умножения.

http://www.newton.dep.anl.gov/askasci/math99/math99199.htm

...