Xxhash: 具有 >128 字节输入的 ARM 和 WASM 上的 XXH3 性能问题

创建于 2019-06-06  ·  38评论  ·  资料来源: Cyan4973/xxHash

我从 0.7.0 版本开始玩 XXH3,并且在 Arm64 和 WebAssembly 上,数据大小 >128 字节,但在 x64 上没有出现奇怪的性能下降。

这是 x64(Xcode 10.1 Clang,在 MacBookPro 上),看起来很棒:
xxh-x64

然而,在 Arm64(iPhone SE,使用 Xcode 10.1 Clang 编译)上——注意 128 字节输入大小后的大幅下降
xxh-arm64

在 WebAssembly(Firefox 67,在同一个 MacBookPro 中运行)上,128 字节后也有下降,其中 XXH3 变得比以前的 xxHash 慢得多:
xxh-wasm

所有38条评论

谢谢@aras-p,这很有启发性。

128 字节是“小输入”和“大输入”模式之间的限制。
“大输入”模式高度依赖向量指令的性能。
这对SSE2AVX2效果更好,因为它是为这些设计的。
@easyaspi314NEON创建了一个特定的变体,这非常有帮助。

但是,很难判断NEON代码路径是否在您的测试中正确触发。
源代码应该自动感知本地条件,但构建宏魔法可能会丢失它。
另一种方法是使用XXH_VECTOR构建宏强制实现使用特定的代码路径。 对于NEON ,代码是3 ,它给出gcc-DXXH_VECTOR=3

Web Assembly 不太可能生成矢量指令,至少目前还没有。
所以它可能依赖于通用的“标量”代码。
v0.7的标量代码没有得到适当的优化,过度依赖编译器来进行优化转换。
在当前的dev分支中,它已被重写,以一种更容易被大多数编译器优化的方式,提供了总体上不错的性能改进( clang甚至能够对其进行自动矢量化.. .)

重写的“标量”模式的性能预计与XXH32一样好或更好。

但是,很难判断测试中是否正确触发了 NEON 代码路径

它被正确触发,所以🤷‍♀

我将很快测试 dev 分支。

测试了 dev 分支,在 WebAssembly 上,在更大的输入上它确实变得更快,但没有你老 xxHash 快:
Screen Shot 2019-06-07 at 8 56 50 AM

在 ARM (iPhoneSE) 上,速度仍然差不多。 NEON 和 SCALAR 代码路径在 >128 字节输入处的吞吐量下降。

使用 Visual Studio 2017(在当前开发分支上)在 x64 上进行测试——SSE 路径不会启动,因为代码仅检查__SSE2__不是由 VS 开箱即用定义的。 将检查扩展为:

#  elif defined(__SSE2__) || defined(_M_AMD64) || defined(_M_X64)
#    define XXH_VECTOR XXH_SSE2

使得在x64上自动使用SSE2代码路径,效果更好。 然而,当输入数据大小在 128-500 字节之间时,它仍然会输给 xxHash64
Clipboard02

感谢 @aras-p 的宏建议。 它将对 Visual 非常有用。 事实上, SSE2保证出现在任何x64 cpu 上,所以如果它是一个更常见的构建宏,为什么不直接寻找这个属性。

我将再次查看 ARM 的性能,现阶段可能会令人失望。 ARM 性能的一个问题是它从一个芯片到另一个芯片,从一个编译器到另一个芯片是如此不同。 不幸的是,从单个测试中得出结论要困难得多,这使开发变得复杂。

关于 Wasm:感谢这个非常有趣的测试! 我们知道标量路径比 64 位指令集上的XXH64慢。 它仅被设计为在 32 位和 64 位指令集上都比XXH32快,这意味着它在 32 位指令集上比XXH64快得多。 这里令人惊讶的部分是我期望 Wasm 使用 32 位指令集,在这种情况下, XXH3应该支配XXH64 。 在您的测试中显然不是这种情况。 所以问题是:Wasm 现在生成 64 位可执行文件了吗?

