【问题标题】:An SSE Stdlib-esque Library?SSE Stdlib 风格的图书馆?
【发布时间】:2011-04-21 05:26:42
【问题描述】:

通常,我在“网上”遇到的与 SSE/MMX 相关的所有内容都是向量和矩阵的数学知识。但是,我正在寻找 SSE 优化的“标准函数”库,例如 Agner Fog 提供的库,或者 GCC 中的一些基于 SSE 的字符串扫描算法。

简要概括一下:这些内容包括 memset、memcpy、strstr、memcmp BSR/BSF,即从 SSE 指令构建的 stdlib-esque

我希望它们使用内在函数而不是汇编用于 SSE1(正式 MMX2),但两者都可以。希望这不会太宽泛。

更新 1

经过一番搜索,我发现了一些有前途的东西,一个图书馆引起了我的注意:

  • LibFreeVec:似乎只有 mac/IBM(由于基于 AltiVec),因此用处不大(对我来说),而且我似乎找不到直接下载链接,也没有说明支持的最低 SSE 版本李>

我还在一些向量化字符串函数(strlen、strstr strcmp)上遇到了an article。但是 SSE4.2 是我无法企及的方式(如前所述,我想坚持使用 SSE1/MMX)。

更新 2

Paul R 激励我做一些基准测试,不幸的是,由于我的 SSE 汇编编码经验接近 zip,我使用了其他人 (http://www.mindcontrol.org/~hplus/) 的基准测试代码并添加到其中。所有测试(不包括原始版本,即 VC6 SP5)均在 VC9 SP1 下编译,并具有完全/自定义优化和/arch:SSE on。

第一个测试是我的一台家用机器(AMD Sempron 2200+ 512mb DDR 333),上限为 SSE1(因此没有通过 MSVC memcpy 进行矢量化):

comparing P-III SIMD copytest (blocksize 4096) to memcpy
calculated CPU speed: 1494.0 MHz
  size  SSE Cycles      thru-sse    memcpy Cycles   thru-memcpy     asm Cycles      thru-asm
   1 kB 2879        506.75 MB/s     4132        353.08 MB/s     2655        549.51 MB/s
   2 kB 4877        598.29 MB/s     7041        414.41 MB/s     5179        563.41 MB/s
   4 kB 8890        656.44 MB/s     13123       444.70 MB/s     9832        593.55 MB/s
   8 kB 17413       670.28 MB/s     25128       464.48 MB/s     19403       601.53 MB/s
  16 kB 34569       675.26 MB/s     48227       484.02 MB/s     38303       609.43 MB/s
  32 kB 68992       676.69 MB/s     95582       488.44 MB/s     75969       614.54 MB/s
  64 kB 138637      673.50 MB/s     195012      478.80 MB/s     151716      615.44 MB/s
 128 kB 277678      672.52 MB/s     400484      466.30 MB/s     304670      612.94 MB/s
 256 kB 565227      660.78 MB/s     906572      411.98 MB/s     618394      603.97 MB/s
 512 kB 1142478     653.82 MB/s     1936657     385.70 MB/s     1380146     541.23 MB/s
1024 kB 2268244     658.64 MB/s     3989323     374.49 MB/s     2917758     512.02 MB/s
2048 kB 4556890     655.69 MB/s     8299992     359.99 MB/s     6166871     484.51 MB/s
4096 kB 9307132     642.07 MB/s     16873183        354.16 MB/s     12531689    476.86 MB/s

full tests

第二批测试是在大学工作站上完成的(Intel E6550、2.33Ghz、2gb DDR2 800?)

VC9 SSE/memcpy/ASM:
comparing P-III SIMD copytest (blocksize 4096) to memcpy
calculated CPU speed: 2327.2 MHz
  size  SSE Cycles      thru-sse    memcpy Cycles   thru-memcpy     asm Cycles      thru-asm
   1 kB 392         5797.69 MB/s    434         5236.63 MB/s    420         5411.18 MB/s
   2 kB 882         5153.51 MB/s    707         6429.13 MB/s    714         6366.10 MB/s
   4 kB 2044        4447.55 MB/s    1218        7463.70 MB/s    1218        7463.70 MB/s
   8 kB 3941        4613.44 MB/s    2170        8378.60 MB/s    2303        7894.73 MB/s
  16 kB 7791        4667.33 MB/s    4130        8804.63 MB/s    4410        8245.61 MB/s
  32 kB 15470       4701.12 MB/s    7959        9137.61 MB/s    8708        8351.66 MB/s
  64 kB 30716       4735.40 MB/s    15638       9301.22 MB/s    17458       8331.57 MB/s
 128 kB 61019       4767.45 MB/s    31136       9343.05 MB/s    35259       8250.52 MB/s
 256 kB 122164      4762.53 MB/s    62307       9337.80 MB/s    72688       8004.21 MB/s
 512 kB 246302      4724.36 MB/s    129577      8980.15 MB/s    142709      8153.80 MB/s
1024 kB 502572      4630.66 MB/s    332941      6989.95 MB/s    290528      8010.38 MB/s
2048 kB 1105076     4211.91 MB/s    1384908     3360.86 MB/s    662172      7029.11 MB/s
4096 kB 2815589     3306.22 MB/s    4342289     2143.79 MB/s    2172961     4284.00 MB/s

full tests

可以看出,SSE 在我的家庭系统上非常快,但落在 intel 机器上(可能是由于编码错误?)。我的 x86 汇编变体在我的家用机器上排名第二,在英特尔系统上排名第二(但结果看起来有点不一致,一个拥抱块它主导了 SSE1 版本)。 MSVC memcpy 赢得了英特尔系统测试,这是由于 SSE2 矢量化,但在我的家用机器上,它失败得很惨,即使是可怕的 __movsd 也胜过它......

陷阱:内存是 2 的所有对齐幂。缓存(希望)被刷新。 rdtsc 用于计时。

兴趣点:MSVC 有一个(未在任何参考中列出)__movsd 内在函数,它输出与我正在使用的相同的汇编代码,但它失败得很惨(即使是内联的!)。这可能就是它未上市的原因。

VC9 memcpy 可以在我的非 sse 2 机器上强制向量化,但是它会破坏 FPU 堆栈,它似乎也有一个错误。

这是我用来测试的full source(包括我的更改,再次感谢http://www.mindcontrol.org/~hplus/ 的原始版本)。项目文件的二进制文件可根据要求提供。

总之,切换变体似乎是最好的,类似于 MSVC crt 变体,只是更坚固,有更多选项和一次性检查(通过内联函数指针?或者像内部直接调用补丁这样更狡猾的东西),但是内联可能不得不使用最佳案例方法来代替

更新 3

Eshan 提出的一个问题提醒了一些有用的和与此相关的东西,虽然仅适用于位集和位操作,BitMagic 并且对于大型位集非常有用,它甚至在 SSE2 (bit) optimization 上有一篇不错的文章。不幸的是,这仍然不是 CRT/stdlib esque 类型库。似乎这些项目中的大多数都致力于特定的一小部分(问题)。

这提出了一个问题,是否值得创建一个开源的、可能是多平台性能的 crt/stdlib 项目,创建各种版本的标准化函数,每个版本都针对特定情况进行了优化,以及“最佳情况/通用函数变体,具有标量/MMX/SSE/SSE2+(à la MSVC)的运行时分支或强制编译时标量/SIMD 切换。

这对于 HPC 或对每一点性能都很重要的项目(如游戏)很有用,让程序员不必担心内置函数的速度,只需稍作调整即可找到最佳优化变体。

更新 4

我认为应该扩展这个问题的性质,包括可以使用 SSE/MMX 来优化非向量/矩阵应用程序的技术,这也可能用于 32/64 位标量代码。一个很好的例子是如何使用标量技术(位操作)、MMX 和 SSE/SIMD 一次检查给定 32/64/128/256 位数据类型中的字节是否出现

另外,我看到很多“只使用 ICC”的答案,这是一个很好的答案,这不是我的答案,因为首先,ICC 不是我可以连续使用的东西(除非英特尔有windows 的免费学生版),因为有 30 次试用。其次,更重要的是,我不仅关注库本身,还关注用于优化/创建它们包含的功能的技术,以供我个人改进和改进,因此我可以将这些技术和原则应用于我自己的代码(如果需要),结合使用这些库。希望这可以清除那部分:)

【问题讨论】:

  • 是什么让您认为这些功能尚未实现以尽可能使用 SSE?
  • @jalf:因为各种原因,我经常调试我的应用程序(使用 ollydbg),所以我知道它的输出代码,我可以看到它的缺点。我唯一一次看到一些 SSE 代码是针对 memset 的,当/arch:SSE2 开启时,它的 SSE2 代码却不会在我的系统上运行。也是一名逆向工程师,我从一般活动中知道 MSVC 如何实现 crt 功能:P

标签: c++ c visual-c++ assembly sse


【解决方案1】:

对于诸如 memset、memcpy 等计算量很少的简单操作,SIMD 优化没有什么意义,因为内存带宽通常是限制因素。

【讨论】:

  • 但是对于内存操作,您不能利用协处理器的能力进行处理,而是因为它能够以相同的延迟处理更大的数据集(一次 8、16 + 字节)使用内置的 x86 指令。 Fog 博士应该在他的 5 卷“指南”中的某个地方进行一些比较。是的,我知道这只对热点很重要,这就是我使用它的目的
  • @Necrolis: 你的加载/存储效率有多高并不重要——如果你可以用标量代码最大化你的内存带宽(这通常很容易,例如 memcpy、memset)然后进一步优化没有任何收获。
  • 问题是我没有用标量代码最大化它,除了非常小的缓冲区(尽管我将这部分归因于 MSVC 没有内联对 memset 的调用等,如果不是这种情况, __assume 可用于“强制”对齐副本,删除分支,即:当一切都是 long 的倍数时,为什么还要处理单词和字节的情况,那么它可能非常接近 SSE,至少在我的系统上)
  • @Necrolis:很可能是您的 memset/memcpy 实现效率低下,但这仍然不能证明 SIMD 实现是合理的 - 您几乎可以肯定为这些例程编写更有效的标量实现 将在不使用 SIMD 的情况下最大化内存带宽。然而,这是一个有趣的练习,你会在这个过程中学到很多东西,所以如果你有时间和兴趣,那就去做吧。
  • @Paul R - 我确实有一些 SSEx 通用功能,它们的性能优于相应的非 SSE 版本,尽管我不知道 如何 远非最佳非 SSE 版本是。因此,我想知道您是否有任何数据来支持您的索赔。标量代码与 SSE 一样高效吗?或者您能否为我们指出一些标量代码,在您看来,它是否会最大限度地利用内存带宽?
【解决方案2】:

也许是 libSIMDx86?

http://simdx86.sourceforge.net

【讨论】:

  • 虽然是一个不错的库,但它主要面向矩阵和矢量数学(我感兴趣的唯一部分是数学部分的 3 个生根函数)。
【解决方案3】:

您可以使用苹果或 OpenSolaris 的 libc。这些 libc 实现包含您要查找的内容。大约 6 年前,我一直在寻找这类东西,我不得不痛苦地写下来。

我记得很久以前参加过一个名为“fastcode”项目的编码竞赛。他们当时使用 Delphi 做了一些很棒的突破性优化。查看他们的结果页面。由于它是用 Pascal 的快速函数调用模型(将参数复制到寄存器)编写的,转换为 C 风格的 stdc 函数调用模型(推入堆栈)可能有点尴尬。尤其是这个项目很久没有更新了,没有为SSE4.2写代码。

Solaris -> src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/lib/libc/

Apple -> www.opensource.apple.com/source/Libc/Libc-594.9.1/

【讨论】:

  • 这些看起来很有希望,不幸的是我没有时间去探索 Apple/Solaris 库(对我来说就像一个迷宫般的文件夹)。快速的代码看起来确实不错,可惜不是所有的东西似乎都有源代码
  • 请注意:大多数此类实现将每个函数放在自己的文件中。所以你只需要搜索一些提到平台架构的目录,比如“x86”或“i386”,然后搜索以“.s”结尾的文件名。
  • @Necrolis, @Paul R:你们有没有遇到过使用 nVidia 或 ATI 等 GPU 的类似高速优化?可能吗?听说过很多,但从来没有机会看到任何实际使用它的组装材料。充其量我最终会得到 OpenGL 或 DirectX 调用,但不会低于此。
  • 您指的是 CUDA(nVidia) 还是 Stream(ATI)?如果是这样,那么是的,看看这个:bmagic.sourceforge.net/bmcudasse2.html & bmagic.sourceforge.net/bmcudasse2-2.html
【解决方案4】:

这是一篇关于如何使用 SIMD 指令向量化字符计数的文章:

http://porg.es/blog/ridiculous-utf-8-character-counting

【讨论】:

  • +1,非常好,只是它的 SSE2 有点可惜,需要将 GCC 内置插件映射到 MSVC :(
【解决方案5】:

我个人不会费心尝试编写 libc 函数的超级优化版本,试图以良好的性能处理所有可能的场景。

相反,为特定情况编写优化版本,在这种情况下,您对手头的问题有足够的了解,可以编写正确的代码......以及它在哪些方面很重要。 memsetClearLargeBufferCacheWriteThrough 之间存在语义差异。

【讨论】:

  • 是的,这就是为什么我同时提到一般用途的最佳案例和更具体的版本(并且可以通过定义进行配置)。我想我会在圣诞节假期在 github 上开始做一些事情,看看我能不能解决这个问题,然后我的问题归结为我的 SSE 知识很差,另一方面我的 x86 优化知识非常强。
【解决方案6】:

老实说,我要做的只是安装英特尔 C++ 编译器并学习各种可用的自动 SIMD 优化标志。我们在通过简单地使用 ICC 编译来优化代码性能方面拥有非常好的经验。

请记住,整个 STL 库基本上只是头文件,所以整个东西都会编译到您的 exe/lib/dll 中,因此可以根据需要进行优化。

ICC 有许多选项,您可以(最简单地)指定要定位的 SSE 级别。您还可以使用它来生成具有多个代码路径的二进制文件,这样如果您编译的最佳 SSE 配置不可用,它将运行为功能较弱的 SIMD CPU 配置的一组不同的(仍然优化的)代码.

【讨论】:

  • 当然,我必须在 30 天内完成所有这些,因为我没有钱购买“完整”许可证。我决定一种方法是做你推荐的事情,但改用 GCC 4.5.x。但是它仍然很耗时,我希望有人已经完成了其中的一部分。此外,STD 库并不总是被推送(静态链接)在二进制文件中,使用 MSVC,它会链接到 msvcrtxx.dll 以用于大多数重要的功能。
  • GCC 没有做与 ICC 相同的优化 - 这就是为什么有一个 ICC 的副本用于 linux 和专门编译的 Linux 内核,吹捧他们已经使用 ICC 编译的事实。我不是说 STD,而是说 STL。 STL 始终是静态链接的,IIRC。
【解决方案7】:

这是一个用 C 语言快速实现的 memcpy,如果需要,它可以替换标准库版本的 memcpy:

http://www.danielvik.com/2010/02/fast-memcpy-in-c.html

【讨论】:

  • 它是一个不错的链接,但是,他的版本下降得相当厉害,我的汇编版本几乎是两倍的速度,有时甚至超过一倍(第一个是他的,在 thru-c 下,第二个是我的,通过asm):necrolis.pastebin.com/pAFzJYr7
【解决方案8】:

strstr 很难优化,因为 (a) \0-termination 意味着无论如何您都必须读取每个字节, (b) 它必须在所有边缘情况下都很好。

话虽如此,使用 SSE2 操作,您可以将标准 strstr 提高 10 倍。 我注意到 gcc 4.4 现在将这些操作用于 strlen,但不适用于 其他字符串操作。 有关如何将 SSE2 寄存器用于 strlen、strchr、strpbrk 等的更多信息。 在mischasan.wordpress.com。请原谅我的超简洁代码布局。

#include <emmintrin.h> // Other standard #includes you can figure out...

static inline unsigned under(unsigned x)
    { return (x - 1) & ~x; }
static inline __m128i xmfill(char b)
    { return _mm_set1_epi8(b); }
static inline __m128i xmload(void const*p)
    { return _mm_load_si128((__m128i const*)p); }
static inline unsigned xmatch(__m128i a, __m128i b)
    { return _mm_movemask_epi8(_mm_cmpeq_epi8(a, b)); }

char const *sse_strstr(char const *tgt, char const *pat)
{
    unsigned    len = sse_strlen(pat);
    if (len == 0) return tgt;
    if (len == 1) return sse_strchr(tgt,*pat);
    __m128i     x, zero = {};
    __m128i     p0 = _m_set1_epi8(pat[0]), p1 = _m_set1_epi8(pat[1]);
    uint16_t    pair = *(uint16_t const*)pat;
    unsigned    z, m, f = 15 & (uintptr_t)tgt;
    char const* p;

    // Initial unaligned chunk of tgt:
    if (f) {
        z = xmatch(x = xmload(tgt - f), zero) >> f;
        m = under(z) & ((xmatch(x,p0) & (xmatch(x,p1) >> 1)) >> f);
        for (; m; m &= m - 1)
             if (!memcmp((p = tgt+ffs(m)-1)+2, pat+2, len-2))
                return p;
        if (z)
            return NULL;
        tgt += 16 - f;
        if (*(uint16_t const*)(tgt - 1) == pair
                && !memcmp(tgt+1, pat+2, len-2))
            return tgt - 1;
    }

    // 16-byte aligned chunks of tgt:
    while (!(z = xmatch(x = xmload(tgt), zero))) {
        m = xmatch(x,p0) & (xmatch(x,p1) >> 1);
        for (; m; m &= m - 1)
             if (!memcmp((p = tgt+ffs(m)-1)+2, pat+2, len-2))
                return p;
        tgt += 16;
        if (*(uint16_t const*)(tgt - 1) == pair && !memcmp(tgt+1, pat+2, len-2))
            return tgt - 1;
    }

    // Final 0..15 bytes of tgt:
    m = under(z) & xmatch(x,p0) & (xmatch(x,p1) >> 1);
    for (; m; m &= m - 1)
        if (!memcmp((p = tgt+ffs(m)-1)+2, pat+2, len-2))
            return p;

    return NULL;
}

【讨论】:

  • LLVM 有一个非常好的矢量化 strstr 用于跳过块 cmets(它具有 msvc、gcc 和 altivec 的分支,因此它也非常便携)。谢谢你的链接,我去看看
  • 这已经过时了。当前的 glibc 在可用的情况下具有 SSE 4.2 优化,其中包含快速字符串操作所需的指令。
  • glibc 是哪个版本的?我同意 glibc 进行提升和拱门是最好的。不幸的是,SSE4.2 仅支持英特尔是一个问题,而自 2003 年以来几乎所有与英特尔兼容的芯片都有 SSE2。对于非(intel SSE4.2)平台,我的 glibc strstr 似乎不太聪明:-(
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-02-05
  • 2022-07-20
  • 2015-05-27
  • 2012-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多