Уловка деления константы (степени двойки) на целое число

ПРИМЕЧАНИЕ. Это теоретический вопрос. Я доволен производительностью моего кода как такового. Мне просто интересно, есть ли альтернатива.

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

// The fixed value of the numerator
#define SIGNAL_PULSE_COUNT 0x4000UL

// The division that could use a neat trick.
uint32_t signalToReferenceRatio(uint32_t referenceCount)
{
    // Promote the numerator to a 64 bit value, shift it left by 32 so
    // the result has an adequate number of bits of precision, and divide
    // by the numerator.
    return (uint32_t)((((uint64_t)SIGNAL_PULSE_COUNT) << 32) / referenceCount);
}

Я нашел несколько (много) ссылок на трюки с делением на константу, как целочисленную, так и с плавающей запятой. Например, на вопрос Какой самый быстрый способ разделить целое число на 3? есть несколько хороших ответов, включая ссылки на другие академические и общественные материалы.

Учитывая, что числитель является постоянным и представляет собой целую степень двойки, есть ли хитрый трюк, который можно было бы использовать вместо фактического 64-битного деления; какая-то побитовая операция (сдвиги, AND, XOR и тому подобное) или подобное?

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

«Пусть решает компилятор» - не ответ, потому что я хочу знать, есть ли уловка.

Дополнительная контекстная информация

Я разрабатываю драйвер на микроконтроллере с 16-битными данными и 24-битным командным словом. Драйвер творит чудеса с периферийными модулями, чтобы получить счетчик импульсов опорной частоты для фиксированного числа импульсов частоты сигнала. Требуемый результат - это отношение сигнальных импульсов к опорному импульсу, выраженное в виде 32-битного значения без знака. Арифметика функции определяется производителем устройства, для которого я разрабатываю драйвер, и результат обрабатывается дальше, чтобы получить реальное значение с плавающей запятой, но это выходит за рамки этого вопроса.

Микроконтроллер, который я использую, имеет цифровой сигнальный процессор, который имеет ряд операций деления, которые я мог бы использовать, и я не боюсь делать это в случае необходимости. При таком подходе придется преодолеть некоторые незначительные проблемы, помимо сборки инструкций по сборке, чтобы заставить его работать, например, использование DSP для выполнения функции PID в ISR драйвера BLDC, но я не могу справиться с этим.


