Быстрое сложение BCD

Этот вопрос был вдохновлен вопросом, который я недавно встретил на Stackoverflow. Это напомнило мне, что на ранних этапах разработки x86-64 ISA я написал 32-битный код x86 для сложения BCD без использования инструкций поддержки BCD, доступных в x86 ISA. Это относится к тому, что, судя по комментариям, на некоторых платформах может быть известно как упакованный BCD, то есть одна десятичная цифра, хранящаяся в тетраде битов, обычно называемая полубайтом.

Хотя было совершенно очевидно, что удаление инструкций DAA и DAS (коррекция десятичной дроби после сложения/вычитания) безопасно с точки зрения производительности, лучше всего было иметь под рукой конкретные доказательства того, как можно реализовать, скажем, сложение BCD без этих инструкций. инструкции на современном процессоре Athlon. Мой код того времени, откопанный в глубинах Интернета, слегка скорректированный и исправленный, приведен ниже. Обратите внимание, что RCR тогда не был таким большим убийцей производительности, как сейчас. Я вновь рассмотрел этот вопрос с сегодняшней точки зрения, решив остаться с обработкой 32-битными фрагментами для упрощения представления данных на различных архитектурах. Как распространить обработку 64-битных фрагментов на 64-битные ISA, очевидно.

Концептуально сложение BCD является простым. Чтобы облегчить распространение переноса между полубайтами, к каждому полубайту двоичной суммы двух исходных операндов временно добавляют 6. Для тех полубайтов, которые не производят перенос, либо вычитают 6 из полубайта (метод отмены), либо повторяют исходное двоичное сложение, но без добавления 6 к этому полубайту (метод повтора). Работая над своим исходным кодом в 1999 году, я обнаружил, что использование метода повторного выполнения приводит к несколько более высокой производительности, возможно, из-за более короткой цепочки зависимостей. Основная задача при сложении BCD заключается в определении того, где происходят переносы между полубайтами. Мой подход заключался в том, чтобы проверить младший бит следующего старшего полубайта: если его значение отличается от предварительного значения бита суммы (исключающее ИЛИ двух исходных младших битов), то должен был произойти перенос, поэтому следующий младший полубайт создал ошибку. выполнять. Несколько неприятный частный случай — обработка выноса из наиболее значимого полубайта.

Когда Кнут опубликовал том 4a TAOCP в 2011 году, я обнаружил, что он содержит алгоритм сложения BCD с помощью метода отмены и обнаружения переносов между полубайтами путем проверки старших битов соответствующих полубайтов в исходных операндах и скорректированной суммы, чтобы проверить, есть ли вынос из этого кусочка. Это делает код чрезвычайно элегантным, когда последняя функциональность выражается через вычисление большинства из 3, которое Кнут предпочитает называть более общим именем медиана.

Экспериментально я обнаружил, что метод повтора обычно превосходит метод отмены в широком диапазоне архитектур: x86, ARM, RISC-V, Sparc, Power. Что касается обнаружения переносов полубайтов, метод LSB и метод MSB обычно кажутся равными с точки зрения производительности, но метод Кнута MSB превосходит архитектуры, которые могут более эффективно реализовать медианную операцию за счет использования преимуществ дополнительных логических операций. (например, ORN, ANDN, XNOR, NOR, NAND) за пределами минимального набора логических операций (OR, AND, XOR, NOT). Это особенно актуально для современных графических процессоров NVIDIA, которые предоставляют инструкцию LOP3, способную вычислить любую логическую операцию с тремя входами. Это сопоставляет медианную операцию с любым из входных данных, возможно, отрицательным, в один экземпляр LOP3.

Хотя я считаю свои навыки работы с битами довольно солидными, прошлый опыт показал, что я часто нахожусь на отметке 85% по сравнению с лучшим решением, предложенным в ответах здесь. В частности, я хочу выяснить: существуют ли более эффективные способы решения проблемы переноса полубайтов? Использование простой ISA, такой как 32-битный RISC-V, может дать лучшее представление об этом. Любое превосходное решение, применимое к этой ISA, вряд ли повредит ISA с более широким набором доступных инструкций. Помимо этого, приветствуется любая умная идея, которая может уменьшить накладные расходы на обнаружение переноса полубайтов, о чем свидетельствует уменьшенное количество инструкций или укороченная цепочка зависимостей.

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

