29

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

4

15 回答 15

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

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 cpu 都支持 SSE。

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

于 2009-02-25T16:24:23.673 回答
11

这类问题是一个很好的低级分析器必不可少的完美示例。(类似于 VTune)它可以让您更清楚地了解热点所在的位置。

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

像这样的东西:

inline int 
minimum(int a, int b)
{
  int mask = (a - b) >> 31;
  return ((a & mask) | (b & ~mask));
}
于 2009-02-26T16:19:09.153 回答
6

如果您使用 SSE 指令,您显然受限于支持这些指令的处理器。这意味着 x86,可以追溯到 Pentium 2 左右(不记得它们是什么时候引入的,但那是很久以前的事了)

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

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

或者,使用编译器提供的内在函数(如果有记忆,它们通常在 xmmintrin.h 中定义)

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

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

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

于 2009-02-25T16:15:15.333 回答
4

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

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

于 2009-02-25T16:19:03.000 回答
3

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

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

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

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

我对您的建议是,只有在您确实需要性能改进,并且在英特尔 IPP 之类的库中找不到解决您的问题的函数,并且您可以忍受可移植性问题时,才去尝试它。

于 2009-02-25T16:16:19.047 回答
3

从我的经验中我可以看出,SSE 比纯 c 版本的代码(不使用内联汇编,不使用内在函数)带来了巨大的(4 倍或更高)加速,但如果编译器可以,手动优化的汇编器可以击败编译器生成的汇编。 t 弄清楚程序员的意图(相信我,编译器不会涵盖所有可能的代码组合,而且他们永远不会)。哦,编译器不能每次都以最快的速度对其运行的数据进行布局。但是你需要很多经验来加速英特尔编译器(如果可能的话)。

于 2010-07-10T22:11:28.070 回答
2

SSE 指令最初只在 Intel 芯片上,但最近(因为 Athlon?)AMD 也支持它们,所以如果你针对 SSE 指令集编写代码,你应该可以移植到大多数 x86 procs。

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

于 2009-02-25T16:12:09.197 回答
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 !

于 2009-02-27T08:44:16.027 回答
1

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

于 2009-02-25T16:31:16.980 回答
1

SIMD 内在函数(例如 SSE2)可以加快这类事情的速度,但需要专业知识才能正确使用。它们对对齐和流水线延迟非常敏感;粗心的使用会使性能比没有它们时更差。通过简单地使用缓存预取来确保您的所有 int 都及时处于 L1 中,以便您对它们进行操作,您将获得更容易和更直接的加速。

除非您的函数需要每秒超过 100,000,000 个整数的吞吐量,否则 SIMD 对您来说可能不值得麻烦。

于 2009-02-26T08:43:04.367 回答
1

只是简单地补充一下之前所说的关于不同 CPU 上可用的不同 SSE 版本的内容:这可以通过查看 CPUID 指令返回的相应功能标志来检查(有关详细信息,请参见例如 Intel 的文档)。

于 2009-02-26T11:49:12.610 回答
1

看看C/C++的内联汇编器,这里有一篇DDJ 文章。除非您 100% 确定您的程序将在兼容的平台上运行,否则您应该遵循许多人在此处给出的建议。

于 2009-02-26T12:01:51.643 回答
1

我同意以前的海报。好处可能很大,但要获得它可能需要大量工作。有关这些说明的英特尔文档超过 4K 页。您可能想查看来自 Ocali Inc. 的免费 EasySSE(内部函数 + 示例的 c++ 包装器库)。

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

于 2011-11-29T20:07:09.597 回答
0

我不建议您自己这样做,除非您非常精通组装。正如Skizz所指出的,使用 SSE 很可能需要对数据进行仔细的重组,而其好处往往充其量是值得怀疑的。

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

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

于 2009-02-25T18:01:38.027 回答