person Evil Dog Pie    schedule 28.01.2016    source источник
comment
Даже если он есть, я бы не стал использовать C, кроме сборки. Тогда вы можете быть уверены, что никакая оптимизация не будет произведена, и сможете все запрограммировать так, как хотите.   -  person cadaniluk    schedule 28.01.2016
comment
16-битных ARM ядер нет! А оптимизацию оставьте вашему компилятору. Не делайте преждевременных оптимизаций. Что такое сгенерированный код на ассемблере? И еще: оптимизация деления, но затем использование чисел с плавающей запятой ... непоследовательно.   -  person too honest for this site    schedule 28.01.2016
comment
Что вы ожидаете от этого трюка? Что это должно дать вам такое, чего не дает нормальное деление?   -  person user694733    schedule 28.01.2016
comment
Уловка, вероятно, состоит в том, чтобы использовать константы времени компиляции, а затем гарантировать, что функция встроена. После этого компилятор сможет оптимизировать от случая к случаю наилучшим образом.   -  person Lundin    schedule 28.01.2016
comment
Насколько я знаю, такой уловки не существует. Компилятор, вероятно, их тоже не знает. Есть много уловок, если делитель постоянный, но (кроме тривиальных случаев) нет, если числитель постоянный. FWIW GCC, похоже, тоже не знает подобного трюка.   -  person harold    schedule 28.01.2016
comment
@Olaf Спасибо. Это 16-битные данные, 24-битное командное слово dsPIC33EPXXX. Я исправил вопрос.   -  person Evil Dog Pie    schedule 28.01.2016
comment
@MikeofSST: Это все еще не ядро ​​ARM! Если все же настаиваете, укажите точное название ядра.   -  person too honest for this site    schedule 28.01.2016
comment
@cad Assembly может быть моим подходом, особенно если я использую DSP. Я буду писать код на C, компилировать, вручную оптимизировать промежуточный вывод сборки и добавлять его в небольшую, но священную и не поддерживаемую библиотеку ассемблерных подпрограмм.   -  person Evil Dog Pie    schedule 28.01.2016
comment
@Olaf Еще раз спасибо. Понятия не имею, почему я подумал, что это ARM. По крайней мере, сегодня я кое-чему научился. :-)   -  person Evil Dog Pie    schedule 28.01.2016
comment
@Olaf Плавающая точка используется только в последний момент, потому что протокол связи требует, чтобы конечное значение было представлено таким образом. Дополнительное вычисление - это полином, который оценивается с помощью команд умножения и накопления DSP для 32 целых чисел с 64-битными промежуточными результатами. Я не пытаюсь оптимизировать свой код, я пытаюсь выяснить, есть ли альтернатива (возможно, неосуществимая). Вопрос скорее интересен, чем нужен.   -  person Evil Dog Pie    schedule 28.01.2016
comment
@ user694733 Я ожидаю, что этот трюк расширит мой кругозор и заставит задуматься о математическом гении людей, использующих SO. В интересах поддержки моего кода и дружелюбия к коллегам я почти наверняка буду придерживаться разделения - оно читабельно и обслуживается - если только не будет чисто гениального ответа, который ясен, понятен и не запутает меня через две недели. . Моя цель - улучшить свои знания, а не код.   -  person Evil Dog Pie    schedule 28.01.2016
comment
Вы должны рассматривать отсутствие ответов как No.   -  person Klas Lindbäck    schedule 28.01.2016
comment
@ KlasLindbäck Вы могли быть правы. Будет ли «Нет» кратчайшим правильным ответом на SO? :-)   -  person Evil Dog Pie    schedule 28.01.2016
comment
Какой диапазон referenceCount? Полный 32-битный диапазон?   -  person chux - Reinstate Monica    schedule 28.01.2016
comment
Атрибуты трюка сводятся к 1/referenceCount и составлению дроби, масштабированной SIGNAL_PULSE_COUNT, OP может допускать небольшую ошибку, прямой power_of_2/x слишком медленный. Предположим, SIGNAL_PULSE_COUNT == 0 не беспокоит. Дайте этому посту немного времени.   -  person chux - Reinstate Monica    schedule 28.01.2016
comment
@chux В реальном мире referenceCount никогда не будет больше 24 бит. «Идеальный» диапазон находится между (приблизительно) 0x120000 и 0xB40000. Фактический диапазон зависит от физических факторов окружающей среды, таких как давление и температура, но это приведет к тому, что пределы диапазона изменятся не более чем на несколько сотен. И да, «трюк» по сути сводится к нахождению обратной величины, выраженной целым числом, с некоторым заранее определенным масштабированием.   -  person Evil Dog Pie    schedule 28.01.2016
comment
@chux Теперь ты заставил меня задуматься об этом! Может быть, где-нибудь есть криптографический метод для выполнения мультипликативных инверсий с использованием арифметики модулей?   -  person Evil Dog Pie    schedule 28.01.2016
comment
Еще 2 идеи: Воспользуйтесь соотношением ширины диапазона SIGNAL_PULSE_COUNT, равным 1:10. 2) Воспользуйтесь предыдущими расчетами, поскольку, как я предполагаю, SIGNAL_PULSE_COUNT не меняется слишком быстро. 3) Отсутствие ответов не означает отсутствие ответов, Терпение молодого кузнечика.   -  person chux - Reinstate Monica    schedule 28.01.2016
comment
Ссылка FP: stackoverflow.com/q/15380564/2410359   -  person chux - Reinstate Monica    schedule 28.01.2016
comment
Самое близкое, что я знаю, это spiral.ece.cmu.edu/mcm/gen. html. Это не совсем то, что вы хотите, но, возможно, вы сможете извлечь из этого урок.   -  person Jeff Hammond    schedule 28.01.2016
comment
некоторые dsp имеют встроенные функции, такие как _rcpsp из texas instruments (ищите версия сборки функции RCPSP). Он вычисляет 1 / x, где x - это плавающая точка. Конечно, это не то, что вы хотите :) Я имею в виду, что вы должны проверить набор инструкций DSP и найти любую инструкцию, которая будет вам полезна. Если вам повезет, вы даже можете найти внутреннюю версию, совместимую с C.   -  person seleciii44    schedule 29.01.2016
comment
@ seleciii44 На практике он, вероятно, будет закодирован так, как написано в вопросе, поскольку вряд ли это будет иметь большое значение, если расчет займет несколько микросекунд. Если мне действительно нужно его оптимизировать, я прислушусь к вашему совету и более подробно изучу набор инструкций DSP, так как могут быть инструкции, с которыми я не знаком. (Компилятор Microchip предоставляет некоторые __builtin_ функции для обеспечения доступа к инструкциям DSP из C.)   -  person Evil Dog Pie    schedule 29.01.2016
comment
Если вам удастся найти уловку, дайте нам знать. Но из-за характера вашего объяснения кажется, что даже если вы делаете это медленно, на него уходит очень небольшая часть времени выполнения. В таком случае, даже если бы вы могли уменьшить его количество циклов до нуля, вы бы не увидели больших улучшений.   -  person Mike Dunlavey    schedule 29.01.2016
comment
@MikeDunlavey Вы правы. Я бы не увидел значительных улучшений. Но я не пытаюсь улучшить свой код (что здорово - ха-ха), скорее я пытаюсь расширить свои знания, используя свой код в качестве контекста; это теоретический вопрос.   -  person Evil Dog Pie    schedule 29.01.2016
comment
Я написал быструю и неточную версию для C6400. Это зависит от инструкции LMBD, чтобы определить, где находится самая значимая 1 в целом числе. Он использует 10 инструкций и 1 поиск по таблице. В этом коде есть ошибка в несколько процентов, поэтому позже я переключился на IQMath от TI, который намного точнее.   -  person user3528438    schedule 29.01.2016