关于性能差距:
是的,众所周知,在达到“长模式”方面存在差距。 这种模式具有一些非平凡的启动成本,需要时间来收回。 预计大约 512 字节后,性能应该会恢复到标准水平,但具体里程数会有所不同,具体取决于 CPU 类型和编译器版本。
为了缩小这种差距,一种可能性是将短模式扩展到更大的输入。
到目前为止,由于担心核心哈希函数变得太大,这可能不利于指令缓存,因此避免了该解决方案。 指令缓存污染不能用综合散列测试来衡量,因为它只在混合工作负载中很重要(当散列只是更长管道的一部分时)。 未展开的循环可能有助于在没有这种副作用的情况下到达那里。 它会使分支预测器有点混乱,但我怀疑这种影响只对短输入很重要,因此中等大小的输入可能会负担得起。

我会看看这种可能性,看看它是否可以顺利集成到当前的设计中。

Wasm 现在生成 64 位可执行文件吗

我的理解是 Wasm 具有本机 64 位类型(请参阅 https://github.com/WebAssembly/design/blob/master/Semantics.md#64-bit-integer-operators),它们可能几乎 1:1 映射到硬件说明,但“指针大小”是 32 位(请参阅 https://github.com/WebAssembly/design/blob/master/Semantics.md#addressing),因此从这方面来看,它是一个 32 位平台。 但是我认为在实际的 64 位 CPU 上运行时,64 位操作应该是“快速的”。

所以它有点等同于 Linux 中
有趣的是,这是一个微妙的区别。 事实上,它使wasm对 64 位更加友好。

顺便说一下,Wasm 似乎甚至可以使用SSE2
https://emscripten.org/docs/porting/simd.html

要启用 SIMD,请在编译时传递 -msimd128 标志

哦,原来 Xcode 在为 iOS 编译时默认执行-Os优化级别,我没有注意到。 XXH3_accumulate_512内的内循环没有展开。 用-O2编译可以得到更好的画面:

Screen Shot 2019-06-07 at 10 36 15 PM

对困惑感到抱歉! MSVC 和 WebAssembly 点仍然存在,因为它们分别用/O2-O2编译。

MSVC 和 WebAssembly 点仍然存在,因为它们分别是用 /O2 和 -O2 编译的。

尝试为 WebAssembly 启用 SIMD 标志听起来合理吗?

尝试为 WebAssembly 启用 SIMD 标志听起来合理吗?

也许吧,但如果没有一些额外的工作,就不会变魔术。 因为它要求代码不使用 SSE2 或 NEON 等内在函数,而是使用 Clang 的特殊“向量类型”扩展(这看起来很像着色器语言——例如float4等)。

我也不确定浏览器对 SIMD 的支持是什么,因为今天它看起来仍然是“实验性的”。 也许有一天我会玩弄它,敬请期待!

哇,好点:
这根本不是关于启用英特尔的内在函数! 很明显,回想起来......
因此,它完全依赖于标量代码路径
(或为clang向量扩展设计的新的特定的)。

关于自动矢量化有一个有趣的词:

这也将打开 LLVM 的自动矢量化通道,因此无需修改源代码即可从 SIMD 中受益。

我提到它是因为我记得最近版本的 XXH3( dev分支)的 LLVM 自动矢量化给我留下了深刻的印象。 它不如SSE2代码路径好,但仍然获得了巨大的性能提升。

| XXH_矢量| 苹果 llvm 10.0 | gcc v8.3.0 |
| --- | --- | --- |
| 0 "标量" | 24 GB/秒 | 11 GB/秒 |
| 1 "sse2" | 30 GB/秒 | 27 GB/秒 |
| 2 "avx2" | 55 GB/秒 | 45 GB/秒 |

请注意标量路径的巨大速度差异,我将其解释为“llvm 能够自动矢量化标量路径”。

根据您的建议,在 Visual 的dev分支中更新了SSE2检测宏

正如您的图表所建议的那样,最好将“短模式”扩展到更长的距离。
midsize功能分支中,我试图为这个扩展找到一个可接受的阈值。

似乎不会有“适合所有人的合适尺寸”。 “理想”阈值因目标和编译器版本而异。 目前,此功能分支确定为 384 字节。 它可能有点高,因为在使用默认编译器时,我可以在 macOS 笔记本电脑上测量 250-384 范围内的性能损失。 在同一台笔记本电脑上使用gcc时,情况有所不同。 我可以猜测不同的 cpu 或编译器会得到不同的结果。

无论如何,如果您认为阈值最好设置在某个不同的值上,我将不胜感激。

这对编译器来说确实很重要。 Clang 确实很擅长这段代码,但我发现 MSVC 和 GCC 都在努力生成体面的代码。 IDK 为什么,很明显我们想要什么代码......:confused:

我考虑过像我们为 XXH32/64 做的那样手动部分展开。

在大多数平台上,每个分支两次迭代似乎是有益的,任何更大的迭代似乎都不一致。 然而,x86 似乎更喜欢展开整个事情。 :思维:

我会试着在下周左右重新开始工作。 我还有几节课要完成。

至于门槛,我觉得稍微提高一点也没什么不好。

好的,所以我一直在周末进行一系列性能测试,以在短模式和长模式之间找到更精确的“阈值”。

使用gcc v8.3.0,测试的384字节阈值感觉比较好:正好在这一节短模式和长模式的性能重叠(不是一条直线,没有“单点”的地方)变得平等)。

