Re[13]: Попинайте арифметику
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 28.06.14 08:03
Оценка:
Здравствуйте, Marty, Вы писали:

W>>Аналогично и с сигнализацией переполнения — везде всё настолько по-разному реализовано, что какого-то единого интерфейса предложить нельзя.

M>Есть процессоры без флаговых регистров? А язык C для них есть?
M>Intel, ARM, MIPS — есть флаги;

Ты явно что-то путаешь. У MIPS флагового регистра в привычном виде нет. У него есть команды перехода по условию и установки целевого регистра по условию (в 0 или 1), а условие выглядит как сравнение src1 и src2, при этом src1 — регистр, а src2 — регистр или константа. Условия — стандартный набор типа "равно/больше/меньше" с вариантами со знаком или без. Такая же ситуация на Alpha. При разработке обоих тут следовали идее, что флаговый регистр (PSW) — узкое место и не надо завязываться на него для всех операций.

Для примера возьмём MIPS код в libgmp (mpn/mips32/add_n.asm, сложение длинных натуральных), комментирую:

C INPUT PARAMETERS
C res_ptr       $4
C s1_ptr        $5
C s2_ptr        $6
C size          $7

ASM_START()
PROLOGUE(mpn_add_n)

        lw      $10,0($5); младшее слово первого аргумента
        lw      $11,0($6); младшее слово второго аргумента

        addiu   $7,$7,-1
        and     $9,$7,4-1       C number of limbs in first loop
        beq     $9,$0,.L0       C if multiple of 4 limbs, skip first loop
         move   $2,$0; инициализация переноса в 0

        subu    $7,$7,$9

; входим со значениями в текущей позиции, уже загруженными в $10 и $11
.Loop0: addiu   $9,$9,-1; декремент переменной цикла - позиции в массиве
        lw      $12,4($5); прочли следующее слово из 1-го аргумента
        addu    $11,$11,$2; прибавили перенос к текущему значению из 2-го аргумента
        lw      $13,4($6); прочли следующее слово из 2-го аргумента
        sltu    $8,$11,$2; запись результата сравнения на меньше без знака,
                         ; то есть $8 получит 1 только если перенос с предыдущего цикла
                         ; дал беззнаковое переполнение, иначе ставится в 0
        addu    $11,$10,$11; складываем два входных значения текущей позиции
        sltu    $2,$11,$10; запись результата сравнения на меньше без знака, аналогично,
                          ; запись факта переполнения
        sw      $11,0($4); выгрузка суммы на текущем шаге в массив результата
        or      $2,$2,$8; объединение двух признаков переноса в один логическим "или"

        addiu   $5,$5,4; инкремент указателя 1-го аргумента на 1 слово
        addiu   $6,$6,4; инкремент указателя 2-го аргумента на 1 слово
        move    $10,$12; следующее прочтённое слово из 1-го аргумента сделать текущим
        move    $11,$13; следующее прочтённое слово из 2-го аргумента сделать текущим
        bne     $9,$0,.Loop0
         addiu  $4,$4,4


Дальнейший код (ещё один цикл, более крупными прыжками) пропустил. Я надеюсь, по комментариям видно, как получается сумма и вычисление переноса. На C один шаг такого суммирования внутри цикла можно было бы записать так:

unsigned /* out_carry */
sum_step(unsigned *result_word,
              unsigned in_carry, unsigned input1, unsigned input2)
{
    unsigned tmp, ovf1, ovf2;
    tmp = input2 + in_carry;
    ovf2 = (tmp < in_carry);
    tmp += input1;
    ovf1 = (tmp < input1);
    *result_word = tmp;
    return ovf1 | ovf2;
}


Громоздко? Да. Дольше варианта, когда явно ставится и анализируется флаг переноса?
Да, если считать на количество команд.
Но: работает? Да, работает, корректно. И в описанном сишном варианте гарантированно работает независимо от архитектуры и без ассемблерных вставок (которые, конечно, на каком-нибудь x86 ускорят итерацию в несколько раз).

Аналогичные проблемы, но несколько более облегчённые, были на S/360 (до введения в S/390 команд типа ALC), несмотря на то, что это CISC. Там надо было точно так же сделать два сложения, от обоих сохранить выходной бит 1 поля CC и сделать логический OR для получения признака переноса на следующий цикл. По сравнению с этим привычный тебе формат (появившийся впервые, насколько я смог раскопать, в PDP-11), это уже позднее сверхудачное нововведение.

M> контроллеры всякие — все, что видел, тоже с флаговым регистром в том или ином виде. Поведение несколько отличается, но это как раз повод просто упрятать конкретную реализацию в стд библиотеку. Просто стандарт обязывал бы производителя платформы поддерживать еще пару-тройку функций, вот и все.


Как видишь, нет, не с флаговым регистром. С другой стороны, я полностью согласен с тем, что функцию, идентичную по поведению такой sum_step, имеет смысл подумать внести в стандарт, хотя бы в необязательные, но в расширения со стандартизованным интерфейсом. Ну а на каком-нибудь x86 она превратится в банальную цепочку из shr + adc + setc, с возможностью устранения крайних элементов в ней, если между вызовами CF не меняется.

Операции с числами со знаком тоже легко делаются через такое же, просто старший бит старшего слова значения определяет знак Но обрати внимание, что тот же GMP сделал mpn нижним уровнем, а знаки чисел уже над ним. При "реально" длинной арифметике так эффективнее.

M>>>А что, в Java такие вещи можно отслеживать? Или там исключениями будут кидаться?

W>>А там виртуальная машина жёстко зафиксирована. Неважно какой у тебя процессор, архитектура, интерпретатор или jit-компилятор, — изволь эмулировать то что написано. Например, тот же long в java с точки зрения программы всегда 64-битный и в two's complement представлении — это гарантируется. Если процессор так не умеет, то реализация обязана, например, программной эмуляцией добиться соответствующего поведения.
M>Ну, это да. Но как там с переполнениями?

Не проверяются, там арифметика гарантированно кольцевая по модулю (2**32 или 2**64).
Зато ты можешь проверить результаты как минимум сложения или вычитания практически таким же способом, как я показал: если c = a+b, то проверка на переполнение делается через:

((b >= 0) ? (c < a) : (c > a))


Для GCC аналогичное можно получить, скомпилировав с -fwrapv (для целого входного файла для версий до 4.4, с грануляцией до функции, начиная с 4.4). Clang знает тот же -fwrapv для входного файла.

Вот с проверкой умножения, конечно, сложнее всего. Обратное деление слишком дорого. Если нет ассемблерной поддержки, лучше брать входные значения размером в половину доступного слова. Будет дольше, зато надёжно.
The God is real, unless declared integer.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.