Ответы (4)


Вы не можете использовать хитрые математические приемы, чтобы не выполнять деление, но вы, конечно, все равно можете использовать уловки программирования, если знаете диапазон своего счетчика ссылок:

  • Ничто не может сравниться с предварительно вычисленной таблицей поиска по скорости.
  • Существуют быстрые алгоритмы приближения квадратного корня (вероятно, уже в вашем DSP), и вы можете улучшить приближение одной или двумя итерациями Ньютона-Рафсона. Если вычисления с числами с плавающей запятой достаточно точны для вас, вы, вероятно, сможете превзойти 64-битное целочисленное деление с точки зрения скорости (но не с точки зрения ясности кода).

Вы упомянули, что результат будет преобразован в числа с плавающей запятой позже, может быть полезно вообще не вычислять целочисленное деление, а использовать ваше оборудование с плавающей запятой.

person planckh    schedule 29.01.2016
comment
Это верно. Это кажется слишком легким. У меня в голове формируется форма «хитроумного трюка» (что, вероятно, неверно). Кое-что об использовании модульного мультипликативного обратного преобразования с модулем, являющимся ближайшим к SIGNAL_PULSE_COUNT простым числом, а затем с использованием частного случая теоремы Эйлера для достижения близкого приближения ... - person Evil Dog Pie; 29.01.2016
comment
Я имел в виду, конечно, быстрый приблизительный обратный квадратный корень. Вам обязательно стоит проверить этот метод, если вас интересует магия, вычисляющая обратные. - person planckh; 01.02.2016

Я разработал версию Matlab, используя арифметику с фиксированной точкой.

