Размножать бит переноса между двумя встроенными блоками ASM GCC

Дорогой Assembly/С++ dev,

Вопрос: распространяет ли перенос (или любой флаг) между двумя блоками ASM реалистично или совершенно безумно, даже если он работает?

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

Я должен признаться, что создал свой ASM (строковый блок) с помощью BOOST_PP, это не очень славно (любопытно посмотреть на него vli). Библиотека работала хорошо.

Однако я отмечаю, что в это время невозможно было передать флаг переноса регистра состояния между двумя встроенными блоками ASM. Это логично, потому что для любой мнемоники, сгенерированной компилятором между двумя блоками, регистр reset (кроме инструкции mov (из моего знания сборки)).

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

#include <iostream>
#include <array>
#include <cassert>
#include <algorithm>

//forward declaration
template<std::size_t NumBits>
struct integer;


//helper using object function, partial specialization  is forbiden on functions
template <std::size_t NumBits, std::size_t W, bool K = W == integer<NumBits>::numwords>
struct helper {
    static inline void add(integer<NumBits> &a, const integer<NumBits> &b){
        helper<NumBits, integer<NumBits>::numwords>::add(a,b);
    }
};

// first addition (call first)
template<std::size_t NumBits, std::size_t W>
struct helper<NumBits, W, 1> {
    static inline void add(integer<NumBits> &a, const integer<NumBits> &b){
        __asm__ (
                              "movq %1, %%rax \n"
                              "addq %%rax, %0 \n"
                              : "+m"(a[0]) // output
                              : "m" (b[0]) // input only
                              : "rax", "cc", "memory");
        helper<NumBits,W-1>::add(a,b);
    }
};

//second and more propagate the carry (call next)
template<std::size_t NumBits, std::size_t W>
struct helper<NumBits, W, 0> {
    static inline void add(integer<NumBits> &a, const integer<NumBits> &b){
        __asm__ (
                              "movq %1, %%rax \n"
                              "adcq %%rax, %0 \n"
                              : "+m"(a[integer<NumBits>::numwords-W])
                              : "m" (b[integer<NumBits>::numwords-W])
                              : "rax", "cc", "memory");
        helper<NumBits,W-1>::add(a,b);
    }
};

//nothing end reccursive process (call last)
template<std::size_t NumBits>
struct helper<NumBits, 0, 0> {
    static inline void add(integer<NumBits> &a, const integer<NumBits> &b){};
};

// tiny integer class
template<std::size_t NumBits>
struct integer{
    typedef uint64_t      value_type;
    static const std::size_t numbits = NumBits;
    static const std::size_t numwords = (NumBits+std::numeric_limits<value_type>::digits-1)/std::numeric_limits<value_type>::digits;
    using container = std::array<uint64_t, numwords>;

    typedef typename container::iterator             iterator;

    iterator begin() { return data_.begin();}
    iterator end() { return data_.end();}

    explicit integer(value_type num = value_type()){
        assert( -1l >> 1 == -1l );
        std::fill(begin(),end(),value_type());
        data_[0] = num;
    }

    inline value_type& operator[](std::size_t n){ return data_[n];}
    inline const value_type& operator[](std::size_t n) const { return data_[n];}

    integer& operator+=(const integer& a){
        helper<numbits,numwords>::add(*this,a);
        return *this;
    }

    integer& operator~(){
        std::transform(begin(),end(),begin(),std::bit_not<value_type>());
        return *this;
    }

    void print_raw(std::ostream& os) const{
        os << "(" ;
        for(std::size_t i = numwords-1; i > 0; --i)
            os << data_[i]<<" ";
        os << data_[0];
        os << ")";
    }

    void print(std::ostream& os) const{
        assert(false && " TO DO ! \n");
    }

private:
    container data_;
};

template <std::size_t NumBits>
std::ostream& operator<< (std::ostream& os, integer<NumBits> const& i){
    if(os.flags() & std::ios_base::hex)
        i.print_raw(os);
    else
        i.print(os);
    return os;
}

int main(int argc, const char * argv[]) {
    integer<256> a; // 0
    integer<256> b(1);

    ~a; //all the 0 become 1

    std::cout << " a: " << std::hex << a << std::endl;
    std::cout << " ref: (ffffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffff) " <<  std::endl;

    a += b; // should propagate the carry

    std::cout << " a+=b: " << a << std::endl;
    std::cout << " ref: (0 0 0 0) " <<  std::endl; // it works but ...

    return 0;
}