#define NUM_TESTS    (100000000)

#define TEST_PORTABLE_BCD64 (1) // 0: x86-specific test of 18-digit BCD arithm.
                                // 1: portable test of 16-digit BCD arithmetic
#define PORTABLE     (1)  // 0: x86-specific inline assembly
#define NATIVE_64BIT (0)  // applies only if PORTABLE==1
#define USE_KNUTH    (1)  // applies only if PORTABLE==1 && NATIVE_64BIT==0

#if PORTABLE && !NATIVE_64BIT
/* majority-of-3 operation that Knuth refers to more generically as median */
uint32_t median32 (uint32_t x, uint32_t y, uint32_t z)
{
    return (x | (y & z)) & (y | z);
}

/* 8-digit BCD addition with carry-out */
uint32_t BCD32_ADDcc (uint32_t x, uint32_t y, uint32_t *cy)
{
#if USE_KNUTH
    // derived from Knuth, TAOCP 4a (2011), answer to exercise 100 section 7.1.3
    uint32_t z, u, t;
    z = y + 0x66666666;
    u = x + z;
    t = median32 (x, z, ~u) & 0x88888888;
    *cy = t >> 31;
    return (x + y) + (t - (t >> 2));
#else // ! USE_KNUTH
    const uint32_t sixes = 0x66666666;
    const uint32_t eights = 0x88888888;
    uint32_t xe, s, u, carry_out;
    xe = x + sixes;         // add "nibble offset"; cannot overflow
    s = xe ^ y;             // preliminary sum bits
    u = xe + y;
    carry_out = u < xe;     // carry-out from most significant nibble
    s = (carry_out << 31) | ((s ^ u) >> 1); // sum bits with carry-in
    s = s & eights;         // isolate nibble lsbs with carry-in
    *cy = carry_out;        // record carry-out from most significant nibble
    u = (x + y) + (s - (s >> 2)); // add "nibble offset" to nibbles that need it
    return u;
#endif // USE_KNUTH
}

/* 8-digit BCD addition with carry-in */
uint32_t BCD32_ADDC (uint32_t x, uint32_t y, uint32_t cy)
{
#if USE_KNUTH
    // derived from Knuth, TAOCP 4a (2011), answer to exercise 100 section 7.1.3
    uint32_t z, u, t;
    x = x + cy;
    z = y + 0x66666666;
    u = x + z;
    t = median32 (x, z, ~u) & 0x88888888;
    return (x + y) + (t - (t >> 2));
#else // !USE_KNUTH
    const uint32_t sixes = 0x66666666;
    const uint32_t eights = 0x88888888;
    uint32_t xe, s, u, carry_out;
    y = y + cy;             // incorporate carry-in; cannot overflow
    xe = x + sixes;         // add "nibble offset"; cannot overflow
    s = xe ^ y;             // preliminary sum bits
    u = xe + y;              
    carry_out = u < xe;     // carry-out from most significant nibble
    s = (carry_out << 31) | ((s ^ u) >> 1); // sum bits with carry-in
    s = s & eights;         // isolate nibble lsbs with carry-in
    u = (x + y) + (s - (s >> 2)); // add "nibble offset" to nibbles that need it
    return u;
#endif // USE_KNUTH
}

/* 8-digit BCD addition with carry-in and carry-out */
uint32_t BCD32_ADDCcc (uint32_t x, uint32_t y, uint32_t *cy)
{
#if USE_KNUTH
    // derived from Knuth, TAOCP 4a (2011), answer to exercise 100 section 7.1.3
    uint32_t z, u, t;
    x = x + (*cy);
    z = y + 0x66666666;
    u = x + z;
    t = median32 (x, z, ~u) & 0x88888888;
    *cy = t >> 31;
    return (x + y) + (t - (t >> 2));
#else // !USE_KNUTH
    const uint32_t sixes = 0x66666666;
    const uint32_t eights = 0x88888888;
    uint32_t xe, s, u, carry_out;
    y = y + (*cy);          // incorporate carry-in; cannot overflow
    xe = x + sixes;         // add "nibble offset"; cannot overflow
    s = xe ^ y;             // preliminary sum bits
    u = xe + y;              
    carry_out = u < xe;     // carry-out from most significant nibble
    s = (carry_out << 31) | ((s ^ u) >> 1); // sum bits with carry-in
    s = s & eights;         // isolate nibble lsbs with carry-in
    *cy = carry_out;        // record carry-out from most significant nibble
    u = (x + y) + (s - (s >> 2)); // add "nibble offset" to nibbles that need it
    return u;
#endif // USE_KNUTH
}
#endif // PORTABLE && !NATIVE_64BIT