Этот метод предполагает, что целочисленная версия log2(x) может быть эффективно вычислена, что верно для dsPIC30 / 33F и TI C6000, у которых есть инструкция для определения наиболее значимой единицы целого числа.

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

nrdiv.m

function [ y ] = nrdiv( q, x, lut) 
                          % assume q>31, lut = 2^31/[1,1,2,...255]
    p2 = ceil(log2(x));   % available in TI C6000, instruction LMBD
                          % available in Microchip dsPIC30F/33F, instruction FF1L 
    if p2<8
        pre_shift=0;
    else
        pre_shift=p2-8;
    end                                  % shr = (p2-8)>0?(p2-8):0;

    xn = shr(x, pre_shift);              % xn  = x>>pre_shift;
    y  = shr(lut(xn), pre_shift);        % y   = lut[xn]>pre_shift; 
    y  = shr(y * (2^32 - y*x), 30);      % basic iteration
                                         % step up from q31 to q32
    y  = shr(y * (2^33 - y*x), (64-q));  % step up from q32 to desired q
    if q>39
        y = shr(y * (2^(1+q) - y*x), (q));  % when q>40, additional 
                                            % iteration is required, 
    end                                     % no step up is performed
end
function y = shr(x, r)
    y=floor(x./2^r);             % simulate operator >>
end

test.m

test_number = (2^22-12345);
test_q      = 48;

lut_q31 = round(2^31 ./ [1,[1:1:255]]);
display(sprintf('tested 2^%d/%d, diff=%f\n',test_q, test_number,...
                 nrdiv( 39, (2^22-5), lut_q31) - 2^39/(2^22-5)));

образец вывода

tested 2^48/4181959, diff=-0.156250

ссылка:

деление Ньютона – Рафсона

person user3528438    schedule 29.01.2016

Немного поздно, но вот мое решение.

Сначала несколько предположений:

Проблема:

X = N / D, где N - постоянная и степень двойки.

Все 32-битные беззнаковые целые числа.

X неизвестен, но у нас есть хорошая оценка (предыдущее, но уже не точное решение).

Точного решения не требуется.

Примечание: из-за целочисленного усечения это неточный алгоритм!

Итеративное решение - это нормально (улучшается с каждым циклом).

Деление намного дороже умножения:

Для 32-битного целого числа без знака для Arduino UNO:

'+/-' ~0.75us

'*' ~3.5us

'/' ~ 36us 4 Мы стремимся заменить в основном метод Ньютона:

