Быстрое знаковое 16-битное деление на 7 для 6502

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

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

x/7 ~= (x + x/8 + x/64 ... )/8

указывает, что для обработки 16-битного диапазона, вероятно, потребуется более 100 циклов для завершения из-за единственного регистра накопителя 6502 и относительной медленности сдвига отдельных битов памяти на 6502.

Я думал, что справочная таблица может помочь, но на 6502 я, конечно, ограничен поисковыми таблицами размером 256 байт или меньше. С этой целью можно предположить существование двух 256-байтовых поисковых таблиц, xdiv7 и xmod7, которые при использовании однобайтового значения без знака в качестве индекса в таблице могут быстро получить результат деления байта на 7 или по модулю 7 соответственно. Однако я не уверен, как я могу использовать их, чтобы найти значения для полного 16-битного диапазона.

Параллельно мне также нужен алгоритм по модулю 7, хотя в идеале любое решение, которое можно выработать с делением, также даст результат mod7. Если требуются дополнительные предварительно вычисленные таблицы, я могу добавить их, если общие требования к памяти для всех таблиц не превышают примерно 3 КБ.

Хотя мне в конечном итоге нужен алгоритм деления со знаком, беззнакового будет достаточно, потому что я могу обобщить его на подписанную процедуру по мере необходимости.

Будем очень благодарны любой помощи.


person markt1964    schedule 18.07.2018    source источник
comment
С 6502 незнаком. Но на первый взгляд вроде множителя тоже нет? Если вы можете эффективно выполнить умножение 16 x 16 - 32 бит, то деление на 7 будет простым и быстрым.   -  person Mysticial    schedule 19.07.2018
comment
Помимо этого, алгоритм добавления групп из 3 бит, который вы уже упомянули, вероятно, является самым быстрым подходом, отличным от LUT. Отсутствие эффективного переключателя передач станет огромным препятствием.   -  person Mysticial    schedule 19.07.2018
comment
100 циклов на самом деле звучат не так уж плохо для деления. 8-битное деление составило более 100 циклов на процессоре 8086.   -  person Ross Ridge    schedule 19.07.2018
comment
Да, хотя я надеюсь, что смогу использовать поиск по таблице и некоторые дополнения. Сдвиг 6502 бит имеет стоимость сдвига битов и само по себе почти вдвое дороже, чем добавление, если данные уже не находятся в аккумуляторе.   -  person markt1964    schedule 19.07.2018
comment
Вы пробовали умножить на 022222 вот так. Я думаю, что вы также можете использовать LUT для переключения 3 в вышеупомянутом подходе, потому что у вас нет переключателя ствола. Таким образом, вы также можете выполнить параллельную операцию по модулю 7, потому что это просто сумма цифр в базе 8 по модулю 7.   -  person phuclv    schedule 19.07.2018
comment
Вы можете сделать это с помощью таблиц поиска, но для этого требуется 5 таблиц по 256 байтов и 2 таблицы по 13 байтов. Результирующая сборка состоит из 7 таблиц поиска и 4 инструкций добавления. Если вам интересно, я опубликую C-код, который генерирует таблицы поиска и демонстрирует деление на 7.   -  person user3386109    schedule 19.07.2018
comment
Пожалуйста, сделайте это и не стесняйтесь размещать это в качестве ответа. Спасибо.   -  person markt1964    schedule 19.07.2018


Ответы (3)


Примечание. Как указано в комментариях @Damien_The_Unbeliever, таблицы upperHigh и lowerLow идентичны. Так что их можно объединить в единую таблицу. Однако такая оптимизация усложнит чтение кода и написание объяснений, поэтому объединение таблиц остается упражнением для читателя.


В приведенном ниже коде показано, как сгенерировать частное и остаток при делении 16-битного значения без знака на 7. Самый простой способ объяснить код (IMO) - это пример, поэтому давайте рассмотрим разделение 0xa732 на 7. Ожидаемый результат:

quotient = 0x17e2
remainder = 4  