#if PORTABLE
#if NATIVE_64BIT
/* majority-of-3 operation that Knuth refers to more generically as median */
uint64_t median64 (uint64_t x, uint64_t y, uint64_t z)
{
    return (x | (y & z)) & (y | z);
}

/* 16-digit BCD addition */
uint64_t BCD64_ADD (uint64_t x, uint64_t y)
{
    // derived from Knuth, TAOCP 4a (2011), answer to exercise 100 section 7.1.3
    uint64_t z, u, t;
    z = y + 0x6666666666666666ULL;
    u = x + z;
    t = median64 (x, z, ~u) & 0x8888888888888888ULL;
    return (x + y) + (t - (t >> 2));
}
#else // !NATIVE_64BIT
/* 16-digit BCD addition */
uint64_t BCD64_ADD (uint64_t x, uint64_t y)
{
    uint32_t x_lo = (uint32_t)x;
    uint32_t x_hi = (uint32_t)(x >> 32);
    uint32_t y_lo = (uint32_t)y;
    uint32_t y_hi = (uint32_t)(y >> 32);
    uint32_t cy;
    uint32_t r_lo = BCD32_ADDcc (x_lo, y_lo, &cy);
    uint32_t r_hi = BCD32_ADDC  (x_hi, y_hi,  cy);
    return ((uint64_t)r_hi << 32) | r_lo;
}
#endif // NATIVE_64BIT
#else // !PORTABLE
/*
  16-digit BCD addition; N. Juffa, about 1999. Code retrieved on 3/24/2024 from 
  https://web.archive.org/web/20001211131100/http://www.azillionmonkeys.com/qed/asmexample.html
  Bug fixed and adjusted for Intel compiler's inline asm (may need -fasm-blocks)
*/
uint64_t BCD64_ADD (uint64_t x, uint64_t y)
{
    uint64_t r;

    __asm mov   eax, dword ptr [x]   ; // x (lo)
    __asm mov   ebx, dword ptr [y]   ; // y (lo)
    __asm mov   edx, dword ptr [x+4] ; // x (hi)
    __asm mov   ecx, dword ptr [y+4] ; // y (hi)
    // x in EDX:EAX, y in EDX:EBX. Add least significant 8 BCD digits
    __asm mov   esi, eax             ; // x
    __asm lea   edi, [eax+66666666h] ; // x + 0x66666666
    __asm xor   esi, ebx             ; // x ^ y
    __asm add   eax, ebx             ; // x + y
    __asm shr   esi, 1               ; // t1 = (x ^ y) >> 1
    __asm add   edi, ebx             ; // x + y + 0x66666666
    __asm sbb   ebx, ebx             ; // capture carry: carry ? (-1) : 0
    __asm rcr   edi, 1               ; // t2 = (x + y + 0x66666666) >> 1
    __asm xor   esi, edi             ; // t1 ^ t2
    __asm and   esi, 88888888h       ; // t3 = (t1 ^ t2) & 0x88888888
    __asm add   eax, esi             ; // x + y + t3
    __asm shr   esi, 2               ; // t3 >> 2
    __asm sub   eax, esi             ; // x + y + t3 - (t3 >> 2)
    // Add most significant 8 BCD digits
    __asm sub   ecx, ebx             ; // y - carry // propagate carry
    __asm mov   esi, edx             ; // x
    __asm lea   edi, [edx+66666666h] ; // x + 0x66666666
    __asm xor   esi, ecx             ; // x ^ y
    __asm add   edx, ecx             ; // x + y
    __asm shr   esi, 1               ; // t1 = (x ^ y) >> 1
    __asm add   edi, ecx             ; // x + y + 0x66666666
    // __asm sbb   ebx, ebx             ; capture carry: carry ? (-1) : 0
    __asm rcr   edi, 1               ; // t2 = (x + y + 0x66666666) >> 1
    __asm xor   esi, edi             ; // t1 ^ t2
    __asm and   esi, 88888888h       ; // t3 = (t1 ^ t2) & 0x88888888
    __asm add   edx, esi             ; // x + y + t3
    __asm shr   esi, 2               ; // t3 >> 2
    __asm sub   edx, esi             ; // x + y + t3 - (t3 >> 2)
    // Now EDX:EAX = x + y
    __asm mov   dword ptr [r], eax   ; // save result (lo)
    __asm mov   dword ptr [r+4], edx ; // save result (hi)

    return r;
}
#endif // PORTABLE