Xnew=Xold-f(x)/(f`(x)

где f (x) = 0 для искомого решения.

Решив это, я получаю:

Xnew=XNew*(C-X*D)/N

где C = 2 * N

Первый трюк:

Теперь, когда числитель (константа) теперь является делителем (константой), одно решение здесь (которое не требует, чтобы N было степенью 2):

Xnew=XNew*(C-X*D)*A>>M

где C = 2 * N, A и M - константы (ищите деление на постоянные трюки).

или (оставаясь с методом Ньютона):

Xnew=XNew*(C-X*D)>>M

где C = 2 >> M, где M - мощность.

Итак, у меня есть 2 '*' (7.0us), '-' (0.75us) и '>>' (0.75us?) Или всего 8,5us (а не 36us), исключая другие накладные расходы.

Ограничения:

Поскольку тип данных - 32-битный беззнаковый, 'M' не должен превышать 15, иначе возникнут проблемы с переполнением (вы, вероятно, можете обойти это, используя 64-битный промежуточный тип данных).

N> D (иначе алгоритм взорвется! Хотя бы с целым числом без знака)

Очевидно, алгоритм будет работать со знаковыми и плавающими типами данных)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
int main(void)
{
  unsigned long c,d,m,x;
  // x=n/d where n=1<<m
  m=15;
  c=2<<m;
  d=10;
  x=10;
  while (true)
  {
    x=x*(c-d*x)>>m;
    printf("%ld",x);
    getchar();
  }
  return(0);
}
person AlanC    schedule 12.07.2016
comment
Опечатка: где C = 2 ›› M, где M - мощность, должно быть где C = 2 ‹---------------- M, где M - мощность. - person AlanC; 12.07.2016

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

/*
 * Converts the reference frequency count for a specific signal frequency
 * to a ratio.
 *   Xs = Ns * 2^32 / Nr
 *   Where:
 *   2^32 is a constant scaling so that the maximum accuracy can be achieved.
 *   Ns is the number of signal counts (fixed at 0x4000 by hardware).
 *   Nr is the number of reference counts, passed in W1:W0.
 * @param  W1:W0    The number of reference frequency pulses.
 * @return W1:W0    The scaled ratio.
 */
    .align  2
    .global _signalToReferenceRatio
    .type   _signalToReferenceRatio, @function

    ; This is the position of the most significant bit of the fixed Ns (0x4000).
    .equ    LOG2_DIVIDEND,  14
    .equ    DIVISOR_LIMIT,  LOG2_DIVIDEND+1
    .equ    WORD_SIZE,      16

_signalToReferenceRatio:
    ; Create a dividend, MSB-aligned with the divisor, in W2:W3 and place the
    ; number of iterations required for the MSW in [W14] and the LSW in [W14+2].
    LNK     #4
    MUL.UU  W2, #0, W2
    FF1L    W1, W4
    ; If MSW is zero the argument is out of range.
    BRA     C, .returnZero
    SUBR    W4, #WORD_SIZE, W4
    ; Find the number of quotient MSW loops.
    ; This is effectively 1 + log2(dividend) - log2(divisor).
    SUBR    W4, #DIVISOR_LIMIT, [W14]
    BRA     NC, .returnZero
    ; Since the SUBR above is always non-negative and the C flag set, use this
    ; to set bit W3<W5> and the dividend in W2:W3 = 2^(16+W5) = 2^log2(divisor).
    BSW.C   W3, W4
    ; Use 16 quotient LSW loops.
    MOV     #WORD_SIZE, W4
    MOV     W4, [W14+2]

    ; Set up W4:W5 to hold the divisor and W0:W1 to hold the result.
    MOV.D   W0, W4
    MUL.UU  W0, #0, W0

.checkLoopCount:
    ; While the bit count is non-negative ...
    DEC     [W14], [W14]
    BRA     NC,  .nextWord

.alignQuotient:
    ; Shift the current quotient word up by one bit.
    SL      W0, W0
    ; Subtract divisor from the current dividend part.
    SUB     W2, W4, W6
    SUBB    W3, W5, W7
    ; Check if the dividend part was less than the divisor.
    BRA     NC, .didNotDivide
    ; It did divide, so set the LSB of the quotient.
    BSET    W0, #0
    ; Shift the remainder up by one bit, with the next zero in the LSB.
    SL      W7, W3
    BTSC    W6, #15
    BSET    W3, #0
    SL      W6, W2
    BRA     .checkLoopCount
.didNotDivide:
    ; Shift the next (zero) bit of the dividend into the LSB of the remainder.
    SL      W3, W3
    BTSC    W2, #15
    BSET    W3, #0
    SL      W2, W2
    BRA     .checkLoopCount

.nextWord:
    ; Test if there are any LSW bits left to calculate.
    MOV     [++W14], W6
    SUB     W6, #WORD_SIZE, [W14--]
    BRA     NC, .returnQ
    ; Decrement the remaining bit counter before writing it back.
    DEC     W6, [W14]
    ; Move the working part of the quotient up into the MSW of the result.
    MOV     W0, W1
    BRA     .alignQuotient

.returnQ:
    ; Return the quotient in W0:W1.
    ULNK
    RETURN

.returnZero:
    MUL.UU  W0, #0, W0
    ULNK
    RETURN
.size   _signalToReferenceRatio, .-_signalToReferenceRatio
person Evil Dog Pie    schedule 12.07.2016