1

我有这个函数,它使用 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

4

2 回答 2

5

我发现在大多数现代编译器中,内联汇编支持并不可靠(例如,实现只是简单的错误)。通常最好使用编译器内在函数,它们是看起来像 C 函数的声明,但实际上编译为特定的操作码。

Intrinsics 允许您指定操作码的精确序列,但将寄存器着色留给编译器。这比尝试在 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 内联汇编语法使指针和值之间的差异非常混乱。使用*lhsand*rhs而不仅仅是 lhs 和 rhs; 显然,“=m”语法意味着“隐式使用指向我传递给你的这个东西的指针,而不是这个东西本身。”
  2. GCC 有一个 source,destination 语法—— addps 将其结果存储在第二个参数中,所以你需要输出xmm1,而不是xmm0

在键盘上放了一个固定的例子(以避免混淆这个答案,并证明它有效)。

于 2010-11-16T02:13:30.130 回答
0

我在这里看到错了几件事。首先,加载 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;
    }
}

需要注意的是您的数据类型的大小不足以知道您传递了哪种数据类型。仅仅因为模板类型与您正在检查的基本类型共享相同的大小,并不意味着它是相同的类型。所以我在我的例子中强制铸造覆盖这种情况。这通常可能是一种不安全的做法,除非您确定此函数只会与您指定的类型一起使用。例如,使用浮点大小的整数会导致意外的错误答案,并且编译器将无法警告您。

于 2014-11-19T22:02:38.837 回答