Инструкции SSE2 не работают во встроенной сборке с C ++ - PullRequest
1 голос
/ 16 ноября 2010

У меня есть эта функция, которая использует SSE2 для сложения нескольких значений, она должна складывать lhs и rhs вместе и сохранять результат обратно в lhs:

template<typename T>
void simdAdd(T *lhs,T *rhs)
{
    asm volatile("movups %0,%%xmm0"::"m"(lhs));
    asm volatile("movups %0,%%xmm1"::"m"(rhs));

    switch(sizeof(T))
    {
        case sizeof(uint8_t):
        asm volatile("paddb %%xmm0,%%xmm1":);
        break;

        case sizeof(uint16_t):
        asm volatile("paddw %%xmm0,%%xmm1":);
        break;

        case sizeof(float):
        asm volatile("addps %%xmm0,%%xmm1":);
        break;

        case sizeof(double):
        asm volatile("addpd %%xmm0,%%xmm1":);
        break;

        default:
        std::cout<<"error"<<std::endl;
        break;
    }

    asm volatile("movups %%xmm0,%0":"=m"(lhs));
}

и мой код использует такую ​​функцию:

float *values=new float[4];
float *values2=new float[4];

values[0]=1.0f;
values[1]=2.0f;
values[2]=3.0f;
values[3]=4.0f;

values2[0]=1.0f;
values2[1]=2.0f;
values2[2]=3.0f;
values2[3]=4.0f;

simdAdd(values,values2);
for(uint32_t count=0;count<4;count++) std::cout<<values[count]<<std::endl;

Однако это не работает, потому что когда код выполняется, он выводит 1,2,3,4 вместо 2,4,6,8

Ответы [ 2 ]

5 голосов
/ 16 ноября 2010

Я обнаружил, что поддержка встроенной сборки не является надежной в большинстве современных компиляторов (как, например, реализации просто глючат).Обычно лучше использовать встроенные функции компилятора , которые являются объявлениями, которые выглядят как функции C, но на самом деле компилируются с определенным кодом операции.

Встроенные функции позволяют вам указать точную последовательность кодов операций, но оставить раскраску регистров для компилятора.Это гораздо надежнее, чем пытаться перемещать данные между C-переменными и ASM-регистрами, и именно поэтому встроенные ассемблеры всегда были для меня бесполезны.Он также позволяет компилятору составлять расписание ваших инструкций, что может обеспечить лучшую производительность, если он работает вокруг опасностей конвейера .Т.е. в этом случае вы могли бы сделать

void simdAdd(float *lhs,float *rhs)
{
   _mm_storeu_ps( lhs, _mm_add_ps(_mm_loadu_ps( lhs ), _mm_loadu_ps( rhs )) );
}

В любом случае у вас есть две проблемы:

  1. Ужасный синтаксис встроенной сборки GCC, который приводит к путаницеРазница между указателями и значениями.Используйте *lhs и *rhs вместо просто lhs и rhs;очевидно, синтаксис «= m» означает «неявно использовать указатель на эту вещь, которую я передаю вам, а не на саму вещь».
  2. GCC имеет синтаксис source, destination - addps сохраняет свой результат ввторой параметр, поэтому вам нужно вывести xmm1, а не xmm0.

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

0 голосов
/ 20 ноября 2014

Пара вещей, которые я здесь вижу неправильно.Во-первых, ваши операторы, которые загружают регистры XMM и сохраняют значения обратно в вашу переменную, неверны.

asm volatile("movups %0,%%xmm0"::"m"(lhs));
asm volatile("movups %0,%%xmm1"::"m"(rhs));
...
asm volatile("movups %%xmm0,%0":"=m"(lhs));

Следует читать

asm volatile("movups %0,%%xmm0"::"m"(*lhs));
asm volatile("movups %0,%%xmm1"::"m"(*rhs));
...
asm volatile("movups %%xmm0,%0":"=m"(*lhs));

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

Даже сэти исправления, в общем, это не очень хорошая техника.Я написал свой собственный пример с операторами asm, но он был ошибочным, потому что я забыл учесть невыровненную природу передаваемых параметров. Это становится очень громоздким, если делать с операторами asm, и намного проще и удобочитаемее с помощью встроенных функций.Просто будьте осторожны, чтобы использовать правильные типы данных:

template<typename T>
void simdAdd(T *lhs,T *rhs)
{
    switch(sizeof(T))
    {
        case sizeof(uint8_t):
        {
          __m128i lh128;
          lh128 = _mm_add_epi8( _mm_loadu_si128( (__m128i *)lhs ),
                                _mm_loadu_si128( (__m128i *)rhs ) );
          _mm_storeu_si128( (__m128i *)lhs, lh128 );
        }
        break;

        case sizeof(uint16_t):
        {
          __m128i lh128;
          lh128 = _mm_add_epi16( _mm_loadu_si128( (__m128i *)lhs ),
                                 _mm_loadu_si128( (__m128i *)rhs ) );
          _mm_storeu_si128( (__m128i *)lhs, lh128 );
        }
        break;

        case sizeof(float):
        {
          __m128 lh128;
          lh128 = _mm_add_ps( _mm_loadu_ps( (float *)lhs ),
                              _mm_loadu_ps( (float *)rhs ) );
          _mm_storeu_ps( (float *)lhs, lh128 );
        }
        break;

        case sizeof(double):
        {
          __m128d lh128;
          lh128 = _mm_add_pd( _mm_loadu_pd( (double *)lhs ),
                              _mm_loadu_pd( (double *)rhs ) );
          _mm_storeu_pd( (double *)lhs, lh128 );
        }
        break;

        default:
        std::cout<<"error"<<std::endl;
        break;
    }
}

Что-то, о чем нужно знать, это то, что размер ваших типов данных недостаточен, чтобы знать, какой тип данных вы передали.Тот факт, что тип шаблона имеет тот же размер, что и проверяемые базовые типы, не означает, что это тот же тип.Поэтому я заставляю кастинг покрыть это дело в моем примере.Как правило, это может быть небезопасной практикой, если вы не уверены, что эта функция будет когда-либо использоваться только с указанными вами типами.Например, использование целого числа с плавающей точкой приведет к неожиданно неправильному ответу, и компилятор не сможет предупредить вас об этом.

...