但是对于clang v8.0,384 太大了。 速度损失(使用“短”模式时)在达到 200 字节之前开始。 他们只会在之后增加。

对于Visual ,384 太小了! 那是因为达到“长模式”(使用SSE2 )的性能悬崖是极端的:从“短”到“长”模式有 4 倍的因素! 我必须说我没想到会有这么大的影响,因为我每天主要使用gccclang 。 在短模式和长模式性能开始重叠之前, Visual需要将近 1 KB 的输入。

因此,根据这些数字,我目前正在考虑将阈值更新为 288 字节之类的内容,只留下一个减少的部分,其中对gccclang性能都是不利的,方向相反,并表示差异“足够合理”。

对于 Visual,与当前阈值(128 字节)相比,这只会有一点帮助,通过延迟发生性能悬崖的阈值,并使其更小(<2x)。 但它仍然会在那里。

我怀疑 Visual 需要专门的调查,以便驯服或解决在达到SSE2模式时观察到的性能悬崖。

为了了解编译器之间的性能差距,在 300 字节输入时,假设使用SSE2长模式(现在),Visual 生成 ~26M 哈希/秒(吞吐量,固定大小), gcc-8达到 ~42M 哈希/秒,而clang v8 达到 ~58M 哈希/秒。 所以这是一个相当大的差异。

_注意_:相比之下,“短”模式的性能差异要轻得多:仍然假设输入 300 字节,吞吐量测试将gcc-8置于 ~49M.H/s,而 Visual 和clang是 ~46M.H/s。 clang对于矢量代码来说更好,希望这是长期趋势:编译器会改进,并且在未来的某个时候,最终会变得和今天的clang一样好。

很棒的调查! 顺便说一句,您用于测试的 MSVC 版本是什么? 我的测试是在VS2017 15.9版本上进行的; 查看发行说明,他们据说在 VS2019 中改进了与 SIMD 相关的优化(但我自己没有检查过)。

是的,长模式的 384 字节截止听起来是一个很好的平衡。

我使用的是 Visual Studio 2017,但我没有检查确切的版本。

我想我有另一个带有 Visual Studio 2019 的 VM,所以我可以查看一下。
这个 VM 的问题在于,由于环境噪声,它不适合精确的基准测试,但它应该足以评估矢量性能是否得到改善。

