使用SSE指令

时间:2009-02-25 15:55:23

标签: c++ optimization assembly processor sse

我有一个用C ++编写的循环,它是为一个大整数数组的每个元素执行的。在循环内部,我屏蔽了整数的一些位,然后找到最小值和最大值。我听说如果我使用SSE指令进行这些操作,它将比使用按位AND和if-else条件写入的普通循环运行得快得多。我的问题是我应该参加这些SSE指令吗?此外,如果我的代码在不同的处理器上运行会发生什么?它仍然有用还是这些指令是特定于处理器的?

15 个答案:

答案 0 :(得分:24)

  1. SSE指令是特定于处理器的。您可以在维基百科上查找哪个处理器支持哪个SSE版本。
  2. 如果SSE代码更快或更快取决于许多因素:首先当然是问题是内存限制还是CPU限制。如果内存总线是瓶颈,SSE将无济于事。尝试简化整数计算,如果这使得代码更快,它可能受CPU限制,并且你很有可能加快它。
  3. 请注意,编写SIMD代码要比编写C ++代码困难得多,并且生成的代码更难以更改。始终保持C ++代码是最新的,您需要将其作为注释并检查汇编代码的正确性。
  4. 考虑使用像IPP这样的库,它实现了针对各种处理器优化的常见低级SIMD操作。

答案 1 :(得分:15)

SIMD(以SSE为例)允许您对多个数据块执行相同的操作。因此,使用SSE作为整数运算的直接替换将不会有任何好处,只有您可以同时对多个数据项执行操作时才会获得优势。这涉及加载一些在内存中连续的数据值,执行所需的处理,然后单步执行数组中的下一组值。

问题:

1如果代码路径依赖于正在处理的数据,则SIMD变得更难实现。例如:

a = array [index];
a &= mask;
a >>= shift;
if (a < somevalue)
{
  a += 2;
  array [index] = a;
}
++index;

像SIMD一样不容易做到:

a1 = array [index] a2 = array [index+1] a3 = array [index+2] a4 = array [index+3]
a1 &= mask         a2 &= mask           a3 &= mask           a4 &= mask
a1 >>= shift       a2 >>= shift         a3 >>= shift         a4 >>= shift
if (a1<somevalue)  if (a2<somevalue)    if (a3<somevalue)    if (a4<somevalue)
  // help! can't conditionally perform this on each column, all columns must do the same thing
index += 4

2如果数据不重要,那么将数据加载到SIMD指令中是很麻烦的

3代码是特定于处理器的。 SSE仅适用于IA32(Intel / AMD),并非所有IA32 cpus都支持SSE。

您需要分析算法和数据以查看它是否可以进行SSE并且需要了解SSE的工作原理。英特尔网站上有大量文档。

答案 2 :(得分:10)

这种问题是一个很好的低级分析器必不可少的完美例子。 (像VTune这样的东西)它可以让你更加了解你的热点在哪里。

我的猜测,根据您的描述,您的热点可能是使用if / else进行最小/最大计算导致的分支预测失败。因此,使用SIMD内在函数应该允许您使用最小/最大指令,但是,尝试使用无分支最小/最大计算可能是值得的。这可以通过减少疼痛来实现大部分收益。

这样的事情:

inline int 
minimum(int a, int b)
{
  int mask = (a - b) >> 31;
  return ((a & mask) | (b & ~mask));
}

答案 3 :(得分:6)

如果使用SSE指令,则显然仅限于支持这些指令的处理器。 这意味着x86,可以追溯到奔腾2左右(不记得确切的时候它们被引入,但它已经很久以前了)

SSE2,据我所知,是提供整数运算的,有点近期(Pentium 3?虽然第一款AMD Athlon处理器不支持它们)

在任何情况下,您都有两种使用这些说明的选项。要么在汇编中编写整个代码块(可能是一个坏主意。这使得编译器几乎不可能优化代码,并且人类很难编写高效的汇编程序。)

或者,使用编译器可用的内在函数(如果内存服务,它们通常在xmmintrin.h中定义)

但同样,表现可能无法改善。 SSE代码对其处理的数据提出了额外的要求。主要是要记住的是数据必须在128位边界上对齐。加载到同一寄存器中的值之间也应该存在很少或没有依赖关系(128位SSE寄存器可以容纳4个整数。将第一个和第二个加在一起并不是最佳的。但是将所有四个整数添加到相应的4个整数中另一个登记册会很快)

使用包含所有低级别SSE摆弄的库可能很诱人,但这也可能会破坏任何潜在的性能优势。

我不知道SSE的整数运算支持有多好,因此这也可能是限制性能的一个因素。 SSE主要针对加速浮点运算。

答案 4 :(得分:4)

如果您打算使用Microsoft Visual C ++,您应该阅读:

http://www.codeproject.com/KB/recipes/sseintro.aspx

答案 5 :(得分:3)