Мы начинаем с рассмотрения ввода как двух 8-битных значений, байта upper и байта lower. Байт upper равен 0xa7, а байт lower равен 0x32.

Мы вычисляем частное и остаток от upper байта:

0xa700 / 7 = 0x17db
0xa700 % 7 = 3 

Итак, нам понадобятся три таблицы:

  • upperHigh хранит старший байт частного: upperHigh[0xa7] = 0x17
  • upperLow хранит младший байт частного: upperLow[0xa7] = 0xdb
  • upperRem сохраняет остаток: upperRem[0xa7] = 3

И мы вычисляем частное и остаток от байта lower:

0x32 / 7 = 0x07
0x32 % 7 = 1

Итак, нам понадобятся две таблицы:

  • lowerLow хранит младший байт частного: lowerLow[0x32] = 0x07
  • lowerRem сохраняет остаток: lowerRem[0x32] = 1

Теперь нам нужно собрать окончательный ответ. Остаток - это сумма двух остатков. Поскольку каждый остаток находится в диапазоне [0,6], сумма находится в диапазоне [0,12]. Таким образом, мы можем использовать два поиска по 13 байт, чтобы преобразовать сумму в окончательный остаток и перенос.

Младший байт частного представляет собой сумму этого переноса и значений из таблиц lowerLow и upperLow. Обратите внимание, что сумма может генерировать перенос в старший байт.

Старший байт частного представляет собой сумму этого переноса и значения из таблицы upperHigh.

Итак, чтобы завершить пример:

remainder = 1 + 3 = 4              // simple add (no carry in)
lowResult = 0x07 + 0xdb = 0xe2     // add with carry from remainder
highResult = 0x17                  // add with carry from lowResult

Ассемблерный код для реализации этого состоит из 7 таблиц поиска, инструкции добавления без переноса и двух инструкций добавления с переносом.


#include <stdio.h>
#include <stdint.h>

uint8_t upperHigh[256];  // index:(upper 8 bits of the number)  value:(high 8 bits of the quotient)
uint8_t upperLow[256];   // index:(upper 8 bits of the number)  value:(low  8 bits of the quotient)
uint8_t upperRem[256];   // index:(upper 8 bits of the number)  value:(remainder when dividing the upper bits by 7)
uint8_t lowerLow[256];   // index:(lower 8 bits of the number)  value:(low  8 bits of the quotient)
uint8_t lowerRem[256];   // index:(lower 8 bits of the number)  value:(remainder when dividing the lower bits by 7)
uint8_t carryRem[13]    = { 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 };
uint8_t combinedRem[13] = { 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5 };

void populateLookupTables(void)
{
    for (uint16_t i = 0; i < 256; i++)
    {
        uint16_t upper = i << 8;
        upperHigh[i] = (upper / 7) >> 8;
        upperLow[i] = (upper / 7) & 0xff;
        upperRem[i] = upper % 7;

        uint16_t lower = i;
        lowerLow[i] = lower / 7;
        lowerRem[i] = lower % 7;
    }
}

void divideBy7(uint8_t upperValue, uint8_t lowerValue, uint8_t *highResult, uint8_t *lowResult, uint8_t *remainder)
{
    uint8_t temp = upperRem[upperValue] + lowerRem[lowerValue];
    *remainder = combinedRem[temp];
    *lowResult = upperLow[upperValue] + lowerLow[lowerValue] + carryRem[temp];
    uint8_t carry = (upperLow[upperValue] + lowerLow[lowerValue] + carryRem[temp]) >> 8;  // Note this is just the carry flag from the 'lowResult' calcaluation
    *highResult = upperHigh[upperValue] + carry;
}