_edit_:据我所知,Visual 2019 对切换到SSE2模式的性能影响没有太大帮助。 当使用电流截止阈值(128-129 字节)从“短”模式切换到“长”模式时,我发现大致相同的 x4 影响。

我找到了 MS 博客,他们在那里宣布了 VS2019 的矢量代码优化:
https://devblogs.microsoft.com/cppblog/game-performance-and-compilation-time-improvements-in-visual-studio-2019/

我无法直接比较 VS2017 与 VS2019。 可能2019年确实更好。 就启动时间而言,它仍然很糟糕。 “稳”性能是体面通过(22 GB /秒,比26 GB / s的gcc和28 GB / s的clang )。

嗨@aras-p,你有什么建议来测试 Wasm 性能?

有关信息,
dev分支,代码已经更新,为即将到来的流媒体模式做准备。
需要此步骤以允许用户提供任何类型的“秘密”,以更改哈希结果。

此更改的一个副作用是,在启用AVX2代码生成时,Visual Studio 2019 上的速度已大大提高。 在我的笔记本电脑上,由于某种原因,它现在达到了 ~50 GB/s,比SSE2版本快 2 倍以上。 SSE2路径并没有获得多少收益,收益不到 10%,可能是因为它已经是正确的。

我仍然必须集成“中型”补丁。 我必须首先确保此扩展不会在考虑的范围内引入不良质量的散列。

你会推荐什么来测试 Wasm 的性能

我的小测试平台在这里, https://github.com/aras-p/HashFunctionsTest——我可以尝试最新的dev分支,看看 Wasm 上是否有任何变化

标量代码自上次测试以来没有太大变化,因此预计结果是相似的。

一个目标是在构建链的某处引入-msimd128标志,看看clang编译器是否能够自动矢量化 wasm 中的标量代码路径,就像它可以自动-vectorize 原生 x64 程序集。

嗯,有趣的是,在最新的开发( ac8c5b2 )上,它基本相同,然后开始稍微慢一些,超过 128 字节和高达 1kb,然后与以前的版本相比,性能下降超过 1kb 输入大小。 我在 Chrome 75 和 Firefox 67 上的 Wasm 中都看到了这一点。
Screen Shot 2019-06-13 at 11 32 54 AM

我尝试为 Emscripten 使用-msimd128参数,但最终结果是SIMD is used, but not supported in WASM mode yet错误。 据我所知,我正在使用最新的稳定工具链,所以在 Wasm 的这一点上,整个 SIMD 考验可能仍然“为时过早”。

1 KB 的下降真的很奇怪,而且真的很强大。

想到的一件事是它可能对应于“加扰器”阶段,当使用所有默认参数时,每 KB 都会发生这种情况。
这个阶段虽然对于确保输入的良好混合很重要,但在 CPU 使用方面应该是微不足道的。 它不应该在图表中可见。

目前还不清楚为什么会这样。 它一定发生在最近的修改中,因为在以前的测量中不是这种情况。 我会调查的。

所以,对于信息,
我注意到一个奇怪的行为,在这一行的代码注释中解释:
https://github.com/Cyan4973/xxHash/blob/dev/xxh3.h#L688

这是一种很难拥有一切的情况。

clang在自动矢量化SSE2 (26 GB/s) 方面做得很好,如果这个函数是static ,但如果它是FORCE_INLINE ( 13 GB/秒)。 另一方面,如果AVX2可用,如果此函数被内联,则clang将自动矢量化得很好(45 GB/s),而如果不是,它基本上会提供相同的结果为SSE2 (26 GB/s)。

另一方面,Visual Studio 显然更偏爱FORCE_INLINE ,其中AVX2 (45 GB/s) 的好处很大,而SSE2 (24 GB/秒)。

我一直无法测试 Wasm 性能(我安装emscripten两个系统都失败了,因为环境原因不同)。 但我怀疑在1 KB的怪下降可能是相关的,要么同样的功能,或可能给内联或没有这另外一个