Я получаю правильный результат (он должен быть скомпилирован в release -O2 или -O3!), и ASM прав (на моем Mac с clang++: Apple LLVM версии 9.0.0 (clang-900.0.39.2))

    movq    -96(%rbp), %rax
    addq    %rax, -64(%rbp)

    ## InlineAsm End
    ## InlineAsm Start
    movq    -88(%rbp), %rax
    adcq    %rax, -56(%rbp)

    ## InlineAsm End
    ## InlineAsm Start
    movq    -80(%rbp), %rax
    adcq    %rax, -48(%rbp)

    ## InlineAsm End
    ## InlineAsm Start
    movq    -72(%rbp), %rax
    adcq    %rax, -40(%rbp)

Я стараюсь, что он работает, потому что во время оптимизации компилятор удаляет всю бесполезную инструкцию между блоком ASM (в режиме отладки это не удалось).

Как вы думаете? Определенно небезопасно? Знает ли компилятор, насколько он будет стабильным?

В заключение: я просто делаю это для удовольствия:) Да, GMP - это решение для большой арифметики!

Ответ 1

Использование __volatile__ является злоупотреблением.

Цель __volatile__ - заставить компилятор испускать код сборки в письменном месте, а не полагаться на анализ потока данных, чтобы понять это. Если вы делаете обычные манипуляции с данными в пользовательском пространстве, обычно вы не должны использовать __volatile__, и если вам нужно __volatile__, чтобы заставить ваш код работать, это почти всегда означает, что ваши операнды указаны неправильно.

И да, операнды указаны неправильно. Посмотрите на первый блок.

__asm__ __volatile__ (
                      "movq %1, %%rax \n"
                      "addq %%rax, %0 \n"
                      : "=m"(a[0]) // output
                      : "m" (b[0]) // input only
                      : "rax", "memory");

Здесь есть две ошибки.

  • Ограничение на выходе "=m"(a[0]) неверно. Напомним, что пункт назначения для addq - это как вход, так и вывод, поэтому правильное ограничение равно +, поэтому используйте "+m"(a[0]). Если вы сообщите компилятору, что a[0] выводится только, компилятор может организовать a[0], чтобы содержать значение мусора (посредством устранения мертвого хранилища), которое не является тем, что вы хотите.

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

К сожалению, флаги доступны только в качестве выходных или clobber-операндов для блоков сборки и недоступны в качестве входов. Итак, после всей этой суеты над правильной настройкой операндов, чтобы вы не использовали __volatile__... оказалось, что нет никакого способа указать ваши операнды в любом случае!

Итак, рекомендация здесь заключается в том, что вы должны по крайней мере исправить операнды, которые вы можете исправить, и указать "cc" как clobber. Но есть несколько лучших решений, для которых не требуется __volatile__ вообще...

Решение № 1: используйте GMP.

Функции mpn_ для добавления не выделяют память. Функции mpz_ являются обертками вокруг функций mpn_ с некоторыми дополнительными выделениями логики и памяти.

Решение №2: Напишите все в одном сборочном блоке.

Если вы пишете весь цикл в одном блоке сборки, вам не нужно беспокоиться о сохранении флагов между блоками. Для этого вы можете использовать макросы сборки. Извините беспорядок, я не очень много программиста по сборке:

template <int N>
void add(unsigned long long *dest, unsigned long long *src) {
  __asm__(
      "movq (%1), %%rax"
      "\n\taddq %%rax, (%0)"
      "\n.local add_offset"
      "\n.set add_offset,0"
      "\n.rept %P2" // %P0 means %0 but without the $ in front
      "\n.set add_offset,add_offset+8"
      "\n\tmovq add_offset(%1), %%rax"
      "\n\tadcq %%rax, add_offset(%0)"
      "\n.endr"
      :
      : "r"(dest), "r"(src), "n"(N-1)
      : "cc", "memory", "rax");
}   

Что это значит, это оценить цикл, используя директиву сборки .rept. В итоге вы получите 1 экземпляр addq и N-1 копий adcq, хотя, если вы посмотрите на сборку GCC с помощью -S, вы увидите только один из них. Сам ассемблер будет создавать копии, разматывая цикл.

См. Gist: https://gist.github.com/depp/966fc1f4d535e31d9725cc71d97daf91