int main(void)
{
    populateLookupTables();

    uint16_t n = 0;
    while (1)
    {
        uint8_t upper = n >> 8;
        uint8_t lower = n & 0xff;

        uint16_t quotient1  = n / 7;
        uint16_t remainder1 = n % 7;

        uint8_t high, low, rem;
        divideBy7(upper, lower, &high, &low, &rem);
        uint16_t quotient2 = (high << 8) | low;
        uint16_t remainder2 = rem;

        printf("n=%u q1=%u r1=%u q2=%u r2=%u", n, quotient1, remainder1, quotient2, remainder2);
        if (quotient1 != quotient2 || remainder1 != remainder2)
            printf(" **** failed ****");
        printf("\n");

        n++;
        if (n == 0)
            break;
    }
}
person user3386109    schedule 19.07.2018
comment
Может быть, глупый вопрос, но отличается ли содержимое upperHigh и lowerLow? - person Damien_The_Unbeliever; 19.07.2018
comment
@Damien_The_Unbeliever Ой, они действительно такие же. Обновлю ответ, спасибо! - person user3386109; 19.07.2018
comment
Если они одинаковы, я не понимаю, как это добавляет ясности, чтобы оставить их как отдельные ... объединенная таблица может просто иметь имя, которое отражает два разных использования, вместо того, чтобы тратить дополнительные 256 байтов, которые не дают преимущества в производительности и ничтожно малое преимущество читабельности, которое нельзя получить эквивалентным образом, просто используя описательное именование. - person markt1964; 19.07.2018
comment
@ markt1964: В asm просто поместите две метки в одно и то же место, не фактически дублируйте память. В C - #define или static const uint8_t *upperHigh = lowerLow; - person Peter Cordes; 20.07.2018

В подпрограммах беззнакового целочисленного деления для 8-битового деления на 7:

