Эффективное 128-битное сложение с использованием флага переноса - PullRequest
36 голосов
/ 12 июля 2011

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

Я представлял целое число в виде двух 64-битных длинных без знака. Теперь мне нужно увеличить эти значения на 128-битную константу. Это не сложно, но вы должны вручную перехватить переход от младшего слова к старшему.

У меня есть рабочий код примерно так:

inline void increment128(unsigned long &hiWord, unsigned long &loWord)
  {
    const unsigned long hiAdd=0x0000062DE49B5241;
    const unsigned long loAdd=0x85DC198BCDD714BA;

    loWord += loAdd;
    if (loWord < loAdd) ++hiWord; // test_and_add_carry
    hiWord += hiAdd;
  }

Это жесткий и простой код. Это работает.

К сожалению, это около 20% моего времени выполнения. Убийственная черта - это тест лоуорд. Если я удаляю его, я, очевидно, получаю неправильные ответы, но накладные расходы времени выполнения снижаются с 20% до 4%! Так что тест на переноску стоит особенно дорого!

Мой вопрос: предоставляет ли C ++ аппаратные флаги, даже в качестве расширения GCC? Похоже, что добавления могут быть выполнены без строки test-and-add-carry выше, если фактические скомпилированные инструкции использовали сложение с использованием последней инструкции переноса для добавления hiWord. Есть ли способ переписать строку test-and-add-carry, чтобы компилятор использовал встроенный код операции?

Ответы [ 2 ]

39 голосов
/ 12 июля 2011

На самом деле gcc будет использовать перенос автоматически, если вы тщательно напишите свой код ...

Я скомпилировал этот код с gcc -O2 -Wall -Werror -S:

void increment128_1(unsigned long &hiWord, unsigned long &loWord)
{
    const unsigned long hiAdd=0x0000062DE49B5241;
    const unsigned long loAdd=0x85DC198BCDD714BA;

    loWord += loAdd;
    if (loWord < loAdd) ++hiWord; // test_and_add_carry                                                                                                             
    hiWord += hiAdd;
}

void increment128_2(unsigned long &hiWord, unsigned long &loWord)
{
    const unsigned long hiAdd=0x0000062DE49B5241;
    const unsigned long loAdd=0x85DC198BCDD714BA;

    loWord += loAdd;
    hiWord += hiAdd;
    hiWord += (loWord < loAdd); // test_and_add_carry                                                                                                               
}

Это сборка для increment128_1:

.cfi_startproc
        movabsq     $-8801131483544218438, %rax
        addq        (%rsi), %rax
        movabsq     $-8801131483544218439, %rdx
        cmpq        %rdx, %rax
        movq        %rax, (%rsi)
        ja  .L5
        movq        (%rdi), %rax
        addq        $1, %rax
.L3:
        movabsq     $6794178679361, %rdx
        addq        %rdx, %rax
        movq        %rax, (%rdi)
        ret

... а это сборка для increment128_2:

        movabsq     $-8801131483544218438, %rax
        addq        %rax, (%rsi)
        movabsq     $6794178679361, %rax
        addq        (%rdi), %rax
        movabsq     $-8801131483544218439, %rdx
        movq        %rax, (%rdi)
        cmpq        %rdx, (%rsi)
        setbe       %dl
        movzbl      %dl, %edx
        leaq        (%rdx,%rax), %rax
        movq        %rax, (%rdi)
        ret

Обратите внимание на отсутствие условных веток во второй версии.

[править]

Кроме того, ссылки часто ухудшают производительность, потому что GCC приходится беспокоиться о псевдонимах ... Часто лучше просто передавать вещи по значению. Рассмотрим:

struct my_uint128_t {
    unsigned long hi;
    unsigned long lo;
};

my_uint128_t increment128_3(my_uint128_t x)
{
    const unsigned long hiAdd=0x0000062DE49B5241;
    const unsigned long loAdd=0x85DC198BCDD714BA;

    x.lo += loAdd;
    x.hi += hiAdd + (x.lo < loAdd);
    return x;
}

Монтаж:

        .cfi_startproc
        movabsq     $-8801131483544218438, %rdx
        movabsq     $-8801131483544218439, %rax
        movabsq     $6794178679362, %rcx
        addq        %rsi, %rdx
        cmpq        %rdx, %rax
        sbbq        %rax, %rax
        addq        %rcx, %rax
        addq        %rdi, %rax
        ret

Это на самом деле самый трудный код из трех.

... ОК, так что на самом деле никто из них не использовал перенос автоматически :-). Но они избегают условного перехода, который, как я держал пари, является медленным (поскольку логика прогнозирования переходов ошибается в половине случаев).

[править 2]

И еще один, на который я наткнулся, выполняя небольшой поиск. Знаете ли вы, что GCC имеет встроенную поддержку 128-битных целых чисел?

typedef unsigned long my_uint128_t __attribute__ ((mode(TI)));

my_uint128_t increment128_4(my_uint128_t x)
{
    const my_uint128_t hiAdd=0x0000062DE49B5241;
    const unsigned long loAdd=0x85DC198BCDD714BA;

    return x + (hiAdd << 64) + loAdd;
}

Сборка для этого примерно такая же хорошая, как она получается:

        .cfi_startproc
        movabsq     $-8801131483544218438, %rax
        movabsq     $6794178679361, %rdx
        pushq       %rbx
        .cfi_def_cfa_offset 16
        addq        %rdi, %rax
        adcq        %rsi, %rdx
        popq        %rbx
        .cfi_offset 3, -16
        .cfi_def_cfa_offset 8
        ret

(Не уверен, откуда взялся пуш / поп ebx, но это все еще неплохо.)

Кстати, все это с GCC 4.5.2.

17 голосов
/ 13 июля 2011

Лучший ответ, конечно, заключается в использовании встроенной поддержки __int128_t.

В качестве альтернативы используйте встроенный ассемблер. Я предпочитаю использовать форму именованного аргумента:

__asm("add %[src_lo], %[dst_lo]\n"
      "adc %[src_hi], %[dst_hi]"
      : [dst_lo] "+&r" (loWord), [dst_hi] "+r" (hiWord)
      : [src_lo] "erm" (loAdd), [src_hi] "erm" (hiAdd)
      : );

loWord помечен как ранний операнд , потому что он записывается до того, как некоторые другие операнды прочитаны. Это позволяет избежать неправильного кода для hiAdd = loWord, потому что он не позволит gcc использовать один и тот же регистр для хранения обоих. Он не позволяет компилятору использовать тот же регистр для случая loAdd = loWord, хотя это безопасно.

Как указывает этот вопрос раннего клоббера, встроенный asm действительно легко ошибиться (сложными для отладки способами, которые вызывают проблемы только после некоторого изменения кода, в который он встроен).

x86 и x86-64 inline asm, как предполагается, затормаживают флаги, поэтому явный "cc" клоббер не нужен.

...