【问题标题】:SSE2 instructions not working in inline assembly with C++SSE2 指令在 C++ 内联汇编中不起作用
【发布时间】:2010-11-16 01:38:10
【问题描述】:

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

【问题讨论】:

  • 必须保护内联汇编免受本机 C++ 代码的影响。您的每个块 asm volatile () 都受到保护。不能保证加载在一个块中的寄存器仍将包含下一个块中的值。此外,(load add store) 的执行时间比简单地在本机 c++ 代码中添加值需要更长的时间。在许多情况下,在新硬件上,指令将使用与 SSE 指令相同的标量浮点数运算管道同时执行。使用内联汇编确实没有意义。在需要时使用内部函数。

标签: c++ gcc inline-assembly sse2


【解决方案1】:

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

Intrinsics 允许您指定操作码的精确序列,但将寄存器着色留给编译器。这比尝试在 C 变量和 asm 寄存器之间移动数据要可靠得多,这是内联汇编程序对我来说一直失败的地方。它还允许编译器安排你的指令,如果它在pipeline hazards 附近工作,它可以提供更好的性能。即,在这种情况下你可以这样做

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

我已经输入了a fixed example on codepad(以避免混淆这个答案,并证明它有效)。

【讨论】:

  • 你在这两点上都是正确的。你也可以写 "movups (%0),%%xmm0"::"r"(lhs),达到同样的效果。并且内联程序集应该将 xmm0 和 xmm1 添加到破坏列表中,因为 gcc 可能偶尔会出于内部目的使用 xmm 寄存器,并且不会期望它们的值被更改。
  • GCC 似乎不允许在破坏列表中使用 SSE 寄存器。
【解决方案2】:

我在这里看到了一些错误。首先,您加载 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;
    }
}

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

【讨论】:

  • 抱歉...我之前的答案版本存在一些缺陷,并且示例与之前的答案是多余的。
猜你喜欢
  • 2016-08-25
  • 1970-01-01
  • 1970-01-01
  • 2023-03-10
  • 1970-01-01
  • 2016-09-06
  • 2011-04-02
  • 2012-08-03
  • 1970-01-01
相关资源
最近更新 更多