如果有一种适合所有人的环境,生活就太简单了……

对于 Wasm 来说,内联 vs 静态 vs 非内联似乎没有太大影响。 是什么似乎帮助,在没有更多的表现为浸> 1KB数据大小,是消除这种从XXH3_accumulate ,即不再迫使UNROLL:

#if defined(__clang__) && !defined(__OPTIMIZE_SIZE__) && !defined(__ARM_ARCH)
#  pragma clang loop unroll(enable)
#endif

这是有道理的,需要翻译的代码更少

好的,我想我们可以对 Wasm 进行特殊处理,并在这种情况下删除展开语句。

出乎意料的是,这就是原因,因为这个展开语句已经存在一段时间了,它已经出现在v0.7.0 ,并且当时_not_ 产生了 1 KB 的如此大的下降。

为什么_现在_会成为一个问题是个谜。 也许FORCE_INLINE和展开的某种组合?..

奇怪的是,对于wasm ,我无法观察到 129 时的性能下降(嗯,有一个,但很小),也没有观察到 1 KB 的下降(几乎不存在),在我的测试平台。 请注意确定发生了什么,也许在确切的工具上有些不同?...

尽管如此,我仍将继续将“短模式”扩展到更长的长度,因为在本机代码上可以清楚地看到 129 字节的性能下降。

随着 128 位变体的最新更新合并,
XXH3或多或少已准备好发布新版本。

它集成了这里讨论的中型策略,以减少 128 字节后的性能差距。
64 位和 128 位变体都可以使用长输入的流模式。
两种变体的性能应该遵循大致相似的形状,64 位的通常快 15%。

如果您希望 @aras-p ,您可能有兴趣在此最新版本发布之前对其进行基准测试(在dev分支中),以便它有机会整合潜在的反馈。

好的,这是对各种平台/编译器的比较; 我只测试了 128 位变体,因为那是我感兴趣的 :) 在所有图表中,我都包含了 6 月 6 日( 0117f18 )以及当前的 xxHash dev 分支变体7 月 26 日的 dev 分支( f012d4e )。 x64 PC 测试还包括 Meow 哈希(使用 AES-NI 指令的哈希),并且正如预期的那样,它围绕其他通用哈希运行,最高可达 35GB/s 左右。 我的小测试平台项目和以前一样, https://github.com/aras-p/HashFunctionsTest

MacBookPro 2018(酷睿 i9 2.9GHz)、Xcode 10.2.1、x64。 性能下降转向更大的输入尺寸,并且在更大尺寸下,新的 XXH3 似乎比以前慢了一点。
xxh-mac

PC(AMD TR 1950X 3.4GHz)、Visual Studio 2017 (15.9)、x64。 与 Mac 类似,性能下降移到更大的尺寸,新的 XXH3 似乎比以前慢了一点,甚至更大的尺寸变得更快。
xxh-win64-vs2017

同上,仅适用于 Visual Studio 2019 (16.2)。 XXH3 结果在整个输入大小范围内都有所改善,我怀疑是由于 2019 年 SIMD 优化器代码生成的改进。一般性能配置文件类似。
xxh-win64-vs2019

iPhone/ARM(苹果 A9),ARM64。 中型的性能下降已经消失,但在较大的输入尺寸下,散列比以前慢。
xxh-ios-a9

现在用于 32 位 ish 平台。 WebAssembly,在 2018 年 MacBookPro、Chrome 75 上。一般性能配置文件类似,在较大的输入尺寸下散列稍微慢一些。
xxh-wasm-chrome75

PC (AMD TR 1950X 3.4GHz)、Visual Studio 2017 (15.9)、x86 即 32 位。 XXH3 中的某些内容发生了变化,现在在更大的尺寸下它_快得多_,很好!
xxh-win32-vs2017

同上,仅适用于 Visual Studio 2019 (16.2)。 非常相似的结果。
xxh-win32-vs2019

