【问题标题】:Generate code for multiple SIMD architectures为多个 SIMD 架构生成代码
【发布时间】:2017-11-12 17:27:40
【问题描述】:

我编写了一个库,在其中使用 CMake 来验证 MMX、SSE、SSE2、SSE4、AVX、AVX2 和 AVX-512 的标头是否存在。除此之外,我检查指令是否存在,如果存在,我添加必要的编译器标志,-msse2 -mavx -mfma 等。

这一切都很好,但我想部署一个二进制文件,它适用于多代处理器。

问题:是否可以告诉编译器 (GCC),每当它使用 SIMD 优化函数时,它必须为架构列表生成代码?当然还要引入高级分支

我的想法类似于编译器为函数生成代码的方式,其中输入指针是 4 或 8 字节对齐的。为了防止这种情况,我使用了__builtin_assume_aligned 宏。

什么是最佳实践?多个二进制文件?命名?

【问题讨论】:

  • 这是英特尔编译器可以做的事情,并且在 libstdc++ 中也可以完成(虽然主要是手动 AFAIK)。在程序启动时进行一些能力测试,然后根据扩展指令集的可用性将关键功能分派到不同的版本。
  • GCC 也可以为特定处理器执行此操作,但我想列出一系列处理器并让它生成多个解决方案 - 最好包括高级分支。如果这不可能 - 是否有命名多个二进制文件的约定

标签: gcc simd avx sse4


【解决方案1】:

只要您不关心可移植性,就可以。

最新版本的 GCC 通过使用 target_clones 函数属性使这比我所知道的任何其他编译器都更容易。只需添加属性以及您要为其创建版本的目标列表,GCC 将自动创建不同的变体,以及在运行时自动选择版本的调度函数。

如果你想要更多的可移植性,你可以使用 target 属性,clang 和 icc 也支持,但你必须自己编写调度函数(这并不难),并多次发出函数次(通常使用宏,或重复包含标题)。

AFAIK,如果您希望您的代码与 MSVC 一起使用,您将需要具有不同选项的多个编译器调用。

【讨论】:

  • 谢谢。我正在为 *NIX 和 Windows 编译库,并使用相当旧的 gcc 版本 4.9。我将尝试target_clones 功能。对于 MSVC,我会努力解决的。
  • 不幸的是 target_clones 直到 gcc 6 才出现。
  • It is not as easy as you might think, @Jens。我不熟悉 GCC 的 target_clones 功能,但这看起来是一个聪明的创新。 MSVC 没有任何类似的东西,因此您将始终与这些工具作斗争。单独的 DLL 是唯一明智的解决方案。当然,您必须编写自己的所有动态调度逻辑。我个人更喜欢发布多个版本的 EXE,针对每种支持的架构进行了优化,并且可以通过安装程序动态选择。
  • 不需要单独的共享库;您可以通过编译定义了不同宏的同一文件来发布同一函数的多个版本(假设符号名称不同)。 dispatch 函数也很简单(我在github.com/nemequ/portable-snippets/tree/master/cpu 有一些代码,它做了很多工作)。从我的角度来看,困难的部分是构建系统。每个编译器针对不同的特性需要不同的标志,每个目标 CPU 有不同的特性,每个构建系统(autotools、cmake、meson 等)需要不同的实现。
  • @nemequ。我将在 github 上查看代码。我想花一些时间寻找一个好的解决方案。无论如何,代码都是开源的,但我想发布适用于多个平台的二进制文件。
【解决方案2】:

如果您只是在谈论让编译器生成 SSE/AVX 等指令,并且您有“通用”代码(即您没有使用内在函数显式矢量化,或者得到 lots 编译器将发现并自动矢量化的代码)那么我应该警告您,编译您的整个代码库的 AVX、AVX2 或 AVX512 可能会比编译运行明显适用于 SSE 版本。

当检测到使用寄存器上半部分的 AVX 操作码时,CPU 会启动电路的上半部分(否则会断电)。这会消耗更多功率,产生更多热量并降低芯片的基本时钟速度,通常会降低 10-20%,具体取决于高功率和低功率操作码的组合,因此您可能会立即损失 15% 的性能,然后在您开始看到任何收益之前进行大量矢量化处理以弥补这一性能缺陷。

this thread中查看我的详细解释和参考。

另一方面,如果您正在使用内在函数显式矢量化,并且您确定您有足够大的 AVX 等突发事件使其值得,我已经成功编写了代码,我告诉 MSVC 为 SSE2 编译(默认为x64) 但随后我动态检查 CPU 功能,并且一些功能切换到使用 AVX 内部函数实现的代码路径。

MSVC 允许这样做(它会产生警告,但您可以将其静音),但相同的技术在 GCC 4.9 下很难发挥作用,因为只有在使用适当的代码生成标志时编译器才会考虑声明内在函数。 [更新:@nemequ 在下面解释了如何在 gcc 下使用属性来装饰功能] 根据 GCC 的版本,您可能必须编译具有不同标志的文件才能获得一个可行的系统。

哦,您还必须注意 AVX-SSE 转换(当您离开 AVX 代码部分以返回 SSE 代码时调用 VZEROUPPER) - 可以做到,但我发现了解 CPU 影响是一场更大的战斗比我最初设想的要好。

【讨论】:

  • 一切都被明确地矢量化了。使用 gcc,一个测试程序执行 1.8 uop/clk。与优化标量代码相比,我得到了 87 倍的加速。我检查是否存在标题和选择代码路径的说明。使用 MSVC,我禁用 AVX2,除非明确使用它。启用 AVX2 会使其变慢。为多个对齐生成了许多不需要的代码。需要__builtin_assume_aligned
  • 在这种情况下,如果您可以使用 icc,您可能会发现它是最佳选择,因为它会自动为多个指令集生成代码路径并执行运行时调度。如果您必须继续使用 MSVC 和 gcc,我认为您必须在 MSVC 上进行自己的运行时调度,并在使用 gcc 4.9.x 的不同代码生成选项编译的模块之间进行运行时调度......以后的版本可能会很好非常不同的事情,但我现在卡在 4.9 上,所以不能说。
  • 哦,我不担心对齐....在现代芯片上,即使在执行 SSE 指令时,对齐地址的未对齐加载操作基本上与对齐加载操作一样快同一个地址。所以我到处使用未对齐的操作(但尽量确保分配是对齐的)——对我来说,增加的复杂性是不值得的。
  • GCC 允许您通过将 target 属性附加到函数来使用未在编译时声明的内在 ISA 扩展。见gcc.gnu.org/onlinedocs/gcc/…
  • 原来有一个选项。尝试将gcc -mprefer-avx128 -O3 -march=native 用于您的代码。 (128 位 AVX 指令不会触发 turbo 限制。)-mprefer-avx128 默认为 AMD Bulldozer (-mtune=bdver1) 启用。
猜你喜欢
  • 2011-07-02
  • 1970-01-01
  • 1970-01-01
  • 2019-11-03
  • 1970-01-01
  • 1970-01-01
  • 2015-06-09
  • 2011-04-07
  • 1970-01-01
相关资源
最近更新 更多