/* convert binary operand x in [0, 9999] to BCD */
uint32_t cvt_bin2bcd_4dig (uint32_t x)
{
    uint32_t num, res = 0;
    /* convert to 21:11 fixed point */
    num = ((0x8312 * x) + 0x6000) >> 14; // (2**11 / 10**3) * 2**14
    /* pull out decimal digits one at a time */
    res =              (num >> 11);
    num = (num & ((1 << 11) - 1)) * 5;   // 22:10 fixed point
    res = (res << 4) | (num >> 10);
    num = (num & ((1 << 10) - 1)) * 5;   // 23:9 fixed point
    res = (res << 4) | (num >>  9);
    num = (num & ((1 <<  9) - 1)) * 5;   // 24:8 fixed point
    res = (res << 4) | (num >>  8);
    return res;
}

/* convert binary operand x in [0, 99999999] to BCD */
uint32_t cvt_bin2bcd_8dig (uint32_t x)
{
    uint32_t hi = x / 10000;
    uint32_t lo = x - hi * 10000;
    return (cvt_bin2bcd_4dig (hi) << 16) | cvt_bin2bcd_4dig (lo);
}

/* convert binary operand a in [0, 9999999999999999] to BCD */
uint64_t uint64_to_bcd64 (uint64_t x)
{
    uint32_t hi = (uint32_t)(x / 100000000ULL);
    uint32_t lo = (uint32_t)(x - hi * 100000000ULL);
    return (((uint64_t)cvt_bin2bcd_8dig (hi)) << 32) | cvt_bin2bcd_8dig (lo);   
}

/* return most significant 32 bits of the full product of two 32-bit operands */
uint32_t umul32hi (uint32_t a, uint32_t b)
{
    uint64_t prod = (uint64_t)a * b;
    return (uint32_t)(prod >> 32);
}

/* convert BCD operand x in [0, 99999999] to binary
   based on Knuth TAOCP 2, answer on p. 638 to exercise 19 in section 4.4
*/
uint32_t cvt_bcd2bin_8dig (uint32_t x)
{
    const uint32_t m1 = 0xf0f0f0f0;
    const uint32_t m2 = 0xff00ff00;
    const uint32_t m3 = 0xffff0000;
    const uint32_t c1 = 0x60000000; // 0.375000000000
    const uint32_t c2 = 0x9c000000; // 0.609375000000
    const uint32_t c3 = 0xd8f00000; // 0.847412109375
    x = x - umul32hi (c1, x & m1);
    x = x - umul32hi (c2, x & m2);
    x = x - umul32hi (c3, x & m3);
    return x;
}

/* convert BCD operand x in [0, 9999999999999999] to binary */
uint64_t bcd64_to_uint64 (uint64_t a)
{
    uint32_t hi = (uint32_t)(a >> 32);
    uint32_t lo = (uint32_t)a;
    return 100000000ULL * cvt_bcd2bin_8dig (hi) + cvt_bcd2bin_8dig (lo);
}

/*
  https://groups.google.com/forum/#!original/comp.lang.c/qFv18ql_WlU/IK8KGZZFJx4J
  From: geo <[email protected]>
  Newsgroups: sci.math,comp.lang.c,comp.lang.fortran
  Subject: 64-bit KISS RNGs
  Date: Sat, 28 Feb 2009 04:30:48 -0800 (PST)

  This 64-bit KISS RNG has three components, each nearly
  good enough to serve alone.    The components are:
  Multiply-With-Carry (MWC), period (2^121+2^63-1)
  Xorshift (XSH), period 2^64-1
  Congruential (CNG), period 2^64
*/
static uint64_t kiss64_x = 1234567890987654321ULL;
static uint64_t kiss64_c = 123456123456123456ULL;
static uint64_t kiss64_y = 362436362436362436ULL;
static uint64_t kiss64_z = 1066149217761810ULL;
static uint64_t kiss64_t;
#define MWC64  (kiss64_t = (kiss64_x << 58) + kiss64_c, \
                kiss64_c = (kiss64_x >> 6), kiss64_x += kiss64_t, \
                kiss64_c += (kiss64_x < kiss64_t), kiss64_x)
