【问题标题】:Using SSE instructions使用 SSE 指令
【发布时间】:2010-10-09 20:28:34
【问题描述】:

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

【问题讨论】:

  • SSE 特定于 x86 架构。此外,还有 SSE 版本(SSE2、SSE3、...),并非所有 SSE 功能都适用于每个处理器。正确使用 SSE 肯定会带来巨大的性能提升;但如果您需要优化,请仔细考虑。

标签: c++ optimization assembly processor sse


【解决方案1】:

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

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

类似这样的:

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

【讨论】:

    【解决方案2】:

    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 的工作原理。英特尔网站上有大量文档。

    【讨论】:

    • 问题1一般使用SIMD掩码指令解决。像 __m128 mask = _mm_cmplt_ps(a, somevalue); a = _mm_add_ps(a, _mm_and_ps(mask, _mm_set_ps1(2)); 对于 if(a
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

        编写有助于编译器理解您在做什么的代码。 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!

        【讨论】:

        • 注意:您不应该为每个单独的源打开 -msse 或 -msse2,因为编译器会尝试尽可能使用 sse,这意味着替换正常的 x86 寄存器操作。最好将针对 sse/avx/etc 编译的源代码保存在单独的文件中。
        【解决方案6】:

        除非您相当精通组装,否则我不建议您自己执行此操作。正如Skizz 所指出的那样,使用 SSE 很可能需要对您的数据进行仔细的重组,而好处通常是有问题的。

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

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

        【讨论】:

        • 我还没有看到 GCC 的自动矢量化器弊大于利,尽管我想它总能变得更好。
        • 新版本总是在特性和功能上不断进步。我听说 GCC 的矢量化器相当不错,并且在 4.3 版中更好,尤其是现在它是 -O3 中的默认设置。
        【解决方案7】:

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

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

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

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

            【讨论】:

              【解决方案10】:

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

              【讨论】:

                【解决方案11】:

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

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

                【讨论】:

                  【解决方案12】:

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

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

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

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

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

                  【讨论】:

                    【解决方案13】:

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

                    SSE2,据我的记忆,它是提供整数运算的版本,它是最近才出现的(Pentium 3?虽然第一代 AMD Athlon 处理器不支持它们)

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

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

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

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

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

                    【讨论】:

                      【解决方案14】:

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

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

                      【讨论】:

                      • 你不需要知道汇编就可以使用 simd 内部函数。例如x = _mm_mul_ps (y,z) 将 y 中的 4 个浮点数中的每一个乘以 z 中的 4 个浮点数,并将结果放入 x 中。这有多容易?
                      • @Mark:但那些 SIMD 内在函数只是程序集的包装器。要使用它们,你真的需要知道 SSE 指令是如何工作的,这意味着阅读机器操作。所以你确实需要了解 SSE 汇编。
                      【解决方案15】:
                      1. SSE 指令是特定于处理器的。您可以在 wikipedia 上查找哪个处理器支持哪个 SSE 版本。
                      2. SSE 代码是否更快取决于许多因素:首先当然是问题是内存限制还是 CPU 限制。如果内存总线是瓶颈,SSE 将无济于事。尝试简化你的整数计算,如果这样可以使代码更快,它可能是 CPU 密集型的,你很有可能加快它。
                      3. 请注意,编写 SIMD 代码比编​​写 C++ 代码要困难得多,而且生成的代码更难更改。始终保持 C++ 代码是最新的,您会希望它作为注释并检查您的汇编代码的正确性。
                      4. 考虑使用像 IPP 这样的库,它实现了针对各种处理器优化的常见低级 SIMD 操作。

                      【讨论】:

                      • "如果内存总线是瓶颈,SSE 将无济于事。" - 这忽略了流内存操作。
                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2012-02-27
                      • 2016-04-15
                      • 2019-05-29
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多