感谢@aras-p 的非常彻底和有见地的分析。

一些想法:

新版本的速度:我希望新的 XXH128 比以前的版本慢 10%。 这是更改设计以回答对先前版本的不满的结果,即 64 位混合器的组合不会产生 128 位散列。 如果目标是加密级别的散列,我会同意,但我不相信它对于仅提供良好分散质量的快速散列那么重要,因为需要非常协调的场景才能将 128 位散列降级为 64-一点。 然而,这个话题有一些两极分化,我不想在这上面浪费精力。 因此,现在,每个输入都会立即影响累加器的 128 位。 这转化为一些额外的运行时成本,这感觉合理,但仍然是成本。 我也很惊讶它有时比以前的版本更快,但也许在新版本中更好地“简化”了一些东西。

性能下降:128 位最终混频器的固定成本高于 64 位,这是可以预料的。 因此,稀释该成本需要更多时间,这意味着中型性能下降更大。 但是为XXH3_64bit()引入并为XXH3_128bit()重新采用的新中型模式仍然使用与 64 位相同的大小阈值。 目标是使其与用于流模式的现有XXH3_state_t对象兼容,并共享大部分流代码。 这个可以讨论。 如果这被认为是次优选择,则可以对其进行更新。

与基于AES的算法比较: SSE2不足以面对专用硬件。 XXH3旨在利用AVX2 ,如果可用,以达到类似的性能领域。 例如,在我的笔记本电脑上,“大”数据达到 ~45 GB/s。 然而,虽然SSE2是任何x64 cpu 的保证能力,但后来的 SIMD 版本不是。 我怀疑这就是未对AVX2进行基准测试的原因。

Wasm 性能:是的,我们仍然存在这个问题,在其核心,XXH3 是使用 32 位操作的可向量化算法,并且在面对 64 位指令集_没有_向量化能力时效率相对较低。 这通常不会发生( x64SSE2 _necessively_, arm64NEON _necessously_ 等)但wasm是其中之一情况确实如此。 恐怕我还没有一个很好的解决方案......

Visual Studio x86 性能:我认为您的新检测宏可以让 Visual 选择 SSE2 模式,这可能是实现如此出色加速的原因:) 谢谢!

是的,这一切都说得通,感谢详细解释! 确实与 Meow 哈希进行比较是不公平的,尤其是因为我也没有用 AVX2 编译 XXH3。


所以我正在检查 wasm 组件。

首先,让我们在 wasm 上强制执行手动 128 位乘法。

Clang 在 wasm 上定义了__uint128_t ,但会默默地调用__multi3

这不仅有函数调用的开销,而且还会导致两个冗余的 64->64 乘以零。 虽然这只会在 64 位目标上浪费几个周期,但您可能会在 32 位浏览器上看到每次乘以 25+ 个周期。

显然,基准测试会说实话。 @aras-p,您可以尝试将!defined(__wasm__) &&__SIZEOF_INT128__检查并再次运行替补席吗?

此外,尝试禁用展开编译指示。 __wasm__应该检查。

您可以尝试将!defined(__wasm__) &&__SIZEOF_INT128__检查并再次运行替补吗?

我已经测试了这个建议,它似乎运行良好,至少在我的笔记本电脑上。
性能似乎提高了约 10%。
此改进仅对使用 128 位乘法的小输入有效,大小从 9 到 240。
那还是受欢迎的。 我当然可以将它添加到xxh3.h

_更新_:完成

尝试禁用展开编译指示。 __wasm__ 也应该被检查。

应该已经在这个版本中完成了:
https://github.com/Cyan4973/xxHash/blob/dev/xxh3.h#L763

尽管检查使用__EMSCRIPTEN__构建宏。

我想这个话题现在可以结束了,因为引入了 len <= 240 的“中间范围”算法已经改善了情况。 当然,它可以根据需要重新打开。

此页面是否有帮助?
0 / 5 - 0 等级