#define XSH64  (kiss64_y ^= (kiss64_y << 13), kiss64_y ^= (kiss64_y >> 17), \
                kiss64_y ^= (kiss64_y << 43))
#define CNG64  (kiss64_z = 6906969069ULL * kiss64_z + 1234567ULL)
#define KISS64 (MWC64 + XSH64 + CNG64)

#if TEST_PORTABLE_BCD64
int main (void)
{
    uint64_t bx, by, x, y, ref, res, i;

    printf ("Testing 16-digit BCD arithmetic\n");
    for (i = 0; i < NUM_TESTS; i++) {
        x = KISS64 % 10000000000000000ULL;
        y = KISS64 % 10000000000000000ULL;
        bx = uint64_to_bcd64 (x);
        by = uint64_to_bcd64 (y);
        res = BCD64_ADD (bx, by);
        ref = uint64_to_bcd64 ((x + y) % 10000000000000000ULL);
        if (res != ref) {
            printf ("!!!! x=%016llx  y=%016llx  bx=%016llx  by=%016llx  res=%016llx  ref=%016llx\n", 
                    x, y, bx, by, res, ref);
            return EXIT_FAILURE;
        }
    }
    printf ("test PASSED\n");
    return EXIT_SUCCESS;
}

#else // TEST_PORTABLE_BCD64

typedef struct ten_byte {
    uint64_t low64;
    uint16_t high16;
} ten_byte;

/* 18-digit BCD addition modulo 10**18 using the x87 FPU */
ten_byte bcd18dig_add_ref (ten_byte a, ten_byte b)
{
    const double exp10_18 = 1e18;
    const uint16_t cw_extended_chop = 0xf7f;
    ten_byte aa, bb, r;
    uint16_t cw_original;

    aa = a; // workaround: bad code generated when data not local to function
    bb = b;
    __asm fstcw  [cw_original]        ;// save current CW
    __asm fldcw  [cw_extended_chop]   ;// RC=truncate PC=extended
    __asm fld    qword ptr [exp10_18] ;// 10**18
    __asm fbld   tbyte ptr [aa]       ;// a, 10**8
    __asm fbld   tbyte ptr [bb]       ;// b, a, 10**18
    __asm fadd   st, st(1)            ;// a+b, a, 10**18
    __asm fst    st(1)                ;// a+b, a+b, 10**18
    __asm fdiv   st, st(2)            ;// (a+b)/10**18, a+b, 10**18
    __asm frndint                     ;// trunc((a+b)/10**18), a+b, 10**18
    __asm fmulp  st(2), st            ;// a+b, trunc((a+b)/10**18)*10**18
    __asm fsubrp st(1),st             ;// a+b - trunc((a+b)/10**18)*10**18;
    __asm fbstp  tbyte ptr [r]        ;// <empty>
    __asm fldcw  [cw_original]        ;// restore original CW

    return r;
}

/* 18-digit BCD addition modulo 10**18, processed in 8-digit chunks */
ten_byte bcd18dig_add (ten_byte x, ten_byte y)
{
    ten_byte r;

    uint32_t x_lo = (uint32_t)(x.low64);
    uint32_t x_md = (uint32_t)(x.low64 >> 32);
    uint32_t x_hi = (uint32_t)(x.high16);
    uint32_t y_lo = (uint32_t)(y.low64);
    uint32_t y_md = (uint32_t)(y.low64 >> 32);
    uint32_t y_hi = (uint32_t)(y.high16);
    uint32_t cy;
    uint32_t r_lo = BCD32_ADDcc (x_lo, y_lo, &cy);
    uint32_t r_md = BCD32_ADDCcc(x_md, y_md, &cy);
    uint32_t r_hi = BCD32_ADDC  (x_hi, y_hi,  cy);
    r_hi &= 0xff;
    r.low64 = ((uint64_t)r_md << 32) | r_lo;
    r.high16 = (uint16_t)r_hi;

    return r;
}