;Divide by 7 (From December '84 Apple Assembly Line)
;15 bytes, 27 cycles
  sta  temp
  lsr
  lsr
  lsr
  adc  temp
  ror
  lsr
  lsr
  adc  temp
  ror
  lsr
  lsr

Оценка примерно 100 циклов со сдвигами была довольно точной: 104 цикла до последнего цикла, всего 106 циклов, не включая rts, 112 циклов для всей функции.
ПРИМЕЧАНИЕ: после сборки для C64 и используя эмулятор VICE для C64, я обнаружил, что алгоритм не работает, например, 65535 дает 9343, а правильный ответ - 9362.

   ; for 16 bit division  by 7
   ; input:
  ;   register A is low byte
  ;   register X is high byte
  ; output 
  ;   register A is low byte
  ;   register X is high byte
  ;
  ; memory on page zero
  ; temp     is on page zero, 2 bytes
  ; aHigh    is on page zero, 1 byte
  --
  sta temp
  stx temp+1
  stx aHigh
  --
  lsr aHigh
  ror a
  lsr aHigh
  ror a
  lsr aHigh
  ror a
  ---
  adc temp
  tax
  lda aHigh
  adc temp+1
  sta aHigh
  txa
  --
  ror aHigh
  ror a
  lsr aHigh
  ror a
  lsr aHigh
  ror a
  --
  adc temp
  tax
  lda aHigh
  adc temp+1
  sta aHigh
  txa
  --
  ror aHigh
  ror a
  lsr aHigh
  ror a
  lsr aHigh
  ror a     -- 104 cycles
  ;-------
  ldx aHigh  ; -- 106
  rts        ; -- 112 cycles
person alvalongo    schedule 19.07.2018
comment
Ваш код вычисляет ((x / 8 + x) / 8 + x) / 8, где формула OP читает (x + x / 8 + x / 64) / 8. Какой из них правильный для деления на 7? - person Sep Roland; 22.07.2018
comment
Я так понимаю, вы обобщили алгоритм деления байта на 7, но разве это не должно влечь за собой, что помимо x/8 и x/64 также требуются x/512, x/4096 и x/32768? Это доведет количество циклов до 250! - person Sep Roland; 22.07.2018
comment
Я уже заходил по этой ссылке раньше, и действительно, ваш правильный перевод найденного там кода. Все еще не то, что написал OP. Возможно, вам нужно прояснить это в своем ответе. - person Sep Roland; 22.07.2018
comment
Сен, я не понимаю, как работает исходное 8-битное деление. Я распространяюсь только на 16-битные значения. - person alvalongo; 22.07.2018
comment
Alvalongo, используйте @SepRoland, чтобы уведомить кого-нибудь, когда вы ответите на его комментарий. (Вы получаете уведомление, потому что мы оставляем комментарии под вашим ответом.) - person Peter Cordes; 22.07.2018
comment
В основном, необходимо повторить второй шаг (тот, который выполняет x/64 плюс добавление) 4 раза. - person Sep Roland; 22.07.2018
comment
Плохие новости: после компиляции для C64 и использования эмулятора VICE для C64 я обнаружил, что алгоритм не работает, поскольку 65535 дает 9343, а правильный ответ - 9362. - person alvalongo; 22.07.2018
comment
Неудивительно, если вы проделали те же шаги, что и 8-битное деление. Вам нужно больше шагов для более широкого деления (чтобы получить 16-битную точность), а также чтобы каждый шаг был шире. - person Peter Cordes; 23.07.2018

Другой способ сделать это - преобразовать деление в умножение.

Чтобы вычислить коэффициент умножения, нам нужно взять обратную величину. По сути, мы делаем:

d = n*(1/7)

Для большей точности мы умножаем на удобную степень 2. 2 ^ 16 хорошо работает:

d = floor(n*floor(65536/7)/65536)

Коэффициент умножения: этаж (65536/7), что составляет 9362. Размер результата будет:

ceiling(log2(65535*9362)) = 30 bits (4 bytes rounded up)

Затем мы можем отбросить два младших байта и разделить на 65536 или просто использовать верхние 2 байта для конечного результата.

Чтобы вычислить, какие повороты и сложения нам нужны, мы исследуем двоичное представление множителя 9362:

10010010010010

Обратите внимание на повторение битовой комбинации. Таким образом, эффективная схема была бы для расчета:

((n*9*256/4 + n*9)*8 + n)*2 = 9362*n

Для вычисления n * 9 требуется только потолок (log2 (65535 * 9)) = 20 бит (3 байта).

В псевдосборке это:

LDA number          ; lo byte
STA multiply_nine
LDA number+1        ; high byte
STA multiply_nine+1
LDA #0              
STA multiply_nine+2 ; 3 byte result

ASL multiply_nine   ; multiply by 2
ROL multiply_nine+1
ROL mulitply_nine+2
ASL multiply_nine   ; multiply by 2 (4)
ROL multiply_nine+1
ROL mulitply_nine+2
ASL multiply_nine   ; multiply by 2 (8)
ROL multiply_nine+1
ROL mulitply_nine+2

CLC                 ; not really needed as result is only 20 bits, carry always zero
LDA multiply_nine
ADC number
STA multiply_nine
LDA multiply_nine+1
ADC number+1
STA multiply_nine+1
LDA multiply_nine+2
ADC #0
STA multiply_nine+2 ; n*9

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

person Guillermo Phillips    schedule 28.12.2018
comment
Это похоже на ту же мультипликативную обратную функцию с фиксированной точкой, которую компиляторы используют на машинах с быстрыми инструкциями умножения, для точного деления на константу по всем возможным дивидендам, я думаю. Но для этого обычно нужно сдвинуть вправо верхнюю половину полного результата умножения. Почему GCC использует умножение на странное число при реализации целочисленного деления?. Да, и для сложных делителей, таких как 7, есть еще несколько шагов на x86: godbolt.org/z/e5ut7S. Точна ли эта версия с фиксированной точкой для всех 16-битных дивидендов? - person Peter Cordes; 30.12.2018
comment
Быстрая проверка в Excel показывает, что это не совсем так. Но в худшем случае будет только 1 выход, это может быть нормально, а может и нет. Однако точность всегда можно повысить за счет размера кода. Я ожидаю, что использование, скажем, коэффициента масштабирования 2 ^ 24 устранит несоответствие. Размер кода можно уменьшить, используя циклы для повторяющихся сдвигов, но это займет больше циклов. - person Guillermo Phillips; 30.12.2018