我们已经实现了一些图像处理代码,类似于你所描述的但是在字节数组上,在SSE中。与C代码相比,加速比较大,取决于精确算法的4倍以上,即使在英特尔编译器方面也是如此。但是,正如您已经提到的,您有以下缺点:

  • 可移植性。代码将运行在每个类似英特尔的CPU上,也运行在AMD上,但不会在其他CPU上运行。这对我们来说不是问题,因为我们控制目标硬件。切换编译器甚至是64位操作系统也是一个问题。

  • 你有一个陡峭的学习曲线,但我发现在你掌握了编写新算法的原则之后并不那么难。

  • 可维护性。大多数C或C ++程序员不了解汇编/ SSE。

我的建议是,只有当你真的需要提高性能时才能实现它,并且你无法在像intel IPP这样的库中找到问题的功能,如果你能解决可移植性问题

答案 6 :(得分:3)

我可以从我的经验中看出,SSE带来了超过普通c版本代码的巨大(4倍及以上)加速(没有内联asm,没有使用内在函数)但是手动优化的汇编程序可以胜过编译器生成的汇编,如果编译器无法弄清楚程序员的意图(相信我,编译器不会涵盖所有可能的代码组合,他们永远不会)。 哦,编译器不能每次都以最快的速度布局它运行的数据。 但是你需要很多经验来加速英特尔编译器(如果可能的话)。

答案 7 :(得分:2)

SSE指令最初只是在英特尔芯片上,但最近(因为Athlon?)AMD也支持它们,所以如果你对SSE指令集进行编码,你应该可以移植到大多数x86触发器。

话虽这么说,除非你已经熟悉x86上的汇编程序,否则学习SSE编码可能不值得 - 一个更简单的选择可能是检查编译器文档并查看是否有选项允许编译器自动生成SSE代码。一些编译器以这种方式很好地矢量化循环。 (听到英特尔编译器在这方面做得很好,你可能并不感到惊讶:)

答案 8 :(得分:2)

编写有助于编译器了解您正在做什么的代码。 GCC将理解并优化SSE代码,例如:

typedef union Vector4f
{
        // Easy constructor, defaulted to black/0 vector
    Vector4f(float a = 0, float b = 0, float c = 0, float d = 1.0f):
        X(a), Y(b), Z(c), W(d) { }

        // Cast operator, for []
    inline operator float* ()
    { 
        return (float*)this;
    }

        // Const ast operator, for const []
    inline operator const float* () const
    { 
        return (const float*)this;
    }

    // ---------------------------------------- //

    inline Vector4f operator += (const Vector4f &v)
    {
        for(int i=0; i<4; ++i)
            (*this)[i] += v[i];

        return *this;
    }

    inline Vector4f operator += (float t)
    {
        for(int i=0; i<4; ++i)
            (*this)[i] += t;

        return *this;
    }

        // Vertex / Vector 
        // Lower case xyzw components
    struct {
        float x, y, z;
        float w;
    };

        // Upper case XYZW components
    struct {
        float X, Y, Z;
        float W;
    };
};

不要忘记在构建参数上使用-msse -msse2!

答案 9 :(得分:1)

虽然SSE特定于某些处理器(SSE可能相对安全,SSE2在我的经验中更少),但您可以在运行时检测CPU,并根据目标CPU动态加载代码。

答案 10 :(得分:1)

SIMD内在函数(例如SSE2)可以加快这种速度,但需要专业知识才能正确使用。它们对齐和管道延迟非常敏感;粗心使用可能会使性能比没有它们时更糟糕。通过简单地使用缓存预取来确保所有的整数都在L1中以便您对它们进行操作,您将获得更加轻松和更直接的加速。

除非你的功能需要每秒优于100,000,000个整数的吞吐量,否则SIMD可能不值得为你带来麻烦。

答案 11 :(得分:1)

只是简单地添加之前已经说过的关于不同CPU上可用的不同SSE版本的内容:可以通过查看CPUID指令返回的相应功能标志来查看(有关详细信息,请参阅英特尔的文档)。 p>

答案 12 :(得分:1)

查看C / C ++的内联汇编程序,这里是DDJ article。除非你100%确定你的程序将在兼容的平台上运行,否则你应该遵循许多人在这里给出的建议。

答案 13 :(得分:1)

我同意之前的海报。好处可能非常大,但要获得它可能需要大量的工作。有关这些说明的英特尔文档超过4K页面。您可能想要从Ocali公司免费查看EasySSE(c ++包装库以及内在函数+示例)。

我认为我与EasySSE的关系很明确。

答案 14 :(得分:0)

我不建议你自己这样做,除非你对装配非常熟练。正如Skizz所指出的那样,使用SSE很可能需要对数据进行仔细的重组,而且这种好处通常是值得怀疑的。

编写非常小的循环并保持数据组织非常紧密并且依靠编译器为您执行此操作可能会好得多。英特尔C编译器和GCC(自4.1版)都可以自动矢量化您的代码,并且可能比您做得更好。 (只需在您的CXXFLAGS中添加-ftree-vectorize。)

编辑:我应该提到的另一件事是,有几个编译器支持程序集内在函数,它可能比IMO更容易使用,而不是asm()或__asm {语法。