int main (void)
{
    uint64_t x, y;
    ten_byte bcd_augend, bcd_addend, bcd_sum_ref, bcd_sum_res;
    
    printf ("Testing 18-digit BCD arithmetic against x87 FPU\n");
    for (int i = 0; i < NUM_TESTS; i++) {
        x = KISS64 % 10000000000000000ULL;
        y = KISS64 % 10000000000000000ULL;
        bcd_augend.low64 = uint64_to_bcd64 (x);
        bcd_addend.low64 = uint64_to_bcd64 (x);
        x = KISS64 % 100ULL;
        y = KISS64 % 100ULL;
        bcd_augend.high16 = uint64_to_bcd64 (x);
        bcd_addend.high16 = uint64_to_bcd64 (y);
        
        bcd_sum_ref = bcd18dig_add_ref (bcd_augend, bcd_addend);
        bcd_sum_res = bcd18dig_add     (bcd_augend, bcd_addend);

        if (memcmp (&bcd_sum_res, &bcd_sum_ref, 9) != 0) {
            printf ("a   = %02x%016llx\n", bcd_augend.high16, bcd_augend.low64);
            printf ("b   = %02x%016llx\n", bcd_addend.high16, bcd_addend.low64);
            printf ("res = %02x%016llx\n",bcd_sum_res.high16,bcd_sum_res.low64);
            printf ("ref = %02x%016llx\n",bcd_sum_ref.high16,bcd_sum_ref.low64);
            return EXIT_FAILURE;
        }
    }
    printf ("test PASSED\n");
    return EXIT_SUCCESS;
}
#endif // TEST_PORTABLE_BCD64

Мне нужны ответы, связанные только с производительностью.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
1
522
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

ИМХО, в формулах, не зависящих от архитектуры, мало что можно улучшить.
Полный сумматор дает метод «MSB»: (a | b) ^ ((a ^ b) & (a + b)).
Полусумматор дает метод «LSB»: ((a & b) + ((a ^ b) >> 1)) ^ ((a ^ b) >> 1)

Метод «MUX» также может быть конкурентоспособным (особенно если andnot — это одна операция). Ограниченный диапазон ввода bcd используется для выполнения двух «дешевых» проверок переполнения вместо одной универсальной проверки переполнения.


Рассмотрим полный сумматор, формула для каждого бита:

// where c == carry_in to each bit;
sum = a ^ b ^ c;
carry_out = (b & c) | (a & c) | (a & b);

carry_out (то есть побитовое большинство из трёх) можно записать и другими способами:

maj3 = (a | b) & ((a & b) | c);
maj3 = (a | b) &~((a ^ b) &~c);
maj3 = (a | b) ^ ((a ^ b) &~c); 
maj3 = (a & b) ^ ((a ^ b) & c);
maj3 = (a & b) | ((a ^ b) & c);
maj3 = (a & b) | ((a | b) & c);
maj3 = ((b ^ a) & (c ^ a)) ^ a;
// etc.

Решите для carry_in:

sum == a + b;
a + b == a ^ b ^ c;
(a + b) ^ a ^ b == c;

Замените и упростите:

carry_out = (a | b) ^ ((a ^ b) &~c);
carry_out = (a | b) ^ ((a ^ b) &~((a + b) ^ a ^ b)); 
carry_out = (a | b) ^ ((a ^ b) & (a + b));

С произвольной точностью carry_out == carry_in >> 1.
При модульной арифметике carry_out из позиции самого старшего бита теряется.

Существует трюк на основе полусумматора для вычисления (a + b) / 2 без беззнакового переполнения.

a + b также можно записать как:

a + b == (a & b) + (a | b);
a + b == (a & b) + (a & b) + (a ^ b);
// a + b == (a | b) + (a | b) - (a ^ b);

так:

a ^ b ^ c == a + b;
a ^ b ^ c == ((a & b) << 1) + (a ^ b);
(a ^ b ^ c) >> 1 == (a & b) + ((a ^ b) >> 1);
c >> 1 == ((a & b) + ((a ^ b) >> 1)) ^ ((a ^ b) >> 1);

Что дает следующие формулы для упакованного сложения BCD:

// a version based on a full-adder
uint32_t bcd_add_msb(uint32_t x, uint32_t y, uint32_t *c) {
    uint32_t a, b, carries, fixup;
    a = x + 0x66666666;
    b = y + *c; // carry_in is optional until the sum
    carries = (a | y) ^ ((a ^ y) & (a + b));
    *c = carries >> 31;
    fixup = carries & 0x88888888;
    return (x + b) + (fixup - (fixup >> 2));
}

// a version based on a half-adder
uint32_t bcd_add_lsb(uint32_t x, uint32_t y, uint32_t *c) {
    uint32_t clsum, avg, fixup;
    y += *c;
    clsum = (x ^ y) >> 1;
    avg = (x & y) + clsum + 0x33333333;
    *c = avg >> 31;
    fixup = (clsum ^ avg) & 0x88888888;
    return x + y + (fixup - (fixup >> 2));
}

// a version with two (range limited) overflow checks
// bitwise_select => (x + y) ? (~((x + y) + 0x66666666)) : (x | y);
uint32_t bcd_add_mux(uint32_t x, uint32_t y, uint32_t *c) {
    uint32_t sum, mux, fixup;
    sum = x + y + *c;
    mux = (sum &~ (sum + 0x66666666)) | ((x | y) &~ sum);
    *c = mux >> 31;
    fixup = mux & 0x88888888;
    return sum + (fixup - (fixup >> 2));
}

// Just for fun:
// convert binary operand x in [0, 9999] to packed BCD
uint64_t cvt_bin2bcd_4dig (uint64_t x)
{
    const uint64_t mask = 0x0FFFFE1FFFF87FFF;
    uint64_t y;
    x *= 0x000418A051EC0CCD;
    y = (x & mask) * 10;
    x &= ~mask;
    y &= ~mask;
    return (uint16_t)((y >> 15) + (y >> 33) + (y >> 52) + (x >> 48));
}

/***  explanation for cvt_bin2bcd_4dig() 
the usual divide by constant, fixed-point stuff:
max 9999, div 10, (x * 0xCCD) >> 15 // integer part: 10
max 9999, div 100, (x * 0x147B) >> 19 // integer part: 7
max 9999, div 1000, (x * 0x20C5) >> 23 // integer part: 4

However, since we're discarding the integer part...
we can overlap the integer part of a 'lane' with the fractional part of the next lane.
We have plenty of fractional precision anyways... (e.g. 26 bits required
to calc but only 14 bits needed to round-trip)

so our magic is: (0x20C5ULL << 37) + (0x147BULL << 18) + 0xCCD;
the mask just cuts out the low 4-bits of the integer part for the result:
    mask = ~((0xFULL << 60) + (0xFULL << 37) + (0xF << 15));

***/

Ух ты. Как, черт возьми, cvt_bin2bcd_4dig() работает? У меня есть 64-битная функция преобразования двоично-десятичного кода в двоично-десятичный код, которая работает с 16 десятичными цифрами и основана на переворачивании упражнения 19 Кнута № 4.4, но это потрясающе.

user207421 04.04.2024 02:34

@aqrit Хорошая вещь! Вариант bcd_add_mux() кажется наиболее эффективным при рассмотрении различных архитектур и особенно хорошо работает на простых скалярных архитектурах, таких как armv7-m (gcc создает для него очень сжатый код, который не выглядит выигрышным при ручном кодировании). bcd_add_msb() получает почетное упоминание в суперскалярных архитектурах (в том числе в упорядоченных) из-за открытой ILP. Мне еще предстоит изучить реализацию вашего бонуса cvt_bin2bcd_4dig().

njuffa 10.05.2024 19:53

@aqrit Я внимательно рассмотрел реализацию cvt_bin2bcd_4dig(), представленную в качестве забавного бонуса. Хотя это, безусловно, представляет собой интересную задачу (это было бы намного труднее понять, если бы не предоставленное объяснение), похоже, что она не имеет практической пользы, насколько я могу судить? В лучшем случае его производительность равна производительности моей пешеходной реализации, но на большинстве платформ она на самом деле медленнее (примерно в 1,5 раза). Это соответствует вашим наблюдениям?

njuffa 10.05.2024 21:53

@njuffa Я бы сказал, что нет смысла его использовать. Для получения прибыли на 64-битной платформе всегда можно просто сделать две полосы вашей реализации и отделить 8 цифр.

aqrit 10.05.2024 23:42

Другие вопросы по теме